commit 648c392796e6058f653fa64895ec7a75cd3b097f Author: 彭真涛 <16362733+peng-zhentao@user.noreply.gitee.com> Date: Thu Aug 27 13:33:14 2026 +0800 初始化仓库 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b550dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +/.vs/AIK_WPFWin7/config/applicationhost.config +/.vs/AIK_WPFWin7/DesignTimeBuild/.dtbcache.v2 +/.vs/AIK_WPFWin7/FileContentIndex/220b08dd-7cab-4b06-b9ce-e2ff55f551fb.vsidx +/.vs/AIK_WPFWin7/FileContentIndex/7e16a2b3-374f-48b6-9586-0bfc63662446.vsidx +/.vs/AIK_WPFWin7/FileContentIndex/c1197851-d2be-4a27-a29f-f6dc9281684b.vsidx +/.vs/AIK_WPFWin7/FileContentIndex/e63df965-b67f-4efb-8b76-73728081e2d1.vsidx +/.vs/AIK_WPFWin7/FileContentIndex/ffe84a3e-16f3-47c8-9ac2-14fa66df514f.vsidx +/.vs/AIK_WPFWin7/v17/.futdcache.v2 +/.vs/AIK_WPFWin7/v17/.suo +/.vs/AIK_WPFWin7/v17/DocumentLayout.json +/.vs/AIK_WPFWin7/v17/fileList.bin +/.vs/AIK_WPFWin7/v18/.futdcache.v2 +/.vs/AIK_WPFWin7/v18/.suo +/.vs/AIK_WPFWin7/v18/DocumentLayout.backup.json +/.vs/AIK_WPFWin7/v18/DocumentLayout.json +/.vs/AIK_WPFWin7/v18/fileList.bin +/.vs/ProjectEvaluation/aik_wpfwin7.metadata.v10.bin +/.vs/ProjectEvaluation/aik_wpfwin7.projects.v10.bin +/.vs/ProjectEvaluation/aik_wpfwin7.strings.v10.bin + +# 编译生成目录(bin/obj) +[Bb]in/ +[Oo]bj/ +[Dd]ebug/ +[Rr]elease/ +x64/ +x86/ +bld/ +[Bb]uild/ +GeneratedFiles/ +*.user +*.suo +*.userosscache +*.sln.docstates diff --git a/AIK.Assets/AIK.Assets.csproj b/AIK.Assets/AIK.Assets.csproj new file mode 100644 index 0000000..9cea83d --- /dev/null +++ b/AIK.Assets/AIK.Assets.csproj @@ -0,0 +1,88 @@ + + + + net48 + enable + true + AIK.Assets + 12 + + + + + + + + + + + + + + + + + + + + + + + Always + + + Never + + + Always + + + Never + + + + Always + + + Never + + + + + + Never + + + Never + + + Always + + + Always + + + Never + + + Always + + + + + + Always + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Assets/Fonts/AlimamaShuHeiTi-Bold.ttf b/AIK.Assets/Fonts/AlimamaShuHeiTi-Bold.ttf new file mode 100644 index 0000000..b20919e Binary files /dev/null and b/AIK.Assets/Fonts/AlimamaShuHeiTi-Bold.ttf differ diff --git a/AIK.Assets/Fonts/digital_display.ttf b/AIK.Assets/Fonts/digital_display.ttf new file mode 100644 index 0000000..a006032 Binary files /dev/null and b/AIK.Assets/Fonts/digital_display.ttf differ diff --git a/AIK.Assets/Fonts/iconfont.ttf b/AIK.Assets/Fonts/iconfont.ttf new file mode 100644 index 0000000..7aa3ceb Binary files /dev/null and b/AIK.Assets/Fonts/iconfont.ttf differ diff --git a/AIK.Assets/Fonts/iconfont1.ttf b/AIK.Assets/Fonts/iconfont1.ttf new file mode 100644 index 0000000..a8e7367 Binary files /dev/null and b/AIK.Assets/Fonts/iconfont1.ttf differ diff --git a/AIK.Assets/Images/AIK.png b/AIK.Assets/Images/AIK.png new file mode 100644 index 0000000..ba5cc23 Binary files /dev/null and b/AIK.Assets/Images/AIK.png differ diff --git a/AIK.Assets/Images/AIKChina.ico b/AIK.Assets/Images/AIKChina.ico new file mode 100644 index 0000000..564db41 Binary files /dev/null and b/AIK.Assets/Images/AIKChina.ico differ diff --git a/AIK.Assets/Images/AIKChina.png b/AIK.Assets/Images/AIKChina.png new file mode 100644 index 0000000..d9fd87d Binary files /dev/null and b/AIK.Assets/Images/AIKChina.png differ diff --git a/AIK.Assets/Images/AIKInternationl.ico b/AIK.Assets/Images/AIKInternationl.ico new file mode 100644 index 0000000..9027bb9 Binary files /dev/null and b/AIK.Assets/Images/AIKInternationl.ico differ diff --git a/AIK.Assets/Images/AIKInternationl.png b/AIK.Assets/Images/AIKInternationl.png new file mode 100644 index 0000000..203efd4 Binary files /dev/null and b/AIK.Assets/Images/AIKInternationl.png differ diff --git a/AIK.Assets/Images/k3-tpms.png b/AIK.Assets/Images/k3-tpms.png new file mode 100644 index 0000000..9fc86e8 Binary files /dev/null and b/AIK.Assets/Images/k3-tpms.png differ diff --git a/AIK.Assets/Images/k3_tool.png b/AIK.Assets/Images/k3_tool.png new file mode 100644 index 0000000..080ac04 Binary files /dev/null and b/AIK.Assets/Images/k3_tool.png differ diff --git a/AIK.Assets/Images/k3_tool_logo.png b/AIK.Assets/Images/k3_tool_logo.png new file mode 100644 index 0000000..03adb3f Binary files /dev/null and b/AIK.Assets/Images/k3_tool_logo.png differ diff --git a/AIK.Assets/Images/logo.ico b/AIK.Assets/Images/logo.ico new file mode 100644 index 0000000..58bb011 Binary files /dev/null and b/AIK.Assets/Images/logo.ico differ diff --git a/AIK.Assets/Images/logo.png b/AIK.Assets/Images/logo.png new file mode 100644 index 0000000..1fcaccf Binary files /dev/null and b/AIK.Assets/Images/logo.png differ diff --git a/AIK.Assets/Images/useravatar.png b/AIK.Assets/Images/useravatar.png new file mode 100644 index 0000000..7249f29 Binary files /dev/null and b/AIK.Assets/Images/useravatar.png differ diff --git a/AIK.Assets/Languages/Strings.de.xaml b/AIK.Assets/Languages/Strings.de.xaml new file mode 100644 index 0000000..c70ce61 --- /dev/null +++ b/AIK.Assets/Languages/Strings.de.xaml @@ -0,0 +1,119 @@ + + AIK. Intelligentes Schluesselwerkzeug + Einstellungen + Sprache + App-Einstellungen + Grundparameter und Praeferenzen der Anwendung konfigurieren + + Oberflaecheneinstellungen + Cache-Einstellungen + Daten-Cache leeren + Oberflaechensprache: + Schluesselmodellliste + + Speichern + Abbrechen + Bestaetigen + + Nach Markenname, Englisch oder Pinyin suchen... + Nach Fahrzeugschluesselname, Englisch oder Pinyin suchen... + + Untergeraet: + Frequenz: + Chip: + Zeit: + ID: + + Informationen + + Einstellungen erfolgreich gespeichert + + Kontakt per WeChat + Kontakt per E-Mail + WeChat kopiert + E-Mail kopiert + Telefonnummer + Punkte + Abmelden + + Geraeteaktualisierung + Aktuelle Version + Neue Version + Geraetestatus + Geraetename + Jetzt aktualisieren + Version abrufen + App-Version + Serverversion + Geraeteversion + Versionsaktualisierung + Aktualisierungen fuer Firmware, Anwendung und Geraeteversionen + Schreibprotokoll + Schluesselschreibprotokoll + Punkte aufladen + Mitgliedschaft + Punkteverlauf + Ihre Punkte + Aufladebetrag eingeben + Zahlungsmethode auswählen + WeChat Pay + PayPal + Bezahlen + + Fernbedienungserkennung + Fernbedienung kopieren + HCS / Festcode kopieren + Klicken Sie unten auf die Test-Schaltflaeche, um die Frequenz zu erkennen. Nachdem das Geraet verbunden ist, druecken Sie eine Taste am Untergeraet innerhalb der Testreichweite. Danach wird die Frequenz angezeigt. + Klicken Sie unten auf die Test-Schaltflaeche, um die Frequenz zu erkennen. Nachdem das Geraet verbunden ist, druecken Sie eine Taste am Untergeraet innerhalb der Testreichweite. Danach wird die Frequenz angezeigt. + Modus: + + Erkennung + Diese Taste ueberspringen + Erneut pruefen + Kopieren starten + Verriegeln + Entriegeln + Kofferraum + Panik + + Keine passende Marke gefunden + Kein passender Suchbegriff gefunden + Keine Schluesseldaten verfuegbar + + Daten: + Typ: + Bitrate: + Bitlaenge: + + Programmierung starten + Hinweis + Empfehlen Sie es Ihren Freunden! Scannen Sie den QR-Code mit WeChat, QQ usw., um die App herunterzuladen und zu installieren. + Wir werden Schlosser auch weiterhin wie gewohnt unterstuetzen. + Scannen Sie, um die mobile App herunterzuladen + Neueste Informationen und Support erhalten + Mehr anzeigen + Laden... + Cloud-Daten + Jetzt aktualisieren + Spaeter aktualisieren + Neue Version gefunden + Update-Hinweise + + Cloud-Daten-Synchronisierung + Waehrend der Synchronisierung der Cloud-Daten wird das Geraet neu gestartet und die Verbindung wird voruebergehend getrennt. Das ist normal. Bitte warten Sie, bis das System die Geraetefunktionen erneut initialisiert hat. + Zur Verbesserung der Geraetestabilitaet koennen Sie die Systemeinstellungen anpassen: Systemsteuerung -> Energieoptionen -> Energiesparplaneinstellungen aendern -> Erweiterte Energieeinstellungen aendern -> USB-Einstellungen -> Einstellung fuer selektives USB-Energiesparen -> [Deaktiviert] + Hinweis: Fuehren Sie den Vorgang nur bei verbundenem Geraet aus + USB-Laufwerk oeffnen (Test) + USB-Laufwerk schliessen (Test) + Datei auswaehlen + Ordner auswaehlen + Geraet aktiv halten + Cloud-Daten synchronisieren + + + + + + diff --git a/AIK.Assets/Languages/Strings.en.xaml b/AIK.Assets/Languages/Strings.en.xaml new file mode 100644 index 0000000..8597137 --- /dev/null +++ b/AIK.Assets/Languages/Strings.en.xaml @@ -0,0 +1,117 @@ + + + AIK.Smart Key Tool + Settings + Language + App Settings + Configure the basic parameters and preferences of the application + + Interface Settings + Cache Settings + ClearCache + Interface Language: + Key Model List + + Save + Cancel + Confirm + + Search by brand name, English, or pinyin... + Search vehicle key name, English, or pinyin... + + Series: + Frequency: + Chip: + Time: + ID: + + Information + + + Settings saved successfully + + Contact us on wechat + Contact us by email + Contact us by email Copy Success + Contact us by email Copy Success + PhoneNumber + Integral + Log out + DeviceUpdate + CurrentVersion + NewVersion + DeviceStatus + DeviceName + Upgrade + get version + AppVersion + ServerVersion + DeviceVersion + Version upgrade + Upgrades for firmware, application and device versions + Burn Log + Key Burn Log + Integral Recharge + Membership + Integral Record + Your Points + Enter recharge amount + Select payment method + WeChat Pay + PayPal + Pay + Remote Detection + Remote Copy + HCS / Fixed-Code Copy + Click the Test button below to detect the frequency. After connecting the device, press a button on the sub-unit within the testable range, and the frequency will be displayed. + Click the Test button below to detect the frequency. After connecting the device, press a button on the sub-unit within the testable range, and the frequency will be displayed. + Mode: + + Detection + Skip this key + Recheck + Start copying + Lock + Unlock + Trunk + Panic + + No matching brand found + No matching brand found + No key data available + + Data: + Type: + Bitrate: + Bit length: + Start Burning + Tip + Recommend it to your friends! Scan the QR code with WeChat, QQ, etc. to download and install. + We will continue to support locksmiths as always. + Scan to download the mobile app + Get the latest news and support + View More + Loading... + Cloud Data + Update Now + Update Later + New Version Found + Update Tips + Cloud Data Sync + During cloud data sync, the device will restart and the connection will temporarily disconnect. This is normal. Please wait for the system to reinitialize the device functions. + To improve device stability, adjust the system settings: Control Panel -> Power Options -> Change plan settings -> Change advanced power settings -> USB settings -> USB selective suspend setting -> [Disabled] + Reminder: Please operate while the device is connected + Open USB Drive (Test) + Close USB Drive (Test) + 📁 Select File + 📂 Select Folder + Keep Device Alive + Sync Cloud Data + + + + + + diff --git a/AIK.Assets/Languages/Strings.es.xaml b/AIK.Assets/Languages/Strings.es.xaml new file mode 100644 index 0000000..abb19a9 --- /dev/null +++ b/AIK.Assets/Languages/Strings.es.xaml @@ -0,0 +1,116 @@ + + + AIK.Herramienta de Llave Inteligente + Configuración + Idioma + Configuración de la Aplicación + Configurar los parámetros básicos y preferencias de la aplicación + + Configuración de Interfaz + Configuración de Caché + Limpiar Caché de Datos + Idioma de la interfaz: + Lista de Modelos de Llave + + Guardar + Cancelar + Confirmar + + Buscar nombre de marca, inglés o pinyin... + Buscar nombre de llave de vehículo, inglés o pinyin... + + Llave Remota: + Frecuencia: + Información de Referencia: + Hora: + ID: + + Información de Referencia + + Configuración guardada correctamente + + Contáctanos por WeChat + Contáctanos por Correo Electrónico + Copiar WeChat + Copiar Correo Electrónico + Teléfono Móvil + Puntos + Cerrar sesión + Actualización de Dispositivo + Versión Actual + Última Versión + Estado del Dispositivo + Nombre del Dispositivo + Actualizar Ahora + Obtener Versión + Versión de la APP + Versión del Servidor + Versión del Dispositivo + Centro de Actualización + Actualización de firmware, aplicación, dispositivo, etc. + Registro de grabación + Registro de grabación de llaves + Recarga de puntos + Membresía + Registro de puntos + Tus puntos + Ingrese el importe de recarga + Seleccione método de pago + WeChat Pay + PayPal + Pagar + Detección Remota + Copia Remota + Copia HCS / Código Fijo + Haga clic en el botón Probar de abajo para detectar la frecuencia. Tras conectar el dispositivo, pulse un botón en la unidad secundaria dentro del rango de prueba y se mostrará la frecuencia. + Haga clic en el botón Probar de abajo para detectar la frecuencia. Tras conectar el dispositivo, pulse un botón en la unidad secundaria dentro del rango de prueba y se mostrará la frecuencia. + Modo: + + Detección + Omitir esta tecla + Volver a comprobar + Iniciar copia + Cerrar con llave + Desbloquear + Maletero + Buscar vehículo + + No se encontró ninguna marca coincidente + No se encontró ninguna marca coincidente + Sin datos de la llave disponibles + + Datos: + Tipo: + Tasa de bits: + Longitud en bits: + Iniciar Programación + Consejo + ¡Recomiéndalo a tus amigos! Escanea el código QR con WeChat, QQ, etc. para descargar e instalar. + Seguiremos apoyando a los cerrajeros como siempre. + Escanea para descargar la aplicación móvil + Obtén las últimas noticias y soporte + Ver más + Cargando... + Datos en la Nube + Actualizar Ahora + Actualizar Más Tarde + Nueva Versión Encontrada + Consejos de Actualización + Sincronización de datos en la nube + Durante la sincronización de datos en la nube, el dispositivo se reiniciará y la conexión se desconectará temporalmente. Esto es normal. Espere pacientemente a que el sistema reinicialice las funciones del dispositivo. + Para mejorar la estabilidad del dispositivo, ajuste la configuración del sistema: Panel de control -> Opciones de energía -> Cambiar la configuración del plan -> Cambiar la configuración avanzada de energía -> Configuración de USB -> Configuración de suspensión selectiva de USB -> [Deshabilitado] + Recordatorio: realice la operación mientras el dispositivo esté conectado + Abrir unidad USB (prueba) + Cerrar unidad USB (prueba) + 📁 Seleccionar archivo + 📂 Seleccionar carpeta + Mantener el dispositivo activo + Sincronizar datos en la nube + + + + + + diff --git a/AIK.Assets/Languages/Strings.fa.xaml b/AIK.Assets/Languages/Strings.fa.xaml new file mode 100644 index 0000000..6c1bfc9 --- /dev/null +++ b/AIK.Assets/Languages/Strings.fa.xaml @@ -0,0 +1,116 @@ + + AIK. ابزار کلید هوشمند + تنظیمات + زبان + تنظیمات برنامه + پیکربندی پارامترهای اصلی و ترجیحات برنامه + + تنظیمات رابط + تنظیمات کش + پاک کردن کش داده‌ها + زبان رابط: + لیست مدل‌های کلید + + ذخیره + لغو + تایید + + جستجوی نام برند، انگلیسی یا پینیین... + جستجوی نام کلید خودرو، انگلیسی یا پینیین... + + دستگاه فرعی: + فرکانس: + اطلاعات مرجع: + زمان: + شناسه: + + اطلاعات مرجع + + تنظیمات با موفقیت ذخیره شد + + تماس با ما از طریق ویچت + تماس با ما از طریق ایمیل + کپی ویچت + کپی ایمیل + تلفن همراه + امتیاز + خروج + + به‌روزرسانی دستگاه + نسخه فعلی + آخرین نسخه + وضعیت دستگاه + نام دستگاه + اکنون به‌روزرسانی کنید + دریافت نسخه + نسخه برنامه + نسخه سرور + نسخه دستگاه + مرکز به‌روزرسانی + به‌روزرسانی فیلم‌وار، برنامه، دستگاه و غیره + گزارش سوزنده + تاریخچه سوزنده کلید + شارژ امتیاز + عضویت + سابقه امتیاز + امتیازهای شما + مبلغ شارژ را وارد کنید + روش پرداخت را انتخاب کنید + پرداخت وی‌چت + PayPal + پرداخت + تشخیص از راه دور + کپی از راه دور + کپی HCS / کد ثابت + برای تشخیص فرکانس، روی دکمهٔ تست زیر کلیک کنید. پس از اتصال دستگاه، یکی از دکمه‌های واحد فرعی را در محدودهٔ قابل تست فشار دهید تا فرکانس نمایش داده شود. + برای تشخیص فرکانس، روی دکمهٔ تست زیر کلیک کنید. پس از اتصال دستگاه، یکی از دکمه‌های واحد فرعی را در محدودهٔ قابل تست فشار دهید تا فرکانس نمایش داده شود. + حالت: + + تشخیص + رد کردن این کلید + بررسی مجدد + شروع کپی + قفل کردن + باز کردن قفل + صندوق عقب + دکمه پانیک + + هیچ برند مطابقی یافت نشد + هیچ برند مطابقی یافت نشد + داده‌های کلید در دسترس نیست + + داده‌ها: + نوع: + نرخ بیت: + طول بیت: + شروع برنامه‌ریزی + نکته + به دوستان خود پیشنهاد دهید! با استفاده از ویچت، کیوکیو و غیره کد QR را اسکن کنید تا دانلود و نصب شود. + همچون گذشته، همچنان از قفل‌سازان عزیز حمایت خواهیم کرد. + برای دانلود برنامه موبایل اسکن کنید + جدیدترین اخبار و پشتیبانی را دریافت کنید + مشاهده بیشتر + در حال بارگذاری... + داده‌های ابری + اکنون به‌روزرسانی کنید + بعداً به‌روزرسانی کنید + نسخه جدید پیدا شد + نکات به‌روزرسانی + همگام‌سازی داده‌های ابری + در هنگام همگام‌سازی داده‌های ابری، دستگاه راه‌اندازی مجدد می‌شود و اتصال به‌طور موقت قطع خواهد شد. این طبیعی است. لطفاً با حوصله منتظر بمانید تا سیستم عملکردهای دستگاه را دوباره مقداردهی کند. + برای افزایش پایداری دستگاه، تنظیمات سیستم را تغییر دهید: کنترل پنل -> گزینه‌های برق -> تغییر تنظیمات طرح -> تغییر تنظیمات پیشرفته برق -> تنظیمات USB -> تنظیم تعلیق انتخابی USB -> [غیرفعال] + یادآوری: لطفاً عملیات را زمانی انجام دهید که دستگاه متصل است + باز کردن درایو USB (آزمایشی) + بستن درایو USB (آزمایشی) + 📁 انتخاب فایل + 📂 انتخاب پوشه + دستگاه را فعال نگه دارید + همگام‌سازی داده‌های ابری + + + + + + diff --git a/AIK.Assets/Languages/Strings.fr.xaml b/AIK.Assets/Languages/Strings.fr.xaml new file mode 100644 index 0000000..ae83d81 --- /dev/null +++ b/AIK.Assets/Languages/Strings.fr.xaml @@ -0,0 +1,116 @@ + + AIK. Outil de clés intelligent + Paramètres + Langue + Paramètres de l'application + Configurer les paramètres de base et les préférences de l'application + + Paramètres d'interface + Paramètres du cache + Effacer le cache de données + Langue de l'interface : + Liste des modèles de clés + + Enregistrer + Annuler + Confirmer + + Rechercher un nom de marque, en anglais ou en pinyin... + Rechercher un nom de clé de véhicule, en anglais ou en pinyin... + + Sous-unité : + Fréquence : + Informations de référence : + Heure : + ID : + + Informations de référence + + Paramètres enregistrés avec succès + + Contactez-nous sur WeChat + Contactez-nous par e-mail + Copier WeChat + Copier l'e-mail + Téléphone + Points + Déconnexion + + Mise à jour de l'appareil + Version actuelle + Dernière version + État de l'appareil + Nom de l'appareil + Mettre à jour maintenant + Obtenir la version + Version de l'application + Version du serveur + Version de l'appareil + Mises à jour de version + Mises à jour du firmware, de l'application, de l'appareil, etc. + Journal d'écriture + Historique d'écriture des clés + Recharger des points + Adhésion + Historique des points + Vos points + Saisir le montant de recharge + Sélectionnez le mode de paiement + WeChat Pay + PayPal + Payer + Détection à distance + Copie à distance + Copie HCS / Code fixe + Cliquez sur le bouton Tester ci-dessous pour détecter la fréquence. Après avoir connecté l’appareil, appuyez sur un bouton de l’unité secondaire à portée de test, et la fréquence s’affichera. + Cliquez sur le bouton Tester ci-dessous pour détecter la fréquence. Après avoir connecté l’appareil, appuyez sur un bouton de l’unité secondaire à portée de test, et la fréquence s’affichera. + Mode : + + Détection + Ignorer cette touche + Revérifier + Commencer la copie + Verrouiller + Déverrouiller + Coffre + Recherche de véhicule + + Aucune marque correspondante trouvée + Aucune marque correspondante trouvée + Aucune donnée de clé disponible + + Données : + Type : + Débit binaire : + Longueur en bits : + Démarrer la programmation + Astuce + Recommandez-le à vos amis ! Scannez le code QR avec WeChat, QQ, etc. pour télécharger et installer. + Nous continuerons à accompagner les serruriers comme toujours. + Scannez pour télécharger l’application mobile + Obtenez les dernières actualités et le support + Voir plus + Chargement... + Données Cloud + Mettre à Jour Maintenant + Mettre à Jour Plus Tard + Nouvelle Version Trouvée + Conseils de Mise à Jour + Synchronisation des données cloud + Pendant la synchronisation des données cloud, l'appareil redémarrera et la connexion sera temporairement interrompue. C'est normal. Veuillez patienter pendant que le système réinitialise les fonctions de l'appareil. + Pour améliorer la stabilité de l'appareil, ajustez les paramètres système : Panneau de configuration -> Options d’alimentation -> Modifier les paramètres du mode -> Modifier les paramètres d’alimentation avancés -> Paramètres USB -> Paramètre de suspension sélective USB -> [Désactivé] + Rappel : veuillez effectuer l’opération lorsque l'appareil est connecté + Ouvrir la clé USB (test) + Fermer la clé USB (test) + 📁 Sélectionner un fichier + 📂 Sélectionner un dossier + Maintenir l'appareil actif + Synchroniser les données cloud + + + + + + diff --git a/AIK.Assets/Languages/Strings.he.xaml b/AIK.Assets/Languages/Strings.he.xaml new file mode 100644 index 0000000..5147bc9 --- /dev/null +++ b/AIK.Assets/Languages/Strings.he.xaml @@ -0,0 +1,116 @@ + + AIK. כלי מפתח חכם + הגדרות + שפה + הגדרות אפליקציה + הגדרת פרמטרים ו העדפות בסיסיות לאפליקציה + + הגדרות ממשק + הגדרות מטמון + נקה מטמון נתונים + שפת ממשק: + רשימת דגמי מפתחות + + שמור + בטל + אישור + + חפש שם מותג, אנגלית או פיניין... + חפש שם מפתח רכב, אנגלית או פיניין... + + יחידת משנה: + תדר: + מידע רפרנטי: + זמן: + מזהה: + + מידע רפרנטי + + ההגדרות נשמרו בהצלחה + + צור איתנו קשר בויצ'אט + צור איתנו קשר באימייל + העתק ויצ'אט + העתק אימייל + טלפון + נקודות + התנתק + + עדכון התקן + גרסה נוכחית + גרסה חדשה + מצב התקן + שם התקן + עדכן כעת + קבל גרסה + גרסה של האפליקציה + גרסה של השרת + גרסה של ההתקן + מרכז עדכונים + עדכוני פירמה, אפליקציה, התקן וכו' + יומן תהליך + היסטוריה של תהליך מפתחות + טעינת נקודות + מנוי + רשומת נקודות + הנקודות שלך + הזן סכום טעינה + בחר אמצעי תשלום + WeChat Pay + PayPal + שלם + זיהוי מרחוק + העתקה מרחוק + העתקת HCS / קוד קבוע + לחצו על כפתור הבדיקה להלן כדי לזהות את התדר. לאחר חיבור המכשיר, לחצו על כפתור ביחידת המשנה בתוך טווח הבדיקה, והתדר יוצג. + לחצו על כפתור הבדיקה להלן כדי לזהות את התדר. לאחר חיבור המכשיר, לחצו על כפתור ביחידת המשנה בתוך טווח הבדיקה, והתדר יוצג. + מצב: + + זיהוי + דלג על מקש זה + בדוק שוב + התחל העתקה + לנעול + פתח נעילה + תא מטען + מצא רכב + + לא נמצאה מותג תואם + לא נמצאה מותג תואם + אין נתוני מפתח זמינים + + נתונים: + סוג: + קצב סיביות: + אורך הסיביות: + התחל תכנות + טיפ + המלץ לחבריך! סרוק את קוד ה-QR עם WeChat, QQ וכו' כדי להוריד ולהתקין. + נמשיך לתמוך בנגרי המפתחות כרגיל. + סרוק כדי להוריד את האפליקציה לנייד + קבל את העדכונים והתמיכה האחרונים + הצג עוד + טוען... + נתונים בענן + עדכן עכשיו + עדכן מאוחר יותר + נמצאה גרסה חדשה + טיפים לעדכון + סנכרון נתוני ענן + במהלך סנכרון נתוני הענן, המכשיר יופעל מחדש והחיבור יתנתק באופן זמני. זה תקין. אנא המתן בסבלנות בזמן שהמערכת מאתחלת מחדש את פונקציות המכשיר. + כדי לשפר את יציבות המכשיר, שנה את הגדרות המערכת: לוח הבקרה -> אפשרויות צריכת חשמל -> שנה הגדרות תוכנית -> שנה הגדרות צריכת חשמל מתקדמות -> הגדרות USB -> הגדרת השהיה סלקטיבית של USB -> [מושבת] + תזכורת: יש לבצע את הפעולה כשהמכשיר מחובר + פתח כונן USB (בדיקה) + סגור כונן USB (בדיקה) + 📁 בחר קובץ + 📂 בחר תיקייה + השאר את המכשיר פעיל + סנכרן נתוני ענן + + + + + + diff --git a/AIK.Assets/Languages/Strings.hi.xaml b/AIK.Assets/Languages/Strings.hi.xaml new file mode 100644 index 0000000..026f918 --- /dev/null +++ b/AIK.Assets/Languages/Strings.hi.xaml @@ -0,0 +1,116 @@ + + AIK. स्मार्ट की टूल + सेटिंग्स + भाषा + ऐप सेटिंग्स + ऐप के मूल पैरामीटर और पसंद कॉन्फ़िगर करें + + इंटरफ़ेस सेटिंग्स + कैश सेटिंग्स + डेटा कैश साफ़ करें + इंटरफ़ेस भाषा: + की मॉडल सूची + + सहेजें + रद्द करें + पुष्टि करें + + ब्रांड नाम, अंग्रेजी या पिनयिन में खोजें... + वाहन की नाम, अंग्रेजी या पिनयिन में खोजें... + + उपकरण: + आवृत्ति: + संदर्भ जानकारी: + समय: + ID: + + संदर्भ जानकारी + + सेटिंग्स सफलतापूर्वक सहेजी गई + + हमसे वीचैट पर संपर्क करें + हमसे ईमेल पर संपर्क करें + वीचैट कॉपी करें + ईमेल कॉपी करें + मोबाइल + अंक + लॉग आउट + + डिवाइस अपडेट + वर्तमान संस्करण + नवीनतम संस्करण + डिवाइस स्थिति + डिवाइस नाम + अभी अपडेट करें + संस्करण प्राप्त करें + ऐप संस्करण + सर्वर संस्करण + डिवाइस संस्करण + संस्करण अपडेट + फ़र्मवेयर, ऐप, डिवाइस आदि के अपडेट + बर्न लॉग + की बर्न लॉग + पॉइंट रिचार्ज + सदस्यता + पॉइंट रिकॉर्ड + आपके पॉइंट + रिचार्ज राशि दर्ज करें + भुगतान विधि चुनें + WeChat Pay + PayPal + भुगतान + रिमोट डिटेक्शन + रिमोट कॉपी + HCS / फिक्स्ड-कोड कॉपी + आवृत्ति का पता लगाने के लिए नीचे दिए गए टेस्ट बटन पर क्लिक करें। डिवाइस कनेक्ट करने के बाद, टेस्ट रेंज के भीतर सब-यूनिट का कोई बटन दबाएं, और आवृत्ति दिखाई देगी। + आवृत्ति का पता लगाने के लिए नीचे दिए गए टेस्ट बटन पर क्लिक करें। डिवाइस कनेक्ट करने के बाद, टेस्ट रेंज के भीतर सब-यूनिट का कोई बटन दबाएं, और आवृत्ति दिखाई देगी। + मोड: + + पता लगाना + इस कुंजी को छोड़ें + फिर से जांचें + कॉपी करना शुरू करें + ताला लगाएं + ताला खोलें + ट्रंक + आपातकालीन बजर + + कोई मेल खाने वाला ब्रांड नहीं मिला + कोई मेल खाने वाला ब्रांड नहीं मिला + कुंजी डेटा उपलब्ध नहीं है + + डेटा: + प्रकार: + बिटरेट: + बिट लंबाई: + प्रोग्रामिंग शुरू करें + सुझाव + इसे अपने दोस्तों को अवश्य बताएं! WeChat, QQ आदि से QR कोड स्कैन करके डाउनलोड और इंस्टॉल करें। + हम हमेशा की तरह लॉकस्मिथ्स का समर्थन करते रहेंगे। + मोबाइल ऐप डाउनलोड करने के लिए स्कैन करें + नवीनतम जानकारी और सहायता प्राप्त करें + अधिक देखें + लोड हो रहा है... + क्लाउड डेटा + अभी अपडेट करें + बाद में अपडेट करें + नew वर्जन पाया गया + अपडेट टिप्स + क्लाउड डेटा सिंक + क्लाउड डेटा सिंक करते समय, डिवाइस रीस्टार्ट होगा और कनेक्शन अस्थायी रूप से टूट जाएगा। यह सामान्य है। सिस्टम के डिवाइस फ़ंक्शन को दोबारा प्रारंभ करने तक कृपया धैर्य रखें। + डिवाइस की स्थिरता बढ़ाने के लिए सिस्टम सेटिंग समायोजित करें: कंट्रोल पैनल -> पावर ऑप्शंस -> प्लान सेटिंग बदलें -> उन्नत पावर सेटिंग बदलें -> USB सेटिंग्स -> USB सेलेक्टिव सस्पेंड सेटिंग -> [अक्षम] + याद दिलाना: कृपया डिवाइस कनेक्टेड होने पर ही ऑपरेशन करें + USB ड्राइव खोलें (परीक्षण) + USB ड्राइव बंद करें (परीक्षण) + 📁 फ़ाइल चुनें + 📂 फ़ोल्डर चुनें + डिवाइस को सक्रिय रखें + क्लाउड डेटा सिंक करें + + + + + + diff --git a/AIK.Assets/Languages/Strings.id.xaml b/AIK.Assets/Languages/Strings.id.xaml new file mode 100644 index 0000000..a7398bb --- /dev/null +++ b/AIK.Assets/Languages/Strings.id.xaml @@ -0,0 +1,116 @@ + + AIK. Alat Kunci Cerdas + Pengaturan + Bahasa + Pengaturan Aplikasi + Konfigurasi parameter dasar dan preferensi aplikasi + + Pengaturan Antarmuka + Pengaturan Cache + Hapus cache data + Bahasa antarmuka: + Daftar Model Kunci + + Simpan + Batal + Konfirmasi + + Cari nama merek, bahasa Inggris, atau pinyin... + Cari nama kunci kendaraan, bahasa Inggris, atau pinyin... + + Subunit: + Frekuensi: + Informasi Referensi: + Waktu: + ID: + + Informasi Referensi + + Pengaturan berhasil disimpan + + Hubungi kami via WeChat + Hubungi kami via Email + Salin WeChat + Salin Email + Telepon Seluler + Poin + Keluar + + Pembaruan Perangkat + Versi Saat Ini + Versi Terbaru + Status Perangkat + Nama Perangkat + Perbarui Sekarang + Dapatkan Versi + Versi Aplikasi + Versi Server + Versi Perangkat + Pembaruan Versi + Pembaruan firmware, aplikasi, perangkat, dll. + Log Pembakaran + Riwayat Pembakaran Kunci + Isi ulang poin + Keanggotaan + Riwayat poin + Poin Anda + Masukkan jumlah isi ulang + Pilih metode pembayaran + WeChat Pay + PayPal + Bayar + Deteksi Jarak Jauh + Salin Remote + Salin HCS / Kode Tetap + Klik tombol Uji di bawah untuk mendeteksi frekuensi. Setelah menghubungkan perangkat, tekan tombol pada unit sekunder dalam jangkauan pengujian, dan frekuensinya akan ditampilkan. + Klik tombol Uji di bawah untuk mendeteksi frekuensi. Setelah menghubungkan perangkat, tekan tombol pada unit sekunder dalam jangkauan pengujian, dan frekuensinya akan ditampilkan. + Mode: + + Deteksi + Lewati kunci ini + Periksa ulang + Mulai menyalin + Kunci + Buka kunci + Bagasi + Cari mobil + + Tidak ada merek yang cocok ditemukan + Tidak ada merek yang cocok ditemukan + Tidak ada data kunci tersedia + + Data: + Jenis: + Laju bit: + Panjang bit: + Mulai Pemrograman + Tips + Rekomendasikan ke teman Anda! Pindai kode QR dengan WeChat, QQ, dll. untuk mengunduh dan menginstal. + Kami akan terus mendukung para tukang kunci seperti biasa. + Pindai untuk mengunduh aplikasi seluler + Dapatkan berita terbaru dan dukungan + Lihat lainnya + Memuat... + Data Cloud + Perbarui Sekarang + Perbarui Nanti + Versi Baru Ditemukan + Tips Pembaruan + Sinkronisasi data cloud + Saat menyinkronkan data cloud, perangkat akan dimulai ulang dan koneksi akan terputus sementara. Ini normal. Harap tunggu dengan sabar hingga sistem menginisialisasi ulang fungsi perangkat. + Untuk meningkatkan stabilitas perangkat, sesuaikan pengaturan sistem: Control Panel -> Opsi Daya -> Ubah pengaturan paket -> Ubah pengaturan daya lanjutan -> Pengaturan USB -> Pengaturan penangguhan selektif USB -> [Dinonaktifkan] + Pengingat: Harap lakukan operasi saat perangkat terhubung + Buka drive USB (uji) + Tutup drive USB (uji) + 📁 Pilih file + 📂 Pilih folder + Jaga perangkat tetap aktif + Sinkronkan data cloud + + + + + + diff --git a/AIK.Assets/Languages/Strings.in.xaml b/AIK.Assets/Languages/Strings.in.xaml new file mode 100644 index 0000000..026f918 --- /dev/null +++ b/AIK.Assets/Languages/Strings.in.xaml @@ -0,0 +1,116 @@ + + AIK. स्मार्ट की टूल + सेटिंग्स + भाषा + ऐप सेटिंग्स + ऐप के मूल पैरामीटर और पसंद कॉन्फ़िगर करें + + इंटरफ़ेस सेटिंग्स + कैश सेटिंग्स + डेटा कैश साफ़ करें + इंटरफ़ेस भाषा: + की मॉडल सूची + + सहेजें + रद्द करें + पुष्टि करें + + ब्रांड नाम, अंग्रेजी या पिनयिन में खोजें... + वाहन की नाम, अंग्रेजी या पिनयिन में खोजें... + + उपकरण: + आवृत्ति: + संदर्भ जानकारी: + समय: + ID: + + संदर्भ जानकारी + + सेटिंग्स सफलतापूर्वक सहेजी गई + + हमसे वीचैट पर संपर्क करें + हमसे ईमेल पर संपर्क करें + वीचैट कॉपी करें + ईमेल कॉपी करें + मोबाइल + अंक + लॉग आउट + + डिवाइस अपडेट + वर्तमान संस्करण + नवीनतम संस्करण + डिवाइस स्थिति + डिवाइस नाम + अभी अपडेट करें + संस्करण प्राप्त करें + ऐप संस्करण + सर्वर संस्करण + डिवाइस संस्करण + संस्करण अपडेट + फ़र्मवेयर, ऐप, डिवाइस आदि के अपडेट + बर्न लॉग + की बर्न लॉग + पॉइंट रिचार्ज + सदस्यता + पॉइंट रिकॉर्ड + आपके पॉइंट + रिचार्ज राशि दर्ज करें + भुगतान विधि चुनें + WeChat Pay + PayPal + भुगतान + रिमोट डिटेक्शन + रिमोट कॉपी + HCS / फिक्स्ड-कोड कॉपी + आवृत्ति का पता लगाने के लिए नीचे दिए गए टेस्ट बटन पर क्लिक करें। डिवाइस कनेक्ट करने के बाद, टेस्ट रेंज के भीतर सब-यूनिट का कोई बटन दबाएं, और आवृत्ति दिखाई देगी। + आवृत्ति का पता लगाने के लिए नीचे दिए गए टेस्ट बटन पर क्लिक करें। डिवाइस कनेक्ट करने के बाद, टेस्ट रेंज के भीतर सब-यूनिट का कोई बटन दबाएं, और आवृत्ति दिखाई देगी। + मोड: + + पता लगाना + इस कुंजी को छोड़ें + फिर से जांचें + कॉपी करना शुरू करें + ताला लगाएं + ताला खोलें + ट्रंक + आपातकालीन बजर + + कोई मेल खाने वाला ब्रांड नहीं मिला + कोई मेल खाने वाला ब्रांड नहीं मिला + कुंजी डेटा उपलब्ध नहीं है + + डेटा: + प्रकार: + बिटरेट: + बिट लंबाई: + प्रोग्रामिंग शुरू करें + सुझाव + इसे अपने दोस्तों को अवश्य बताएं! WeChat, QQ आदि से QR कोड स्कैन करके डाउनलोड और इंस्टॉल करें। + हम हमेशा की तरह लॉकस्मिथ्स का समर्थन करते रहेंगे। + मोबाइल ऐप डाउनलोड करने के लिए स्कैन करें + नवीनतम जानकारी और सहायता प्राप्त करें + अधिक देखें + लोड हो रहा है... + क्लाउड डेटा + अभी अपडेट करें + बाद में अपडेट करें + नew वर्जन पाया गया + अपडेट टिप्स + क्लाउड डेटा सिंक + क्लाउड डेटा सिंक करते समय, डिवाइस रीस्टार्ट होगा और कनेक्शन अस्थायी रूप से टूट जाएगा। यह सामान्य है। सिस्टम के डिवाइस फ़ंक्शन को दोबारा प्रारंभ करने तक कृपया धैर्य रखें। + डिवाइस की स्थिरता बढ़ाने के लिए सिस्टम सेटिंग समायोजित करें: कंट्रोल पैनल -> पावर ऑप्शंस -> प्लान सेटिंग बदलें -> उन्नत पावर सेटिंग बदलें -> USB सेटिंग्स -> USB सेलेक्टिव सस्पेंड सेटिंग -> [अक्षम] + याद दिलाना: कृपया डिवाइस कनेक्टेड होने पर ही ऑपरेशन करें + USB ड्राइव खोलें (परीक्षण) + USB ड्राइव बंद करें (परीक्षण) + 📁 फ़ाइल चुनें + 📂 फ़ोल्डर चुनें + डिवाइस को सक्रिय रखें + क्लाउड डेटा सिंक करें + + + + + + diff --git a/AIK.Assets/Languages/Strings.it.xaml b/AIK.Assets/Languages/Strings.it.xaml new file mode 100644 index 0000000..c438351 --- /dev/null +++ b/AIK.Assets/Languages/Strings.it.xaml @@ -0,0 +1,116 @@ + + AIK. Strumente Chiave Intelligente + Impostazioni + Lingua + Impostazioni App + Configura parametri di base e preferenze dell'app + + Impostazioni Interfaccia + Impostazioni Cache + Cancella cache dati + Lingua interfaccia: + Elenco Modelli Chiave + + Salva + Annulla + Conferma + + Cerca nome marca, inglese o pinyin... + Cerca nome chiave veicolo, inglese o pinyin... + + Sottounità: + Frequenza: + Informazioni Riferimento: + Ora: + ID: + + Informazioni Riferimento + + Impostazioni salvate correttamente + + Contattaci via WeChat + Contattaci via Email + Copia WeChat + Copia Email + Telefono + Punti + Esci + + Aggiornamento Dispositivo + Versione Corrente + Ultima Versione + Stato Dispositivo + Nome Dispositivo + Aggiorna Ora + Ottieni Versione + Versione App + Versione Server + Versione Dispositivo + Aggiornamenti Versione + Aggiornamenti firmware, app, dispositivo ecc. + Log Scrittura + Cronologia Scrittura Chiave + Ricarica punti + Abbonamento + Storico punti + I tuoi punti + Inserisci importo ricarica + Seleziona metodo di pagamento + WeChat Pay + PayPal + Paga + Rilevamento Remoto + Copia Telecomando + Copia HCS / Codice Fisso + Fai clic sul pulsante Test qui sotto per rilevare la frequenza. Dopo aver collegato il dispositivo, premi un tasto sull'unità secondaria entro la distanza di test e la frequenza verrà visualizzata. + Fai clic sul pulsante Test qui sotto per rilevare la frequenza. Dopo aver collegato il dispositivo, premi un tasto sull'unità secondaria entro la distanza di test e la frequenza verrà visualizzata. + Modalità: + + Rilevamento + Salta questo tasto + Ricontrolla + Avvia copia + Blocca + Sblocca + Baule + Cerca veicolo + + Nessun marchio corrispondente trovato + Nessun marchio corrispondente trovato + Nessun dato chiave disponibile + + Dati: + Tipo: + Bitrate: + Lunghezza in bit: + Avvia Programmazione + Suggerimento + Consiglalo ai tuoi amici! Scansiona il codice QR con WeChat, QQ, ecc. per scaricare e installare. + Continueremo a supportare i fabbri come sempre. + Scansiona per scaricare l’app mobile + Ottieni le ultime novità e il supporto + Visualizza altro + Caricamento... + Dati Cloud + Aggiorna Ora + Aggiorna Più Tardi + Nuova Versione Trovata + Suggerimenti per l'Aggiornamento + Sincronizzazione dati cloud + Durante la sincronizzazione dei dati cloud, il dispositivo si riavvierà e la connessione verrà temporaneamente interrotta. È normale. Attendere pazientemente che il sistema reinizializzi le funzioni del dispositivo. + Per migliorare la stabilità del dispositivo, regolare le impostazioni di sistema: Pannello di controllo -> Opzioni risparmio energia -> Modifica impostazioni combinazione -> Cambia impostazioni avanzate risparmio energia -> Impostazioni USB -> Impostazione sospensione selettiva USB -> [Disattivato] + Promemoria: eseguire l’operazione mentre il dispositivo è connesso + Apri unità USB (test) + Chiudi unità USB (test) + 📁 Seleziona file + 📂 Seleziona cartella + Mantieni attivo il dispositivo + Sincronizza dati cloud + + + + + + diff --git a/AIK.Assets/Languages/Strings.ko.xaml b/AIK.Assets/Languages/Strings.ko.xaml new file mode 100644 index 0000000..86c7f6b --- /dev/null +++ b/AIK.Assets/Languages/Strings.ko.xaml @@ -0,0 +1,116 @@ + + AIK. 스마트 키 도구 + 설정 + 언어 + 앱 설정 + 앱의 기본 매개변수 및 환경설정 구성 + + 인터페이스 설정 + 캐시 설정 + 데이터 캐시 삭제 + 인터페이스 언어: + 키 모델 목록 + + 저장 + 취소 + 확인 + + 브랜드 이름, 영어 또는 병음 검색... + 차량 키 이름, 영어 또는 병음 검색... + + 서브 유닛: + 주파수: + 참조 정보: + 시간: + ID: + + 참조 정보 + + 설정이 성공적으로 저장되었습니다 + + 위챗으로 문의하기 + 이메일로 문의하기 + 위챗 복사 + 이메일 복사 + 휴대폰 + 포인트 + 로그아웃 + + 장치 업데이트 + 현재 버전 + 최신 버전 + 장치 상태 + 장치 이름 + 지금 업그레이드 + 버전 확인 + 앱 버전 + 서버 버전 + 장치 버전 + 버전 업데이트 + 펌웨어, 앱, 장치 등 업데이트 + 소각 로그 + 키 소각 기록 + 포인트 충전 + 멤버십 + 포인트 기록 + 내 포인트 + 충전 금액 입력 + 결제 방법 선택 + WeChat Pay + PayPal + 결제 + 원격 감지 + 리모컨 복사 + HCS / 고정 코드 복사 + 아래의 테스트 버튼을 클릭하여 주파수를 감지하세요. 장치를 연결한 후, 테스트 가능 거리 내에서 서브 유닛의 버튼을 누르면 주파수가 표시됩니다. + 아래의 테스트 버튼을 클릭하여 주파수를 감지하세요. 장치를 연결한 후, 테스트 가능 거리 내에서 서브 유닛의 버튼을 누르면 주파수가 표시됩니다. + 모드: + + 감지 + 이 키 건너뛰기 + 재확인 + 복사 시작 + 잠금 + 잠금 해제 + 트렁크 + 찾기 + + 일치하는 브랜드 없음 + 일치하는 브랜드 없음 + 키 데이터 없음 + + 데이터: + 유형: + 비트레이트: + 비트 길이: + 프로그래밍 시작 + + 친구들에게 추천하세요! 위챗, QQ 등으로 QR 코드를 스캔하여 다운로드 및 설치하세요. + 앞으로도 자물쇠 기술자 여러분을 항상 지원하겠습니다. + 모바일 앱 다운로드를 위해 스캔하세요 + 최신 소식과 지원 정보를 확인하세요 + 더 보기 + 로딩 중... + 클라우드 데이터 + 지금 업데이트 + 나중에 업데이트 + 새 버전 발견 + 업데이트 팁 + 클라우드 데이터 동기화 + 클라우드 데이터를 동기화하는 동안 장치가 재시작되고 연결이 일시적으로 끊어집니다. 이는 정상적인 현상입니다. 시스템이 장치 기능을 다시 초기화할 때까지 잠시 기다려 주세요. + 장치 안정성을 높이려면 시스템 설정을 조정하세요: 제어판 -> 전원 옵션 -> 절전 계획 설정 변경 -> 고급 전원 관리 옵션 설정 변경 -> USB 설정 -> USB 선택적 절전 모드 설정 -> [사용 안 함] + 알림: 장치가 연결된 상태에서 작업해 주세요 + USB 드라이브 열기(테스트) + USB 드라이브 닫기(테스트) + 📁 파일 선택 + 📂 폴더 선택 + 장치 활성 상태 유지 + 클라우드 데이터 동기화 + + + + + + diff --git a/AIK.Assets/Languages/Strings.ms.xaml b/AIK.Assets/Languages/Strings.ms.xaml new file mode 100644 index 0000000..1bc6f1b --- /dev/null +++ b/AIK.Assets/Languages/Strings.ms.xaml @@ -0,0 +1,116 @@ + + AIK. Alat Kunci Pintar + Tetapan + Bahasa + Tetapan Aplikasi + Konfigurasi parameter asas dan keutamaan aplikasi + + Tetapan Antara Muka + Tetapan Cache + Padam cache data + Bahasa antara muka: + Senarai Model Kunci + + Simpan + Batal + Sahkan + + Cari nama jenama, Bahasa Inggeris atau pinyin... + Cari nama kunci kenderaan, Bahasa Inggeris atau pinyin... + + Subunit: + Frekuensi: + Maklumat Rujukan: + Masa: + ID: + + Maklumat Rujukan + + Tetapan berjaya disimpan + + Hubungi kami melalui WeChat + Hubungi kami melalui Emel + Salin WeChat + Salin Emel + Telefon Bimbit + Poin + Log Keluar + + Kemas Kini Peranti + Versi Semasa + Versi Terkini + Status Peranti + Nama Peranti + Kemas Kini Sekarang + Dapatkan Versi + Versi Aplikasi + Versi Pelayan + Versi Peranti + Kemas Kini Versi + Kemas kini firmware, aplikasi, peranti dan lain-lain + Log Penulisan + Sejarah Penulisan Kunci + Tambah nilai mata + Keahlian + Rekod mata + Mata anda + Masukkan jumlah tambah nilai + Pilih kaedah pembayaran + WeChat Pay + PayPal + Bayar + Pengesanan Jauh + Salinan Remote + Salinan HCS / Kod Tetap + Klik butang Uji di bawah untuk mengesan frekuensi. Selepas menyambungkan peranti, tekan but pada unit sekunder dalam julat ujian, dan frekuensi akan dipaparkan. + Klik butang Uji di bawah untuk mengesan frekuensi. Selepas menyambungkan peranti, tekan but pada unit sekunder dalam julat ujian, dan frekuensi akan dipaparkan. + Mod: + + Pengesanan + Langkau kekunci ini + Semak semula + Mula menyalin + Kunci + Buka kunci + But + Cari kereta + + Tiada jenama sepadan dijumpai + Tiada jenama sepadan dijumpai + Tiada data kunci tersedia + + Data: + Jenis: + Kadar bit: + Panjang bit: + Mulakan Pengaturcaraan + Petua + Cadangkan kepada rakan anda! Imbas kod QR dengan WeChat, QQ, dsb. untuk muat turun dan pasang. + Kami akan terus menyokong tukang kunci seperti biasa. + Imbas untuk memuat turun aplikasi mudah alih + Dapatkan maklumat terkini dan sokongan + Lihat Lebih Banyak + Memuat... + Data Awan + Kemas Kini Sekarang + Kemas Kini Kemudian + Versi Baru Ditemui + Tips Kemas Kini + Penyegerakan data awan + Semasa menyegerakkan data awan, peranti akan dimulakan semula dan sambungan akan terputus buat sementara waktu. Ini adalah perkara biasa. Sila tunggu dengan sabar sehingga sistem memulakan semula fungsi peranti. + Untuk meningkatkan kestabilan peranti, laraskan tetapan sistem: Panel Kawalan -> Pilihan Kuasa -> Tukar tetapan pelan -> Tukar tetapan kuasa lanjutan -> Tetapan USB -> Tetapan penggantungan terpilih USB -> [Dilumpuhkan] + Peringatan: Sila lakukan operasi semasa peranti disambungkan + Buka pemacu USB (ujian) + Tutup pemacu USB (ujian) + 📁 Pilih fail + 📂 Pilih folder + Pastikan peranti kekal aktif + Segerakkan data awan + + + + + + diff --git a/AIK.Assets/Languages/Strings.pl.xaml b/AIK.Assets/Languages/Strings.pl.xaml new file mode 100644 index 0000000..b02679e --- /dev/null +++ b/AIK.Assets/Languages/Strings.pl.xaml @@ -0,0 +1,116 @@ + + AIK. Inteligentne Narzędzie do Kluczy + Ustawienia + Język + Ustawienia aplikacji + Skonfiguruj podstawowe parametry i preferencje aplikacji + + Ustawienia interfejsu + Ustawienia pamięci podręcznej + Wyczyść dane w pamięci podręcznej + Język interfejsu: + Lista modeli kluczy + + Zapisz + Anuluj + Potwierdź + + Szukaj marki, nazwy angielskiej lub pinyin... + Szukaj nazwy klucza pojazdu, nazwy angielskiej lub pinyin... + + Podrzędny: + Częstotliwość: + Informacje referencyjne: + Czas: + ID: + + Informacje referencyjne + + Ustawienia zapisane pomyślnie + + Skontaktuj się z nami przez WeChat + Skontaktuj się z nami przez e-mail + Kopiuj WeChat + Kopiuj e-mail + Telefon + Punkty + Wyloguj + + Aktualizacja urządzenia + Bieżąca wersja + Najnowsza wersja + Stan urządzenia + Nazwa urządzenia + Aktualizuj teraz + Pobierz wersję + Wersja aplikacji + Wersja serwera + Wersja urządzenia + Aktualizacje wersji + Aktualizacje oprogramowania sprzętowego (firmware), aplikacji oraz urządzeń + Dziennik programowania + Dziennik programowania kluczy + Doładuj punkty + Członkostwo + Historia punktów + Twoje punkty + Wprowadź kwotę doładowania + Wybierz metodę płatności + WeChat Pay + PayPal + Zapłać + Wykrywanie zdalne + Kopiowanie pilota + Kopiowanie HCS / kodu stałego + Kliknij poniższy przycisk Test, aby wykryć częstotliwość. Po podłączeniu urządzenia naciśnij przycisk na jednostce podrzędnej w zasięgu testowym, a częstotliwość zostanie wyświetlona. + Kliknij poniższy przycisk Test, aby wykryć częstotliwość. Po podłączeniu urządzenia naciśnij przycisk na jednostce podrzędnej w zasięgu testowym, a częstotliwość zostanie wyświetlona. + Tryb: + + Wykrywanie + Pomiń ten klucz + Sprawdź ponownie + Rozpocznij kopiowanie + Zablokuj + Odblokuj + Bagażnik + Szukaj samochodu + + Nie znaleziono pasującej marki + Nie znaleziono pasującej marki + Brak danych klucza + + Dane: + Typ: + Szybkość transmisji: + Długość bitowa: + Rozpocznij programowanie + Wskazówka + Poleć znajomym! Zeskanuj kod QR za pomocą WeChat, QQ itp., aby pobrać i zainstalować. + Będziemy nadal wspierać ślusarzy tak jak dotychczas. + Zeskanuj, aby pobrać aplikację mobilną + Uzyskaj najnowsze informacje i wsparcie + Pokaż więcej + Ładowanie... + Dane Chmurowe + Aktualizuj Teraz + Aktualizuj Później + Znaleziono Nową Wersję + Wskazówki Dotyczące Aktualizacji + Synchronizacja danych w chmurze + Podczas synchronizacji danych w chmurze urządzenie uruchomi się ponownie, a połączenie zostanie tymczasowo przerwane. To normalne. Proszę cierpliwie poczekać, aż system ponownie zainicjalizuje funkcje urządzenia. + Aby zwiększyć stabilność urządzenia, dostosuj ustawienia systemowe: Panel sterowania -> Opcje zasilania -> Zmień ustawienia planu -> Zmień zaawansowane ustawienia zasilania -> Ustawienia USB -> Ustawienie wstrzymania selektywnego USB -> [Wyłączone] + Przypomnienie: wykonuj operację, gdy urządzenie jest podłączone + Otwórz dysk USB (test) + Zamknij dysk USB (test) + 📁 Wybierz plik + 📂 Wybierz folder + Utrzymuj urządzenie aktywne + Synchronizuj dane w chmurze + + + + + + diff --git a/AIK.Assets/Languages/Strings.pt-BR.xaml b/AIK.Assets/Languages/Strings.pt-BR.xaml new file mode 100644 index 0000000..2e49152 --- /dev/null +++ b/AIK.Assets/Languages/Strings.pt-BR.xaml @@ -0,0 +1,116 @@ + + AIK. Ferramenta Inteligente de Chaves + Configurações + Idioma + Configurações do Aplicativo + Configure os parâmetros básicos e preferências do aplicativo + + Configurações da Interface + Configurações de Cache + Limpar cache de dados + Idioma da interface: + Lista de Modelos de Chaves + + Salvar + Cancelar + Confirmar + + Buscar marca, nome em inglês ou pinyin... + Buscar nome da chave, termo em inglês ou pinyin... + + Subunidade: + Frequência: + Informações de referência: + Hora: + ID: + + Informações de Referência + + Configurações salvas com sucesso + + Contate-nos pelo WeChat + Contate-nos por e-mail + Copiar WeChat + Copiar e-mail + Celular + Pontos + Sair + + Atualização do Dispositivo + Versão Atual + Nova Versão + Status do Dispositivo + Nome do Dispositivo + Atualizar Agora + Obter Versão + Versão do Aplicativo + Versão do Servidor + Versão do Dispositivo + Atualizações de Versão + Atualizações de firmware, aplicativo, dispositivo, etc. + Registro de Gravação + Histórico de Gravação de Chaves + Recarga de pontos + Assinatura + Registro de pontos + Seus pontos + Insira o valor da recarga + Selecione a forma de pagamento + WeChat Pay + PayPal + Pagar + Detecção Remota + Cópia Remota + Cópia HCS / Código Fixo + Clique no botão Testar abaixo para detectar a frequência. Após conectar o dispositivo, pressione um botão na unidade secundária dentro do alcance testável e a frequência será exibida. + Clique no botão Testar abaixo para detectar a frequência. Após conectar o dispositivo, pressione um botão na unidade secundária dentro do alcance testável e a frequência será exibida. + Modo: + + Detecção + Ignorar esta tecla + Reverificar + Iniciar cópia + Travar + Destravar + Porta-malas + Busca-carro + + Nenhuma marca correspondente encontrada + Nenhuma marca correspondente encontrada + Sem dados da chave disponíveis + + Dados: + Tipo: + Taxa de bits: + Comprimento em bits: + Iniciar Gravação + Dica + Indique aos seus amigos! Escaneie o QR Code com WeChat, QQ, etc. para baixar e instalar. + Continuaremos apoiando os chaveiros como sempre. + Escaneie para baixar o app móvel + Receba as últimas novidades e suporte + Ver mais + Carregando... + Dados na Nuvem + Atualizar Agora + Atualizar Mais Tarde + Nova Versão Encontrada + Dicas de Atualização + Sincronização de dados na nuvem + Durante a sincronização de dados na nuvem, o dispositivo será reiniciado e a conexão será temporariamente interrompida. Isso é normal. Aguarde pacientemente enquanto o sistema reinicializa as funções do dispositivo. + Para melhorar a estabilidade do dispositivo, ajuste as configurações do sistema: Painel de Controle -> Opções de Energia -> Alterar configurações do plano -> Alterar configurações avançadas de energia -> Configurações USB -> Configuração de suspensão seletiva de USB -> [Desabilitado] + Lembrete: execute a operação com o dispositivo conectado + Abrir unidade USB (teste) + Fechar unidade USB (teste) + 📁 Selecionar arquivo + 📂 Selecionar pasta + Manter dispositivo ativo + Sincronizar dados na nuvem + + + + + + diff --git a/AIK.Assets/Languages/Strings.pt.xaml b/AIK.Assets/Languages/Strings.pt.xaml new file mode 100644 index 0000000..808cbdf --- /dev/null +++ b/AIK.Assets/Languages/Strings.pt.xaml @@ -0,0 +1,116 @@ + + + AIK.Ferramenta de Chave Inteligente + Configurações + Idioma + Configurações do Aplicativo + Configurar parâmetros básicos e preferências do aplicativo + + Configurações de Interface + Configurações de Cache + Limpar Cache de Dados + Idioma da Interface: + Lista de Modelos de Chave + + Salvar + Cancelar + Confirmar + + Pesquisar nome da marca, inglês ou pinyin... + Pesquisar nome da chave do veículo, inglês ou pinyin... + + Chave Remota: + Frequência: + Informações de Referência: + Hora: + ID: + + Informações de Referência + + Configurações salvas com sucesso + + Contate-nos pelo WeChat + Contate-nos pelo E-mail + Copiar WeChat + Copiar E-mail + Telefone Celular + Pontos + Sair + Atualização do Dispositivo + Versão Atual + Última Versão + Status do Dispositivo + Nome do Dispositivo + Atualizar Agora + Obter Versão + Versão do APP + Versão do Servidor + Versão do Dispositivo + Centro de Atualização + Atualização de firmware, aplicativo, dispositivo, etc. + Registro de gravação + Registro de gravação de chaves + Recarga de pontos + Assinatura + Registro de pontos + Os seus pontos + Introduza o valor da recarga + Selecione o método de pagamento + WeChat Pay + PayPal + Pagar + Detecção Remota + Cópia Remota + Cópia HCS / Código Fixo + Clique no botão Testar abaixo para detetar a frequência. Após ligar o dispositivo, prima um botão na unidade secundária dentro do alcance testável e a frequência será exibida. + Clique no botão Testar abaixo para detetar a frequência. Após ligar o dispositivo, prima um botão na unidade secundária dentro do alcance testável e a frequência será exibida. + Modo: + + Detecção + Ignorar esta tecla + Reverificar + Iniciar cópia + Trancar + Destrancar + Porta-malas + Busca-carro + + Nenhuma marca correspondente encontrada + Nenhuma marca correspondente encontrada + Sem dados da chave disponíveis + + Dados: + Tipo: + Taxa de bits: + Comprimento em bits: + Iniciar Programação + Dica + Recomende aos seus amigos! Digitalize o código QR com WeChat, QQ, etc. para descarregar e instalar. + Continuaremos a apoiar os serralheiros como sempre. + Digitalize para descarregar a aplicação móvel + Obtenha as últimas notícias e suporte + Ver mais + Carregando... + Dados na Nuvem + Atualizar Agora + Atualizar Mais Tarde + Nova Versão Encontrada + Dicas de Atualização + Sincronização de dados na nuvem + Durante a sincronização de dados na nuvem, o dispositivo irá reiniciar e a ligação será temporariamente interrompida. Isto é normal. Aguarde pacientemente enquanto o sistema reinicializa as funções do dispositivo. + Para melhorar a estabilidade do dispositivo, ajuste as definições do sistema: Painel de Controlo -> Opções de Energia -> Alterar definições do plano -> Alterar definições avançadas de energia -> Definições USB -> Definição de suspensão seletiva de USB -> [Desativado] + Lembrete: execute a operação com o dispositivo ligado + Abrir unidade USB (teste) + Fechar unidade USB (teste) + 📁 Selecionar ficheiro + 📂 Selecionar pasta + Manter dispositivo ativo + Sincronizar dados na nuvem + + + + + + diff --git a/AIK.Assets/Languages/Strings.ru.xaml b/AIK.Assets/Languages/Strings.ru.xaml new file mode 100644 index 0000000..24e2e56 --- /dev/null +++ b/AIK.Assets/Languages/Strings.ru.xaml @@ -0,0 +1,115 @@ + + AIK. Интеллектуальный инструмент для ключей + Настройки + Язык + Настройки приложения + Настройка основных параметров и предпочтений приложения + + Настройки интерфейса + Настройки кэша + Очистить кэш данных + Язык интерфейса: + Список моделей ключей + + Сохранить + Отмена + Подтвердить + + Поиск по названию бренда, английскому или пиньиню... + Поиск по названию автомобильного ключа, английскому или пиньиню... + + Подчинённое устройство: + Частота: + Справочная информация: + Время: + ID: + + Справочная информация + + Настройки успешно сохранены + + Связаться через WeChat + Связаться по электронной почте + Копировать WeChat + Копировать email + Телефон + Баллы + Выйти + Обновление устройства + Текущая версия + Последняя версия + Состояние устройства + Название устройства + Обновить сейчас + Получить версию + Версия приложения + Версия сервера + Версия устройства + Центр обновлений + Обновление прошивки, приложения, устройства и др. + Журнал записи + Журнал записи ключей + Пополнение баллов + Подписка + История баллов + Ваши баллы + Введите сумму пополнения + Выберите способ оплаты + WeChat Pay + PayPal + Оплатить + Удаленное обнаружение + Копирование пульта + Копирование HCS / фиксированного кода + Нажмите кнопку «Тест» ниже, чтобы определить частоту. После подключения устройства нажмите кнопку на дочернем блоке в пределах тестового диапазона, и частота отобразится. + Нажмите кнопку «Тест» ниже, чтобы определить частоту. После подключения устройства нажмите кнопку на дочернем блоке в пределах тестового диапазона, и частота отобразится. + Режим: + + Обнаружение + Пропустить эту кнопку + Повторная проверка + Начать копирование + Заблокировать + Разблокировать + Багажник + Поиск автомобиля + + Соответствующий бренд не найден + Соответствующий бренд не найден + Данные ключа недоступны + + Данные: + Тип: + Битрейт: + Длина в битах: + Начать программирование + Совет + Порекомендуйте друзьям! Отсканируйте QR-код с помощью WeChat, QQ и т.д., чтобы скачать и установить. + Мы и дальше будем надежно поддерживать мастеров-ключников, как всегда. + Отсканируйте, чтобы скачать мобильное приложение + Получайте последние новости и поддержку + Показать ещё + Загрузка... + Облачные Данные + Обновить Сейчас + Обновить Позже + Найдена Новая Версия + Советы по Обновлению + Синхронизация облачных данных + Во время синхронизации облачных данных устройство перезагрузится, а соединение будет временно прервано. Это нормально. Пожалуйста, подождите, пока система повторно инициализирует функции устройства. + Чтобы повысить стабильность устройства, измените параметры системы: Панель управления -> Электропитание -> Настройка схемы электропитания -> Изменить дополнительные параметры питания -> Параметры USB -> Параметр временного отключения USB-порта -> [Отключено] + Напоминание: выполняйте операцию, когда устройство подключено + Открыть USB-накопитель (тест) + Закрыть USB-накопитель (тест) + 📁 Выбрать файл + 📂 Выбрать папку + Поддерживать устройство активным + Синхронизировать облачные данные + + + + + + diff --git a/AIK.Assets/Languages/Strings.th.xaml b/AIK.Assets/Languages/Strings.th.xaml new file mode 100644 index 0000000..216bc64 --- /dev/null +++ b/AIK.Assets/Languages/Strings.th.xaml @@ -0,0 +1,116 @@ + + AIK. เครื่องมือกุญแจอัจฉริยะ + การตั้งค่า + ภาษา + การตั้งค่าแอป + กำหนดค่าพารามิเตอร์และตัวเลือกพื้นฐานของแอป + + การตั้งค่าอินเตอร์เฟซ + การตั้งค่าแคช + ล้างแคชข้อมูล + ภาษาอินเตอร์เฟซ: + รายการรุ่นกุญแจ + + บันทึก + ยกเลิก + ยืนยัน + + ค้นหาชื่อแบรนด์ ภาษาอังกฤษ หรือ พินอิน... + ค้นหาชื่อกุญแจรถ ภาษาอังกฤษ หรือ พินอิน... + + หน่วยย่อย: + ความถี่: + ข้อมูลอ้างอิง: + เวลา: + ID: + + ข้อมูลอ้างอิง + + บันทึกการตั้งค่าเรียบร้อยแล้ว + + ติดต่อเราผ่าน WeChat + ติดต่อเราผ่านอีเมล + คัดลอก WeChat + คัดลอกอีเมล + โทรศัพท์มือถือ + คะแนน + ออกจากระบบ + + อัปเดตอุปกรณ์ + เวอร์ชันปัจจุบัน + เวอร์ชันล่าสุด + สถานะอุปกรณ์ + ชื่ออุปกรณ์ + อัปเดตเดี๋ยวนี้ + ดูเวอร์ชัน + เวอร์ชันแอป + เวอร์ชันเซิร์ฟเวอร์ + เวอร์ชันอุปกรณ์ + ศูนย์อัปเดตเวอร์ชัน + อัปเดตเฟิร์มแวร์ แอป อุปกรณ์ เป็นต้น + บันทึกการเขียนข้อมูล + ประวัติการเขียนข้อมูลกุญแจ + เติมคะแนน + สมาชิก + ประวัติคะแนน + คะแนนของคุณ + ป้อนจำนวนเงินที่ต้องการเติม + เลือกวิธีการชำระเงิน + WeChat Pay + PayPal + ชำระเงิน + ตรวจจับระยะไกล + คัดลอกสัญญาณรีโมท + คัดลอก HCS / รหัสคงที่ + คลิกปุ่มทดสอบด้านล่างเพื่อตรวจจับความถี่ เมื่อเชื่อมต่ออุปกรณ์แล้ว กดปุ่มบนยูนิตย่อยภายในระยะที่สามารถทดสอบได้ ความถี่จะแสดงขึ้นทันที + คลิกปุ่มทดสอบด้านล่างเพื่อตรวจจับความถี่ เมื่อเชื่อมต่ออุปกรณ์แล้ว กดปุ่มบนยูนิตย่อยภายในระยะที่สามารถทดสอบได้ ความถี่จะแสดงขึ้นทันที + โหมด: + + การตรวจจับ + ข้ามปุ่มนี้ + ตรวจสอบอีกครั้ง + เริ่มคัดลอก + ล็อก + เปิดล็อก + ท้ายรถ + ค้นหารถ + + ไม่พบยี่ห้อที่ตรงกัน + ไม่พบยี่ห้อที่ตรงกัน + ไม่มีข้อมูลกุญแจ + + ข้อมูล: + ประเภท: + อัตราบิต: + ความยาวบิต: + เริ่มการโปรแกรม + เคล็ดลับ + แนะนำให้เพื่อนของคุณ! สแกนรหัส QR ด้วย WeChat, QQ ฯลฯ เพื่อดาวน์โหลดและติดตั้ง + เราจะยังคงสนับสนุนช่างกุญแจทุกท่านอย่างเต็มที่เหมือนเช่นเคย + สแกนเพื่อดาวน์โหลดแอปมือถือ + รับข่าวสารและบริการสนับสนุนล่าสุด + ดูเพิ่มเติม + กำลังโหลด... + ข้อมูลในคลาวด์ + อัปเดตตอนนี้ + อัปเดตภายหลัง + พบเวอร์ชั่นใหม่ + คำแนะนำในการอัปเดต + ซิงค์ข้อมูลคลาวด์ + ระหว่างการซิงค์ข้อมูลคลาวด์ อุปกรณ์จะรีสตาร์ตและการเชื่อมต่อจะถูกตัดชั่วคราว ซึ่งเป็นเรื่องปกติ โปรดรออย่างอดทนให้ระบบเริ่มต้นฟังก์ชันของอุปกรณ์ใหม่ + เพื่อเพิ่มความเสถียรของอุปกรณ์ ให้ปรับการตั้งค่าระบบ: แผงควบคุม -> ตัวเลือกการใช้พลังงาน -> เปลี่ยนการตั้งค่าแผน -> เปลี่ยนการตั้งค่าพลังงานขั้นสูง -> การตั้งค่า USB -> การตั้งค่าการระงับแบบเลือกได้ของ USB -> [ปิดใช้งาน] + คำเตือน: โปรดดำเนินการขณะอุปกรณ์เชื่อมต่ออยู่ + เปิดไดรฟ์ USB (ทดสอบ) + ปิดไดรฟ์ USB (ทดสอบ) + 📁 เลือกไฟล์ + 📂 เลือกโฟลเดอร์ + คงการทำงานของอุปกรณ์ + ซิงค์ข้อมูลคลาวด์ + + + + + + diff --git a/AIK.Assets/Languages/Strings.tr.xaml b/AIK.Assets/Languages/Strings.tr.xaml new file mode 100644 index 0000000..53d3b94 --- /dev/null +++ b/AIK.Assets/Languages/Strings.tr.xaml @@ -0,0 +1,116 @@ + + AIK. Akıllı Anahtar Aracı + Ayarlar + Dil + Uygulama Ayarları + Uygulamanın temel parametrelerini ve tercihlerini yapılandırın + + Arayüz Ayarları + Önbellek Ayarları + Veri önbelleğini temizle + Arayüz dili: + Anahtar Modeli Listesi + + Kaydet + İptal + Onayla + + Marka adı, İngilizce veya pinyin ile arama... + Araç anahtarı adı, İngilizce veya pinyin ile arama... + + Alt birim: + Frekans: + Referans bilgileri: + Zaman: + ID: + + Referans Bilgileri + + Ayarlar başarıyla kaydedildi + + WeChat üzerinden bizimle iletişime geçin + E-posta ile bizimle iletişime geçin + WeChat'i kopyala + E-postayı kopyala + Cep telefonu + Puan + Çıkış yap + + Cihaz Güncellemesi + Geçerli Sürüm + En Son Sürüm + Cihaz Durumu + Cihaz Adı + Şimdi Güncelle + Sürümü Al + Uygulama Sürümü + Sunucu Sürümü + Cihaz Sürümü + Sürüm Güncellemeleri + Firmware, uygulama, cihaz güncellemeleri vb. + Yazma Kaydı + Anahtar Yazma Geçmişi + Puan yükleme + Üyelik + Puan geçmişi + Puanlarınız + Yükleme tutarını girin + Ödeme yöntemini seçin + WeChat Pay + PayPal + Öde + Uzaktan Algılama + Uzaktan Kopyalama + HCS / Sabit Kod Kopyalama + Frekansı algılamak için aşağıdaki Test düğmesine tıklayın. Cihazı bağladıktan sonra, test edilebilir mesafe içinde alt ünitenin bir tuşuna basın; frekans ekranda gösterilecektir. + Frekansı algılamak için aşağıdaki Test düğmesine tıklayın. Cihazı bağladıktan sonra, test edilebilir mesafe içinde alt ünitenin bir tuşuna basın; frekans ekranda gösterilecektir. + Mod: + + Tespit + Bu tuşu atla + Yeniden kontrol et + Kopyalamayı başlat + Kilitle + Kilidi aç + Bagajlık + Araç bulma + + Eşleşen marka bulunamadı + Eşleşen marka bulunamadı + Anahtar verisi yok + + Veri: + Tür: + Bit hızı: + Bit uzunluğu: + Programlamayı Başlat + İpucu + Arkadaşlarınıza önerin! WeChat, QQ vb. ile QR kodunu tarayarak indirip kurun. + Çilingirlere her zaman olduğu gibi destek vermeye devam edeceğiz. + Mobil uygulamayı indirmek için tarayın + En son haberleri ve desteği alın + Daha fazla görüntüle + Yükleniyor... + Bulut Verileri + Şimdi Güncelle + Daha Sonra Güncelle + Yeni Sürüm Bulundu + Güncelleme İpuçları + Bulut veri senkronizasyonu + Bulut verileri senkronize edilirken cihaz yeniden başlatılır ve bağlantı geçici olarak kesilir. Bu normaldir. Lütfen sistem cihaz işlevlerini yeniden başlatırken sabırla bekleyin. + Cihaz kararlılığını artırmak için sistem ayarlarını düzenleyin: Denetim Masası -> Güç Seçenekleri -> Plan ayarlarını değiştir -> Gelişmiş güç ayarlarını değiştir -> USB ayarları -> USB seçmeli askıya alma ayarı -> [Devre Dışı] + Uyarı: Lütfen işlemi cihaz bağlıyken gerçekleştirin + USB sürücüsünü aç (test) + USB sürücüsünü kapat (test) + 📁 Dosya seç + 📂 Klasör seç + Cihazı etkin tut + Bulut verilerini senkronize et + + + + + + diff --git a/AIK.Assets/Languages/Strings.uk.xaml b/AIK.Assets/Languages/Strings.uk.xaml new file mode 100644 index 0000000..8ba4fc4 --- /dev/null +++ b/AIK.Assets/Languages/Strings.uk.xaml @@ -0,0 +1,116 @@ + + AIK. Інтелектуальні ключі. Інструменти + Налаштування + Мова + Налаштування додатка + Налаштування базових параметрів та вподобань додатка + + Налаштування інтерфейсу + Налаштування кешу + Очистити кеш даних + Мова інтерфейсу: + Список моделей ключів + + Зберегти + Скасувати + Підтвердити + + Пошук назви бренду, англійською або піньїном... + Пошук назви автомобільного ключа, англійською або піньїном... + + Підмодуль: + Частота: + Довідкові відомості: + Час: + ID: + + Довідкові відомості + + Налаштування успішно збережено + + Зв'яжіться з нами через WeChat + Зв'яжіться з нами через Email + Копіювати WeChat + Копіювати Email + Мобільний телефон + Бали + Вийти + + Оновлення пристрою + Поточна версія + Остання версія + Стан пристрою + Назва пристрою + Оновити зараз + Перевірити версію + Версія додатка + Версія сервера + Версія пристрою + Центр оновлень + Оновлення прошивки, додатка, пристроїв тощо + Журнал запису + Історія запису ключа + Поповнення балів + Підписка + Історія балів + Ваші бали + Введіть суму поповнення + Виберіть спосіб оплати + WeChat Pay + PayPal + Оплатити + Віддалене виявлення + Копіювання пульта + Копіювання HCS / фіксованого коду + Натисніть кнопку «Тест» нижче, щоб визначити частоту. Після підключення пристрою натисніть кнопку на дочірньому пристрої в межах тестової дистанції — частота буде відображена. + Натисніть кнопку «Тест» нижче, щоб визначити частоту. Після підключення пристрою натисніть кнопку на дочірньому пристрої в межах тестової дистанції — частота буде відображена. + Режим: + + Виявлення + Пропустити цю клавішу + Перевірити знову + Почати копіювання + Заблокувати + Розблокувати + Багажник + Пошук автомобіля + + Відповідний бренд не знайдено + Відповідний бренд не знайдено + Дані ключа недоступні + + Дані: + Тип: + Бітрейт: + Довжина в бітах: + Bắt đầu lập trình + Порада + Рекомендуйте друзям! Відскануйте QR-код за допомогою WeChat, QQ тощо, щоб завантажити та встановити. + Ми й надалі будемо підтримувати слюсарів, як і раніше. + Відскануйте, щоб завантажити мобільний додаток + Отримуйте останні новини та підтримку + Показати більше + Завантаження... + Хмарні Дані + Оновити Тепер + Оновити Пізніше + Знайдено Нову Версію + Поради з Оновлення + Синхронізація хмарних даних + Під час синхронізації хмарних даних пристрій перезавантажиться, а з’єднання буде тимчасово перервано. Це нормально. Будь ласка, зачекайте, доки система повторно ініціалізує функції пристрою. + Щоб підвищити стабільність пристрою, змініть параметри системи: Панель керування -> Електроживлення -> Змінити параметри плану -> Змінити додаткові параметри живлення -> Параметри USB -> Параметр вибіркового призупинення USB -> [Вимкнено] + Нагадування: виконуйте операцію, коли пристрій підключено + Відкрити USB-накопичувач (тест) + Закрити USB-накопичувач (тест) + 📁 Вибрати файл + 📂 Вибрати папку + Підтримувати пристрій активним + Синхронізувати хмарні дані + + + + + + diff --git a/AIK.Assets/Languages/Strings.vi.xaml b/AIK.Assets/Languages/Strings.vi.xaml new file mode 100644 index 0000000..67b4af4 --- /dev/null +++ b/AIK.Assets/Languages/Strings.vi.xaml @@ -0,0 +1,116 @@ + + AIK. Công cụ khóa thông minh + Cài đặt + Ngôn ngữ + Cài đặt ứng dụng + Cấu hình tham số cơ bản và tùy chọn ứng dụng + + Cài đặt giao diện + Cài đặt bộ nhớ đệm + Xóa bộ nhớ đệm dữ liệu + Ngôn ngữ giao diện: + Danh sách mô hình khóa + + Lưu + Hủy + Xác nhận + + Tìm kiếm tên thương hiệu, tiếng Anh hoặc pinyin... + Tìm kiếm tên khóa xe, tiếng Anh hoặc pinyin... + + Đơn vị phụ: + Tần số: + Thông tin tham chiếu: + Thời gian: + ID: + + Thông tin tham chiếu + + Cài đặt đã được lưu thành công + + Liên hệ chúng tôi qua WeChat + Liên hệ chúng tôi qua Email + Sao chép WeChat + Sao chép Email + Điện thoại di động + Điểm + Đăng xuất + + Cập nhật thiết bị + Phiên bản hiện tại + Phiên bản mới nhất + Trạng thái thiết bị + Tên thiết bị + Nâng cấp ngay + Lấy phiên bản + Phiên bản ứng dụng + Phiên bản máy chủ + Phiên bản thiết bị + Trung tâm cập nhật phiên bản + Cập nhật firmware, ứng dụng, thiết bị, v.v. + Nhật ký ghi + Lịch sử ghi khóa + Nạp điểm + Thành viên + Lịch sử điểm + Điểm của bạn + Nhập số tiền nạp + Chọn phương thức thanh toán + WeChat Pay + PayPal + Thanh toán + Phát hiện từ xa + Sao chép điều khiển từ xa + Sao chép HCS / Mã cố định + Nhấp vào nút Kiểm tra bên dưới để phát hiện tần số. Sau khi kết nối thiết bị, nhấn một phím trên thiết bị phụ trong phạm vi kiểm tra được, tần số sẽ hiển thị ngay lập tức. + Nhấp vào nút Kiểm tra bên dưới để phát hiện tần số. Sau khi kết nối thiết bị, nhấn một phím trên thiết bị phụ trong phạm vi kiểm tra được, tần số sẽ hiển thị ngay lập tức. + Chế độ: + + Phát hiện + Bỏ qua phím này + Kiểm tra lại + Bắt đầu sao chép + Khóa + Mở khóa + Cốp xe + Tìm xe + + Không tìm thấy thương hiệu phù hợp + Không tìm thấy thương hiệu phù hợp + Không có dữ liệu chìa khóa + + Dữ liệu: + Loại: + Tốc độ bit: + Độ dài bit: + Bắt đầu lập trình + Mẹo + Giới thiệu cho bạn bè của bạn! Quét mã QR bằng WeChat, QQ, v.v. để tải về và cài đặt. + Chúng tôi sẽ tiếp tục đồng hành cùng thợ khóa như vẫn thường làm. + Quét để tải ứng dụng di động + Nhận thông tin mới nhất và hỗ trợ + Xem thêm + Đang tải... + Dữ Liệu Điện Tử + Cập Nhật Ngay + Cập Nhật Sau + Tìm Thấy Phiên Bản Mới + Mẹo Cập Nhật + Đồng bộ dữ liệu đám mây + Trong quá trình đồng bộ dữ liệu đám mây, thiết bị sẽ khởi động lại và kết nối sẽ tạm thời bị ngắt. Đây là hiện tượng bình thường. Vui lòng kiên nhẫn chờ hệ thống khởi tạo lại các chức năng của thiết bị. + Để tăng độ ổn định của thiết bị, hãy điều chỉnh cài đặt hệ thống: Bảng điều khiển -> Tùy chọn nguồn -> Thay đổi cài đặt gói -> Thay đổi cài đặt nguồn nâng cao -> Cài đặt USB -> Cài đặt tạm ngưng chọn lọc USB -> [Đã tắt] + Nhắc nhở: Vui lòng thao tác khi thiết bị đã được kết nối + Mở ổ USB (thử nghiệm) + Đóng ổ USB (thử nghiệm) + 📁 Chọn tệp + 📂 Chọn thư mục + Giữ thiết bị hoạt động + Đồng bộ dữ liệu đám mây + + + + + + diff --git a/AIK.Assets/Languages/Strings.zh.xaml b/AIK.Assets/Languages/Strings.zh.xaml new file mode 100644 index 0000000..7ffdf7d --- /dev/null +++ b/AIK.Assets/Languages/Strings.zh.xaml @@ -0,0 +1,118 @@ + + AIK.智能钥匙工具 + 设置 + 语言 + 应用设置 + 配置应用程序的基本参数和偏好设置 + + 界面设置 + 缓存设置 + 清除数据缓存 + 界面语言: + 钥匙型号列表 + + 保存 + 取消 + 确认 + + 搜索品牌名称、英文或拼音... + 搜索车辆钥匙名称、英文或拼音... + + 子机: + 频率: + 参考信息: + 时间: + ID: + + 参考信息 + + 设置保存成功 + + 微信联系我们 + 邮箱联系我们 + 微信复制 + 邮箱复制 + 手机 + 积分 + 登出 + + 设备升级 + 当前版本 + 最新版本 + 设备状态 + 设备名称 + 立即升级 + 获取版本 + APP版本 + 服务器版本 + 设备版本 + 版本升级 + 固件、应用、设备等版本升级 + 烧录记录 + 钥匙烧录记录 + 积分充值 + 会员开通 + 积分记录 + 您的积分 + 请输入充值金额 + 请选择支付方式 + 微信支付 + PayPal + 支付 + + 遥控检测 + 遥控拷贝 + HCS/固定码拷贝 + 点击下方检测按钮检测频率,连接设备后,按下子机按键并在可测试距离内,即能显示频率 + 点击下方检测按钮检测频率,连接设备后,按下子机按键并在可测试距离内,即能显示频率 + 模式: + + 检测 + 跳过此键 + 重新检测 + 开始拷贝 + 锁车 + 开锁 + 后备箱 + 寻车 + + 没有找到匹配的品牌 + 没有找到匹配的品牌 + 暂无钥匙数据 + + 数据: + 类型: + 码率: + 位数: + + 开始烧录 + 提示 + 推荐给你的朋友吧,使用微信、qq等扫描二维码,即可下载安装 + 我们将一如既往的为广大锁匠保驾护航 + 扫码下载移动端 + 获取最新资讯与服务支持 + 查看更多 + 加载中... + 云数据 + 立即更新 + 稍后更新 + 发现新版本 + 更新提示 + 云数据同步 + 同步云数据时,设备会重启,连接会暂时断开属于正常现象,请耐心等待系统初始化设备功能 + 增加设备稳定性,可调系统设置:控制面板 -> 电源选项 -> 更改计划设置 -> 更改高级电源设置 -> USB 设置 -> USB 选择性暂停设置 -> [已禁用] + 提醒:请在设备已连接状态下进行操作 + 打开U盘(测试) + 关闭U盘(测试) + 📁 选择文件 + 📂 选择文件夹 + 持续保活(设备) + 同步云数据 + + + + + + diff --git a/AIK.Assets/Styles/BannerStyle.xaml b/AIK.Assets/Styles/BannerStyle.xaml new file mode 100644 index 0000000..bfae906 --- /dev/null +++ b/AIK.Assets/Styles/BannerStyle.xaml @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/AIK.Assets/Styles/ButtonStyles.xaml b/AIK.Assets/Styles/ButtonStyles.xaml new file mode 100644 index 0000000..679aeb9 --- /dev/null +++ b/AIK.Assets/Styles/ButtonStyles.xaml @@ -0,0 +1,169 @@ + + + + + + + + + \ No newline at end of file diff --git a/AIK.Assets/Styles/HintTitleStyle.xaml b/AIK.Assets/Styles/HintTitleStyle.xaml new file mode 100644 index 0000000..ccd7c2c --- /dev/null +++ b/AIK.Assets/Styles/HintTitleStyle.xaml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/AIK.Assets/Styles/LoadingStyle.xaml b/AIK.Assets/Styles/LoadingStyle.xaml new file mode 100644 index 0000000..c724670 --- /dev/null +++ b/AIK.Assets/Styles/LoadingStyle.xaml @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Assets/Styles/ScrollViewerStyle.xaml b/AIK.Assets/Styles/ScrollViewerStyle.xaml new file mode 100644 index 0000000..59871ea --- /dev/null +++ b/AIK.Assets/Styles/ScrollViewerStyle.xaml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Assets/Styles/StateStyle.xaml b/AIK.Assets/Styles/StateStyle.xaml new file mode 100644 index 0000000..ad5bfa8 --- /dev/null +++ b/AIK.Assets/Styles/StateStyle.xaml @@ -0,0 +1,209 @@ + + + + #0078D7 + #E3F2FD + #BBDEFB + #2196F3 + #FF9800 + #F44336 + #4CAF50 + #607D8B + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Assets/Styles/VideoControlButtonStyle.xaml b/AIK.Assets/Styles/VideoControlButtonStyle.xaml new file mode 100644 index 0000000..fd1a89f --- /dev/null +++ b/AIK.Assets/Styles/VideoControlButtonStyle.xaml @@ -0,0 +1,52 @@ + + + + + \ No newline at end of file diff --git a/AIK.Behaviors/AIK.Behaviors.csproj b/AIK.Behaviors/AIK.Behaviors.csproj new file mode 100644 index 0000000..0c82216 --- /dev/null +++ b/AIK.Behaviors/AIK.Behaviors.csproj @@ -0,0 +1,29 @@ + + + + net48 + enable + true + 12 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Behaviors/ClickBehavior.cs b/AIK.Behaviors/ClickBehavior.cs new file mode 100644 index 0000000..c392f86 --- /dev/null +++ b/AIK.Behaviors/ClickBehavior.cs @@ -0,0 +1,63 @@ +using Microsoft.Xaml.Behaviors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; + +namespace AIK.Behaviors +{ + public class ClickBehavior : Behavior + { + public static readonly DependencyProperty CommandProperty = + DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(ClickBehavior)); + + public static readonly DependencyProperty CommandParameterProperty = + DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(ClickBehavior)); + + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + public object CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.MouseLeftButtonDown += OnMouseLeftButtonDown; + AssociatedObject.Loaded += OnLoaded; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject.MouseLeftButtonDown -= OnMouseLeftButtonDown; + AssociatedObject.Loaded -= OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + // 调试信息 + //System.Diagnostics.Debug.WriteLine($"ClickBehavior loaded on {AssociatedObject.GetType().Name}"); + //System.Diagnostics.Debug.WriteLine($"Command: {Command}"); + //System.Diagnostics.Debug.WriteLine($"CommandParameter: {CommandParameter}"); + } + + private void OnMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (Command?.CanExecute(CommandParameter) == true) + { + Command.Execute(CommandParameter); + e.Handled = true; + } + } + } +} diff --git a/AIK.Behaviors/PlaceholderBehavior.cs b/AIK.Behaviors/PlaceholderBehavior.cs new file mode 100644 index 0000000..dc93f2b --- /dev/null +++ b/AIK.Behaviors/PlaceholderBehavior.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; + +namespace AIK.Behaviors +{ + public static class PlaceholderBehavior + { + #region 依赖属性定义 + private static readonly DependencyPropertyKey IsPlaceholderActivePropertyKey = + DependencyProperty.RegisterAttachedReadOnly( + "IsPlaceholderActive", + typeof(bool), + typeof(PlaceholderBehavior), + new PropertyMetadata(false)); + + public static readonly DependencyProperty IsPlaceholderActiveProperty = IsPlaceholderActivePropertyKey.DependencyProperty; + + public static readonly DependencyProperty PlaceholderTextProperty = + DependencyProperty.RegisterAttached( + "PlaceholderText", + typeof(string), + typeof(PlaceholderBehavior), + new PropertyMetadata(string.Empty, OnPlaceholderTextChanged)); + + // 添加一个属性来存储原始绑定 + private static readonly DependencyProperty OriginalBindingProperty = + DependencyProperty.RegisterAttached( + "OriginalBinding", + typeof(Binding), + typeof(PlaceholderBehavior)); + + // 添加一个属性来标记是否正在设置Placeholder + private static readonly DependencyProperty IsSettingPlaceholderProperty = + DependencyProperty.RegisterAttached( + "IsSettingPlaceholder", + typeof(bool), + typeof(PlaceholderBehavior), + new PropertyMetadata(false)); + #endregion + + #region 公共方法 + public static bool GetIsPlaceholderActive(DependencyObject obj) + { + return (bool)obj.GetValue(IsPlaceholderActiveProperty); + } + + public static string GetPlaceholderText(DependencyObject obj) + { + return (string)obj.GetValue(PlaceholderTextProperty); + } + + public static void SetPlaceholderText(DependencyObject obj, string value) + { + obj.SetValue(PlaceholderTextProperty, value); + } + #endregion + + #region 私有方法 + private static bool GetIsSettingPlaceholder(DependencyObject obj) + { + return (bool)obj.GetValue(IsSettingPlaceholderProperty); + } + + private static void SetIsSettingPlaceholder(DependencyObject obj, bool value) + { + obj.SetValue(IsSettingPlaceholderProperty, value); + } + + private static Binding GetOriginalBinding(DependencyObject obj) + { + return (Binding)obj.GetValue(OriginalBindingProperty); + } + + private static void SetOriginalBinding(DependencyObject obj, Binding value) + { + obj.SetValue(OriginalBindingProperty, value); + } + #endregion + + #region 事件处理 + private static void OnPlaceholderTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextBox textBox) + { + // 确保只注册一次事件 + textBox.Loaded -= OnTextBoxLoaded; + textBox.GotFocus -= OnTextBoxGotFocus; + textBox.LostFocus -= OnTextBoxLostFocus; + textBox.TextChanged -= OnTextBoxTextChanged; + + textBox.Loaded += OnTextBoxLoaded; + textBox.GotFocus += OnTextBoxGotFocus; + textBox.LostFocus += OnTextBoxLostFocus; + textBox.TextChanged += OnTextBoxTextChanged; + + // 保存原始绑定 + var binding = BindingOperations.GetBinding(textBox, TextBox.TextProperty); + if (binding != null) + { + SetOriginalBinding(textBox, binding); + } + + // 如果已经加载,立即初始化 + if (textBox.IsLoaded) + { + InitializePlaceholder(textBox); + } + } + } + + private static void OnTextBoxLoaded(object sender, RoutedEventArgs e) + { + InitializePlaceholder((TextBox)sender); + } + + private static void OnTextBoxGotFocus(object sender, RoutedEventArgs e) + { + var textBox = (TextBox)sender; + if (GetIsPlaceholderActive(textBox)) + { + // 标记开始设置Placeholder,避免触发数据源更新 + SetIsSettingPlaceholder(textBox, true); + + // 移除placeholder + textBox.Text = string.Empty; + textBox.SetValue(IsPlaceholderActivePropertyKey, false); + textBox.Foreground = Brushes.Black; + + // 恢复数据绑定 + RestoreBinding(textBox); + + SetIsSettingPlaceholder(textBox, false); + } + } + + private static void OnTextBoxLostFocus(object sender, RoutedEventArgs e) + { + var textBox = (TextBox)sender; + if (string.IsNullOrEmpty(textBox.Text)) + { + ShowPlaceholder(textBox); + } + } + + private static void OnTextBoxTextChanged(object sender, TextChangedEventArgs e) + { + var textBox = (TextBox)sender; + + // 如果正在设置Placeholder,不处理TextChanged事件 + if (GetIsSettingPlaceholder(textBox)) + return; + + // 只有在不是placeholder状态时才处理 + if (!GetIsPlaceholderActive(textBox)) + { + // 如果文本变为空,显示placeholder + if (string.IsNullOrEmpty(textBox.Text) && !textBox.IsFocused) + { + ShowPlaceholder(textBox); + } + else + { + // 正常文本变化,确保数据绑定是激活的 + EnsureBindingActive(textBox); + } + } + } + #endregion + + #region 核心方法 + private static void InitializePlaceholder(TextBox textBox) + { + // 检查当前文本是否为空或者就是Placeholder文本 + var currentText = textBox.Text; + var placeholderText = GetPlaceholderText(textBox); + + if (string.IsNullOrEmpty(currentText) || currentText == placeholderText) + { + ShowPlaceholder(textBox); + } + } + + private static void ShowPlaceholder(TextBox textBox) + { + var placeholderText = GetPlaceholderText(textBox); + if (!string.IsNullOrEmpty(placeholderText)) + { + // 标记开始设置Placeholder + SetIsSettingPlaceholder(textBox, true); + + // 临时解除数据绑定,避免触发数据源更新 + SuspendBinding(textBox); + + // 设置Placeholder文本 + textBox.Text = placeholderText; + textBox.SetValue(IsPlaceholderActivePropertyKey, true); + textBox.Foreground = new SolidColorBrush(Color.FromArgb(0xFF, 0xA0, 0xAE, 0xC0)); + + SetIsSettingPlaceholder(textBox, false); + } + } + + // 临时解除数据绑定 + private static void SuspendBinding(TextBox textBox) + { + // 清除绑定,但保留绑定表达式 + var bindingExpression = textBox.GetBindingExpression(TextBox.TextProperty); + if (bindingExpression != null) + { + // 保存原始绑定(如果还没有保存的话) + if (GetOriginalBinding(textBox) == null) + { + var binding = BindingOperations.GetBinding(textBox, TextBox.TextProperty); + SetOriginalBinding(textBox, binding); + } + + // 清除绑定,这样设置文本就不会更新数据源 + BindingOperations.ClearBinding(textBox, TextBox.TextProperty); + } + } + + // 恢复数据绑定 + private static void RestoreBinding(TextBox textBox) + { + var originalBinding = GetOriginalBinding(textBox); + if (originalBinding != null) + { + // 恢复原始绑定 + textBox.SetBinding(TextBox.TextProperty, originalBinding); + + // 立即更新数据源为空值 + var bindingExpression = textBox.GetBindingExpression(TextBox.TextProperty); + bindingExpression?.UpdateSource(); + } + } + + // 确保数据绑定是激活的 + private static void EnsureBindingActive(TextBox textBox) + { + var currentBinding = BindingOperations.GetBinding(textBox, TextBox.TextProperty); + var originalBinding = GetOriginalBinding(textBox); + + // 如果没有当前绑定但有原始绑定,恢复绑定 + if (currentBinding == null && originalBinding != null) + { + textBox.SetBinding(TextBox.TextProperty, originalBinding); + } + } + #endregion + } +} + diff --git a/AIK.Behaviors/ViewModelInitializationBehavior.cs b/AIK.Behaviors/ViewModelInitializationBehavior.cs new file mode 100644 index 0000000..05acf7b --- /dev/null +++ b/AIK.Behaviors/ViewModelInitializationBehavior.cs @@ -0,0 +1,38 @@ +using AIK.Service.IService; +using Microsoft.Xaml.Behaviors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Behaviors +{ + public class ViewModelInitializationBehavior : Behavior + { + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.Loaded += OnViewLoaded; + } + + protected override void OnDetaching() + { + AssociatedObject.Loaded -= OnViewLoaded; + base.OnDetaching(); + } + + private async void OnViewLoaded(object sender, RoutedEventArgs e) + { + if (AssociatedObject.DataContext is ILoadAware loadAware) + { + await loadAware.OnLoadedAsync(); + } + //else if (AssociatedObject.DataContext is IInitializable initializable) + //{ + // await initializable.InitializeAsync(); + //} + } + } +} diff --git a/AIK.Behaviors/ViewModelInitializationWindowBehavior.cs b/AIK.Behaviors/ViewModelInitializationWindowBehavior.cs new file mode 100644 index 0000000..4f96355 --- /dev/null +++ b/AIK.Behaviors/ViewModelInitializationWindowBehavior.cs @@ -0,0 +1,44 @@ +using AIK.Service.IService; +using Microsoft.Xaml.Behaviors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Behaviors +{ + public class ViewModelInitializationWindowBehavior : Behavior + { + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.Loaded += OnViewLoaded; + AssociatedObject.Loaded += OnViewModelRequestClose; + } + + protected override void OnDetaching() + { + AssociatedObject.Loaded -= OnViewLoaded; + AssociatedObject.Loaded -= OnViewModelRequestClose; + base.OnDetaching(); + } + + private async void OnViewLoaded(object sender, RoutedEventArgs e) + { + if (AssociatedObject.DataContext is ILoadAware loadAware) + { + await loadAware.OnLoadedAsync(); + } + } + private void OnViewModelRequestClose(object? sender, EventArgs e) + { + AssociatedObject?.Dispatcher.Invoke(() => + { + if (AssociatedObject.IsVisible) + AssociatedObject.Close(); + }); + } + } +} diff --git a/AIK.Common/AIK.Common.csproj b/AIK.Common/AIK.Common.csproj new file mode 100644 index 0000000..ffc03f9 --- /dev/null +++ b/AIK.Common/AIK.Common.csproj @@ -0,0 +1,68 @@ + + + + net48 + enable + true + 12 + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + True + True + Settings.settings + + + + + + PublicSettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Common/AIKProtocol/AIKCommunication.cs b/AIK.Common/AIKProtocol/AIKCommunication.cs new file mode 100644 index 0000000..bcdd979 --- /dev/null +++ b/AIK.Common/AIKProtocol/AIKCommunication.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.AIKProtocol +{ + public class AIKCommunication + { + /// + /// 数据包总包数 + /// + public int TotalPackets { get; set; } = 0; + /// + /// 发送的文件数据(bin文件) + /// + public byte[] FileData { get; set; } + /// + /// 当前正在发送包的索引 + /// + public int CurrentIndex { get; set; } = 0; + + private readonly object _sendLock = new object(); + private bool _isSending = false; + /// + /// 所有数据包是否完成发送 + /// + private bool _isAllSendSuccess = false; + #region 数据包发送 + /// + /// 发送起始码,建立连接使用 + /// + /// + public byte[] GetStartFrame() + { + var buf = new byte[8] { 0XA5, 0X5E, 0XAA, 0XBB, 0XCC, 0XDD, 0X00, 0X00 }; + return buf; + } + + /// + /// 发送头帧 + /// + /// + public async Task SendHeader(string sourceitem) + { + List bytes = new List(); + bytes.Add(0x01); + bytes.Add(0x00); + bytes.Add(0xFF); + int lastSlashIndex = sourceitem.LastIndexOf('/'); + string fileName = lastSlashIndex >= 0 ? sourceitem.Substring(lastSlashIndex + 1) : sourceitem; + byte[] filenamedata = Encoding.Default.GetBytes(fileName); + bytes.AddRange(filenamedata); + bytes.Add(0x00); + //string sourceitem = "/keys/bin/1.bin"; + //string sourceitem = "/keys/bin/(7926)AiLiSheng47DaiA_DaiZuoYouCeHuaMen13NianHou.bin"; + //FileData = await WebHelper.GetVehKeyBin(sourceitem); + if (FileData == null) + { + return; + } + TotalPackets = FileData.Length / 1024; + byte[] DataBuffdata = Encoding.Default.GetBytes(FileData.Length.ToString()); + bytes.AddRange(DataBuffdata); + bytes.Add(0x00); + if (bytes.Count < 128) + { + int copi = 128 + 3 - bytes.Count; + for (int i = 0; i < copi; i++) + { + bytes.Add(0x00); + } + } + var cdata = new byte[128]; + Array.Copy(bytes.ToArray(), 3, cdata, 0, 128); + var crc = ProtocolHelper.CalculateCRC16(cdata); + var CRCHigh = (byte)((crc >> 8) & 0xFF); + var CRCLow = (byte)(crc & 0xFF); + bytes.Add((byte)CRCHigh); + bytes.Add((byte)CRCLow); + + var rdata = bytes.ToArray(); + await WriteLargeDataFrame(rdata, 100); + } + /// + /// 发送数据帧 + /// + /// + /// + public async Task SendDataFrame(int page) + { + bool flag = false; + var rdata = GetSendDataFrame(page, FileData); + bool success = await WriteLargeDataFrame(rdata.ToArray(), 100); + if (success) + { + flag = true; + } + else + { + Debug.WriteLine("TestData发送失败"); + } + return flag; + } + + /// + /// 发送结束帧 + /// + /// + public async Task SendEOTFrame() + { + var rdata = new byte[133]; + rdata[0] = 0x01; + rdata[1] = 0x00; + rdata[2] = 0xFF; + await WriteLargeDataFrame(rdata, 100); + } + + /// + /// 根据包页及文件数据获取数据帧 + /// + /// + /// + /// + private List GetSendDataFrame(int page, byte[] filedata) + { + try + { + var bytes = new List(); + if (page == filedata.Length / 1024 && filedata.Length % 1024 == 0) + { + return bytes; + } + bytes.Add(0x02); + bytes.Add((byte)(page)); + bytes.Add((byte)~(page)); + if (page == filedata.Length / 1024 - 1 && filedata.Length % 1024 <= 128 && filedata.Length % 1024 > 0) + { + var cdata = new byte[128]; + Array.Copy(filedata, page * 1024, cdata, 0, filedata.Length - page * 1024); + var crc = ProtocolHelper.CalculateCRC16(cdata); + var CRCHigh = (byte)((crc >> 8) & 0xFF); + var CRCLow = (byte)(crc & 0xFF); + bytes.AddRange(cdata); + bytes.Add((byte)CRCHigh); + bytes.Add((byte)CRCLow); + } + else + { + var cdata = new byte[1024]; + var arrarycount = 1024; + if (filedata.Length - page * 1024 > 0 && filedata.Length - page * 1024 < 1024) + { + arrarycount = filedata.Length - page * 1024; + } + Array.Copy(filedata, page * 1024, cdata, 0, arrarycount); + var crc = ProtocolHelper.CalculateCRC16(cdata); + var CRCHigh = (byte)((crc >> 8) & 0xFF); + var CRCLow = (byte)(crc & 0xFF); + bytes.AddRange(cdata); + bytes.Add((byte)CRCHigh); + bytes.Add((byte)CRCLow); + } + return bytes; + } + catch (Exception ex) + { + throw; + } + + } + #endregion + + #region 写入数据帧 + /// + /// 分包发送大数据 + /// + public async Task WriteLargeDataFrame(byte[] data, int maxPacketSize = 100) + { + try + { + Debug.WriteLine($"开始发送数据,总长度: {data.Length} 字节,分包大小: {maxPacketSize} 字节"); + + int totalPackets = (int)Math.Ceiling((double)data.Length / maxPacketSize); + int successfulPackets = 0; + + for (int i = 0; i < totalPackets; i++) + { + int offset = i * maxPacketSize; + int length = Math.Min(maxPacketSize, data.Length - offset); + byte[] packet = new byte[length]; + Array.Copy(data, offset, packet, 0, length); + + bool packetSuccess = await WritePacketDataFrame(packet, i + 1, totalPackets); + if (packetSuccess) + { + successfulPackets++; + } + else + { + Debug.WriteLine($"第 {i + 1} 包发送失败"); + // 可以选择重试或直接失败 + // return false; + } + + // 添加小延迟,避免发送过快 + await Task.Delay(30); + } + + bool overallSuccess = successfulPackets == totalPackets; + Debug.WriteLine($"数据发送完成: {successfulPackets}/{totalPackets} 包成功"); + return overallSuccess; + } + catch (Exception ex) + { + Debug.WriteLine($"发送大数据异常: {ex.Message}"); + return false; + } + } + /// + /// 发送单个数据包 + /// + private async Task WritePacketDataFrame(byte[] packet, int packetNumber, int totalPackets) + { + try + { + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"发送第 {packetNumber} 包异常: {ex.Message}"); + return false; + } + } + #endregion + + } +} diff --git a/AIK.Common/AIKProtocol/K3ToolUSBProtocol.cs b/AIK.Common/AIKProtocol/K3ToolUSBProtocol.cs new file mode 100644 index 0000000..237dc7a --- /dev/null +++ b/AIK.Common/AIKProtocol/K3ToolUSBProtocol.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.AIKProtocol +{ + public static class K3ToolUSBProtocol + { + private static int _totalPackets; + private static byte[]? _fileData; + private static byte[]? _endEOTData; + private static byte[]? _connectData; + private static byte[]? _createRemoteData; + private static byte[]? _closetheUdiskData; + private static byte[]? _opentheUdiskData; + private static readonly object _lockObject = new object(); + public static readonly string ConnectFileName = "usb connect.bin"; + public static readonly string CreateRemoteFileName = "usb create remote.bin"; + public static int TotalPackets + { + get => _totalPackets; + set => _totalPackets = value; + } + + public static byte[] FileData + { + get => _fileData ?? Array.Empty(); + set + { + lock (_lockObject) + { + _fileData = value; + // 当 FileData 不为空时,自动计算 TotalPackets + if (_fileData != null && _fileData.Length > 0) + { + _totalPackets = (int)Math.Ceiling(_fileData.Length / 1024.0); + } + else + { + _totalPackets = 0; + } + } + } + } + + public static byte[] EndEOTData + { + get + { + if (_endEOTData == null || _endEOTData.Length == 0) + { + lock (_lockObject) + { + // 双重检查锁定 + if (_endEOTData == null || _endEOTData.Length == 0) + { + _endEOTData = GetBinData(ResourceEOTName); + } + } + } + return _endEOTData; + } + set => _endEOTData = value; + } + + public static byte[] ConnectData + { + get + { + if (_connectData == null || _connectData.Length == 0) + { + lock (_lockObject) + { + // 双重检查锁定 + if (_connectData == null || _connectData.Length == 0) + { + _connectData = GetBinData(ResourceConnectName); + } + } + } + return _connectData; + } + set => _connectData = value; + } + + public static byte[] CreateRemoteData + { + get + { + if (_createRemoteData == null || _createRemoteData.Length == 0) + { + lock (_lockObject) + { + // 双重检查锁定 + if (_createRemoteData == null || _createRemoteData.Length == 0) + { + _createRemoteData = GetBinData(ResourceCreateRemoteName); + } + } + } + return _createRemoteData; + } + set => _createRemoteData = value; + } + + public static byte[] ClosetheUdiskData + { + get + { + if (_closetheUdiskData == null || _closetheUdiskData.Length == 0) + { + lock (_lockObject) + { + // 双重检查锁定 + if (_closetheUdiskData == null || _closetheUdiskData.Length == 0) + { + _closetheUdiskData = GetBinData(ResourceClosetheUdiskName); + } + } + } + return _closetheUdiskData; + } + set => _closetheUdiskData = value; + } + + public static byte[] OpentheUdiskData + { + get + { + if (_opentheUdiskData == null || _opentheUdiskData.Length == 0) + { + lock (_lockObject) + { + // 双重检查锁定 + if (_opentheUdiskData == null || _opentheUdiskData.Length == 0) + { + _opentheUdiskData = GetBinData(ResourceOpentheUdiskName); + } + } + } + return _opentheUdiskData; + } + set => _opentheUdiskData = value; + } + + public static string ResourceEOTName { get; set; } = "AIK.Common.Resources.eot.bin"; + public static string ResourceConnectName { get; set; } = "AIK.Common.Resources.usb connect.bin"; + public static string ResourceCreateRemoteName { get; set; } = "AIK.Common.Resources.usb create remote.bin"; + + public static string ResourceClosetheUdiskName { get; set; } = "AIK.Common.Resources.Close the U disk.bin"; + + public static string ResourceOpentheUdiskName { get; set; } = "AIK.Common.Resources.Open the U disk.bin"; + #region 头帧 + public static byte[] GetSendHeader(string sourceitem) + { + // 保持原有实现 + List bytes = new List(); + bytes.Add(0x01); + bytes.Add(0x00); + bytes.Add(0xFF); + + int lastSlashIndex = sourceitem.LastIndexOf('/'); + string fileName = lastSlashIndex >= 0 ? sourceitem.Substring(lastSlashIndex + 1) : sourceitem; + byte[] filenamedata = Encoding.GetEncoding(936).GetBytes(fileName); + bytes.AddRange(filenamedata); + bytes.Add(0x00); + + if (FileData == null) return new byte[0]; + + TotalPackets = (int)Math.Ceiling(FileData.Length / 1024.0); + byte[] dataLengthData = Encoding.GetEncoding(936).GetBytes(FileData.Length.ToString()); + bytes.AddRange(dataLengthData); + bytes.Add(0x00); + + // 填充到128字节 + if (bytes.Count < 128 + 3) + { + bytes.AddRange(new byte[128 + 3 - bytes.Count]); + } + + var cdata = new byte[128]; + Array.Copy(bytes.ToArray(), 3, cdata, 0, 128); + var crc = ProtocolHelper.CalculateCRC16(cdata); + bytes.Add((byte)((crc >> 8) & 0xFF)); + bytes.Add((byte)(crc & 0xFF)); + + return bytes.ToArray(); + } + #endregion + + #region 数据帧 + public static List GetSendDataFrame(int page, byte[] filedata) + { + try + { + var bytes = new List(); + if (page >= TotalPackets) return bytes; + + bytes.Add(0x02); + bytes.Add((byte)(page + 1)); + bytes.Add((byte)~(page + 1)); + + int dataStart = page * 1024; + int dataLength = Math.Min(1024, filedata.Length - dataStart); + var frameData = new byte[1024]; + Array.Copy(filedata, dataStart, frameData, 0, dataLength); + + var crc = ProtocolHelper.CalculateCRC16(frameData); + bytes.AddRange(frameData); + bytes.Add((byte)((crc >> 8) & 0xFF)); + bytes.Add((byte)(crc & 0xFF)); + + return bytes; + } + catch (Exception ex) + { + Debug.WriteLine($"生成数据帧错误: {ex.Message}"); + return new List(); + } + } + #endregion + + #region 资源文件帧 + /// + /// 获取bin文件转成byte数组 + /// + /// + /// + private static byte[] GetBinData(string resourceName) + { + try + { + var assembly = Assembly.GetExecutingAssembly(); + + // 查看所有资源名称(调试用) + // var names = assembly.GetManifestResourceNames(); + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + throw new FileNotFoundException($"未找到嵌入资源: {resourceName}"); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + catch (Exception ex) + { + return Array.Empty(); + } + + } + + #region Connect帧 + /// + /// 手动刷新 Connect 数据(强制重新加载) + /// + public static void RefreshConnectData() + { + lock (_lockObject) + { + _connectData = GetBinData(ResourceConnectName); + } + } + + /// + /// 清除缓存的 Connect 数据(下次访问时会重新加载) + /// + public static void ClearConnectDataCache() + { + lock (_lockObject) + { + _connectData = null; + } + } + #endregion + + #region CreateRemote帧 + /// + /// 手动刷新 CreateRemote 数据(强制重新加载) + /// + public static void RefreshCreateRemoteData() + { + lock (_lockObject) + { + _createRemoteData = GetBinData(ResourceCreateRemoteName); + } + } + + /// + /// 清除缓存的 CreateRemote数据(下次访问时会重新加载) + /// + public static void ClearCreateRemoteDataCache() + { + lock (_lockObject) + { + _createRemoteData = null; + } + } + #endregion + + #region EOT帧 + /// + /// 手动刷新 EOT 数据(强制重新加载) + /// + public static void RefreshEndEOTData() + { + lock (_lockObject) + { + _endEOTData = GetBinData(ResourceEOTName); + } + } + + /// + /// 清除缓存的 EOT 数据(下次访问时会重新加载) + /// + public static void ClearEndEOTDataCache() + { + lock (_lockObject) + { + _endEOTData = null; + } + } + #endregion + #endregion + + #region 最终结束帧 + /// + /// 最终结束帧 + /// + /// + public static byte[] GetEndDataFrame() + { + var bytes = new byte[133]; + bytes[0] = 0x01; + bytes[1] = 0x00; + bytes[2] = 0xFF; + // 其余128字节默认为0,无需手动设置 + return bytes; + } + #endregion + + #region 工具方法 + /// + /// 设置文件数据(自动计算 TotalPackets) + /// + /// 文件数据 + public static void SetFileData(byte[] fileData) + { + FileData = fileData; // 这会自动触发 TotalPackets 计算 + } + + /// + /// 从文件路径加载数据(自动计算 TotalPackets) + /// + /// 文件路径 + public static void LoadFileData(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"文件不存在: {filePath}"); + + var fileData = File.ReadAllBytes(filePath); + FileData = fileData; // 自动计算 TotalPackets + } + + /// + /// 获取指定文件的总包数(不设置到全局状态) + /// + /// 文件数据 + /// 总包数 + public static int GetTotalPackets(byte[] fileData) + { + return (int)Math.Ceiling(fileData.Length / 1024.0); + } + + /// + /// 重置所有状态 + /// + public static void Reset() + { + lock (_lockObject) + { + _totalPackets = 0; + _fileData = null; + _endEOTData = null; + } + } + + /// + /// 将 UTF-8 字符串转换为 GBK 编码的字节数组 + /// + public static byte[] Utf8StringToGbkBytes(string utf8Text) + { + if (utf8Text == null) return null; + return Encoding.GetEncoding(936).GetBytes(utf8Text); + } + + /// + /// 将 UTF-8 字节数组转换为 GBK 字节数组 + /// + public static byte[] Utf8BytesToGbkBytes(byte[] utf8Bytes) + { + if (utf8Bytes == null || utf8Bytes.Length == 0) return utf8Bytes; + + // 1. UTF-8 字节 → 字符串(Unicode) + string text = Encoding.UTF8.GetString(utf8Bytes); + + // 2. 字符串 → GBK 字节 + return Encoding.GetEncoding(936).GetBytes(text); + } + #endregion + + #region 验证设备响应数据包 + + private static readonly byte[] NakSignature = { 0x6E, 0x61, 0x6B, 0x00 }; // "nak" + private static readonly byte[] AckSignature = { 0x61, 0x63, 0x6B, 0x00 }; // "ack" + private static readonly byte[] AckcSignature = { 0x61, 0x63, 0x6B, 0x63, 0x00 }; // "ackc" + /// + /// NAK响应结果 + /// + /// usb hid 返回包 + /// + public static bool IsNakResponse(List buffer) => ContainsSubsequence(buffer.ToArray(), NakSignature); + + /// + /// ACKC响应结果 + /// + /// usb hid 返回包 + /// + public static bool IsAckcResponse(List buffer) => ContainsSubsequence(buffer.ToArray(), AckcSignature); + + /// + /// ACK响应结果 + /// + /// usb hid 返回包 + /// + public static bool IsReadyForDataFrameResponse(List buffer) => ContainsSubsequence(buffer.ToArray(), AckSignature); + + private static bool ContainsSubsequence(ReadOnlySpan buffer, ReadOnlySpan pattern) + { + if (buffer.Length != 195) return false; + if (pattern.Length == 0) return false; + if (buffer.Length < pattern.Length) return false; + + for (int i = 0; i <= buffer.Length - pattern.Length; i++) + { + if (buffer.Slice(i, pattern.Length).SequenceEqual(pattern)) + return true; + } + return false; + } + #endregion + + #region 查询固件版本 + /// + /// 查询固件版本version query + /// + /// + public static byte[] VersionQuery() + { + string str = "0100FF6765742076657200300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000CC85"; + byte[] content = new byte[str.Length / 2]; + for (int i = 0; i < content.Length; i++) + { + content[i] = Convert.ToByte(str.Substring(i * 2, 2), 16); + } + return content; + } + #endregion + + + } +} diff --git a/AIK.Common/AIKProtocol/ProtocolHelper.cs b/AIK.Common/AIKProtocol/ProtocolHelper.cs new file mode 100644 index 0000000..0ede3f5 --- /dev/null +++ b/AIK.Common/AIKProtocol/ProtocolHelper.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.AIKProtocol +{ + public static class ProtocolHelper + { + public static ushort CalculateCRC16(IEnumerable data) + { + if (data == null || !data.Any()) + return 0; + + ushort crc = 0; + foreach (byte b in data) + { + crc ^= (ushort)(b << 8); + for (int i = 0; i < 8; i++) + { + if ((crc & 0x8000) != 0) + { + crc = (ushort)((crc << 1) ^ 0x1021); + } + else + { + crc = (ushort)(crc << 1); + } + } + } + return crc; + } + } +} diff --git a/AIK.Common/CommonString.cs b/AIK.Common/CommonString.cs new file mode 100644 index 0000000..5150eae --- /dev/null +++ b/AIK.Common/CommonString.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common +{ + public static class CommonString + { + public static string BinCacheDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "AIKKey", + "BinCache" + ); + public static string ImageCacheDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "AIKKey", + "ImageCache" + ); + public static string CloudCacheDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "AIKKey", + "CloudCache" + ); + } +} diff --git a/AIK.Common/Enum/FileServiceType.cs b/AIK.Common/Enum/FileServiceType.cs new file mode 100644 index 0000000..bdc5858 --- /dev/null +++ b/AIK.Common/Enum/FileServiceType.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + public enum FileServiceType + { + OBD, + BIN, + CloudData + } +} diff --git a/AIK.Common/Enum/HidConnectionState.cs b/AIK.Common/Enum/HidConnectionState.cs new file mode 100644 index 0000000..8f92b30 --- /dev/null +++ b/AIK.Common/Enum/HidConnectionState.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + public enum HidConnectionState + { + Disconnected, // 初始/断开状态 + DeviceOpened, // HID 打开成功,待发 Connect + AwaitingResponse, // 已发 Connect,等待应答 + Ready // 握手完成,可传业务数据 + } +} diff --git a/AIK.Common/Enum/HidOperationTypeEnum.cs b/AIK.Common/Enum/HidOperationTypeEnum.cs new file mode 100644 index 0000000..caedb66 --- /dev/null +++ b/AIK.Common/Enum/HidOperationTypeEnum.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + public enum HidOperationTypeEnum + { + None = 0, + DeviceUpdate, + DeviceVersion, + RemoteGeneration + } +} diff --git a/AIK.Common/Enum/MenuFunction.cs b/AIK.Common/Enum/MenuFunction.cs new file mode 100644 index 0000000..c4f713f --- /dev/null +++ b/AIK.Common/Enum/MenuFunction.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + public enum MenuFunction + { + RemoteGeneration, // 遥控生成 + RemoteCopy, // 遥控拷贝 + RemoteDetection, // 遥控检测 + ChipIdentificationCopy, // 芯片识别拷贝 + ChipGeneration, // 芯片生成 + AccessCardCopy, // 门禁卡拷贝 + KeyUnlock, // 钥匙解锁 + TirePressure, // 胎压功能 + MatchingFunction, // 匹配功能 + ChipWriteStart, // 芯片写启动 + Programmer, // 编程器 + AntiTheftData // 防盗资料 + } +} diff --git a/AIK.Common/Enum/MessageTypeEnum.cs b/AIK.Common/Enum/MessageTypeEnum.cs new file mode 100644 index 0000000..d0cad39 --- /dev/null +++ b/AIK.Common/Enum/MessageTypeEnum.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + public enum MessageTypeEnum + { + Info, + Warning, + Error, + Success + } +} diff --git a/AIK.Common/Enum/TransferPhase.cs b/AIK.Common/Enum/TransferPhase.cs new file mode 100644 index 0000000..54a0694 --- /dev/null +++ b/AIK.Common/Enum/TransferPhase.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + /// + /// USB协议流转小阶段 + /// + public enum TransferPhase + { + Idle, + Phase1_SendingHeader, // 发送头帧 + Phase2_WaitingForHeaderAck, // 等待头帧确认 + Phase3_SendingDataFrames, // 发送数据帧 + Phase4_WaitingForDataFrameAck, // 等待数据帧确认 + Phase5_WaitingForFinalDataAck, // 等待最后一个数据帧确认 + Phase6_SendingEOT, // 发送 EOT + Phase7_WaitingForNak, // 等待 NAK + Phase8_SendingFinalEOT, // 发送最终 EOT + Phase9_WaitingForAckc, // 等待 ACKC + Phase10_SendEndFrame, // 发送结束帧 + Phase11_WaitingForComplete, // 等待最终确认 + Completed + } + public enum ResponseType + { + ReadyForData, + Nak, + Ackc, + Unknown + } + /// + /// 连接流程大阶段(每个大阶段都会走完小阶段) + /// + public enum BigTransferPhase + { + None, + Phase1_Connect, // 大阶段1:设备连接(含初始化/校验) + Phase2_Generate, // 大阶段2:数据生成(遥控生成) + Phase3_Write, // 大阶段3:数据写入(将生成的数据写入设备) + } + public record HidResponseReceivedEventArgs(byte[] Data); + public record HidLogMessageEventArgs(string Message, bool IsError = false); + + public class BigPhaseChangedEventArgs : EventArgs + { + public BigTransferPhase CurrentPhase { get; } + public BigTransferPhase PreviousPhase { get; } + public int CompletedPhases { get; } + public int TotalPhases { get; } + + public BigPhaseChangedEventArgs(BigTransferPhase current, BigTransferPhase previous, int completed, int total) + { + CurrentPhase = current; + PreviousPhase = previous; + CompletedPhases = completed; + TotalPhases = total; + } + } + +} diff --git a/AIK.Common/Enum/UserStatus.cs b/AIK.Common/Enum/UserStatus.cs new file mode 100644 index 0000000..03a22cf --- /dev/null +++ b/AIK.Common/Enum/UserStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Enum +{ + public enum UserStatus + { + NotLoggedIn, // 未登录 + LoggedInOffline, // 已登录但离线 + LoggedInOnline // 已登录且在线 + } +} diff --git a/AIK.Common/Event/ConnectionStateChangedEventArgs.cs b/AIK.Common/Event/ConnectionStateChangedEventArgs.cs new file mode 100644 index 0000000..2763796 --- /dev/null +++ b/AIK.Common/Event/ConnectionStateChangedEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Event +{ + public class ConnectionStateChangedEventArgs : EventArgs + { + public bool IsConnected { get; } + public string Message { get; } + + public ConnectionStateChangedEventArgs(bool isConnected, string message) + { + IsConnected = isConnected; + Message = message; + } + } +} diff --git a/AIK.Common/Event/HidReconnectEventArgs.cs b/AIK.Common/Event/HidReconnectEventArgs.cs new file mode 100644 index 0000000..14564a8 --- /dev/null +++ b/AIK.Common/Event/HidReconnectEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Event +{ + public class HidReconnectEventArgs : EventArgs + { + public int CurrentAttempt { get; } + public int MaxAttempts { get; } + + public HidReconnectEventArgs(int currentAttempt, int maxAttempts) + { + CurrentAttempt = currentAttempt; + MaxAttempts = maxAttempts; + } + } +} diff --git a/AIK.Common/GlobalImageCache.cs b/AIK.Common/GlobalImageCache.cs new file mode 100644 index 0000000..52992bf --- /dev/null +++ b/AIK.Common/GlobalImageCache.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Common +{ + public static class GlobalImageCache + { + private static readonly ConcurrentDictionary _imgListKey = new ConcurrentDictionary(); + + public static BitmapImage GetOrAdd(string url, Func valueFactory) + { + return _imgListKey.GetOrAdd(url, valueFactory); + } + + public static bool TryGetValue(string key, out BitmapImage value) + { + return _imgListKey.TryGetValue(key, out value); + } + } +} diff --git a/AIK.Common/IconString/IconStringHelper.cs b/AIK.Common/IconString/IconStringHelper.cs new file mode 100644 index 0000000..db271da --- /dev/null +++ b/AIK.Common/IconString/IconStringHelper.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.IconString +{ + public static class IconStringHelper + { + public static string InfoIcon => "\uE616"; // 信息图标 + public static string WarningIcon => "\uE604"; + public static string ErrorIcon => "\uE603"; + public static string SuccessIcon => "\uE703"; + + } +} diff --git a/AIK.Common/IconString/MessageColors.cs b/AIK.Common/IconString/MessageColors.cs new file mode 100644 index 0000000..57e61c0 --- /dev/null +++ b/AIK.Common/IconString/MessageColors.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.IconString +{ + public static class MessageColors + { + // 建议使用十六进制或 Brush(根据 UI 框架) + public static string Info => "#1890FF"; // 蓝色 + public static string Warning => "#FAAD14"; // 橙色 + public static string Error => "#FF4D4F"; // 红色 + public static string Success => "#52C41A"; // 绿色 + } +} diff --git a/AIK.Common/IconString/MessageStyle.cs b/AIK.Common/IconString/MessageStyle.cs new file mode 100644 index 0000000..34d64aa --- /dev/null +++ b/AIK.Common/IconString/MessageStyle.cs @@ -0,0 +1,24 @@ +using AIK.Common.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.IconString +{ + public static class MessageStyle + { + public static (string Icon, string Color) GetByType(MessageTypeEnum type) + { + return type switch + { + MessageTypeEnum.Info => (IconStringHelper.InfoIcon, MessageColors.Info), + MessageTypeEnum.Warning => (IconStringHelper.WarningIcon, MessageColors.Warning), + MessageTypeEnum.Error => (IconStringHelper.ErrorIcon, MessageColors.Error), + MessageTypeEnum.Success => (IconStringHelper.SuccessIcon, MessageColors.Success), + _ => (IconStringHelper.InfoIcon, MessageColors.Info), // 默认 info + }; + } + } +} diff --git a/AIK.Common/ImageUtils.cs b/AIK.Common/ImageUtils.cs new file mode 100644 index 0000000..fdb3cab --- /dev/null +++ b/AIK.Common/ImageUtils.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Common +{ + public class ImageUtils + { + /// + /// 从文件路径加载图像(同步) + /// + public static BitmapImage LoadFromFile(string filePath) + { + var bitmap = new BitmapImage(); + using (var stream = File.OpenRead(filePath)) + { + bitmap.BeginInit(); + bitmap.StreamSource = stream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + } + return bitmap; + } + + /// + /// 从字节数组加载图像 + /// + public static BitmapImage LoadFromBytes(byte[] imageData) + { + using (var ms = new MemoryStream(imageData)) + { + return StreamToBitmapImage(ms); + } + } + + /// + /// 从任意Stream加载图像(不负责关闭流) + /// + public static BitmapImage StreamToBitmapImage(Stream stream) + { + if (stream.CanSeek) stream.Position = 0; + + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = stream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + return bitmap; + } + + public static BitmapImage StreamToBitmapImageFull(Stream stream) + { + if (stream == null) return null; + try + { + if (stream.CanSeek) stream.Position = 0; + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + memoryStream.Position = 0; + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = stream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + + return bitmap; + } + catch (Exception) + { + return null; + } + + } + } +} diff --git a/AIK.Common/Interface/IFileDialogService.cs b/AIK.Common/Interface/IFileDialogService.cs new file mode 100644 index 0000000..81064d6 --- /dev/null +++ b/AIK.Common/Interface/IFileDialogService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Interface +{ + public interface IFileDialogService + { + Task OpenFileAsync(string title = "选择文件"); + Task SelectFolderAsync(string description = "请选择文件夹"); + } +} diff --git a/AIK.Common/Interface/IModalDialogService.cs b/AIK.Common/Interface/IModalDialogService.cs new file mode 100644 index 0000000..7bb1900 --- /dev/null +++ b/AIK.Common/Interface/IModalDialogService.cs @@ -0,0 +1,38 @@ +using AIK.Common.Enum; +using AIK.Common.IconString; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Common.Interface +{ + public interface IModalDialogService + { + Task ShowProgressAsync( + Func, CancellationToken, Task<(bool, string)>> work, + string title = "请稍候", + string message = "正在烧录...", + bool canCancel = true); + + Task ShowDialogAsync( + string title = "请稍候", + string message = "正在处理...", + MessageTypeEnum typeEnum = MessageTypeEnum.Info, + bool canCancel = true, bool isAutoClose = true, int closeTime = 3000); + + Task ShowTipsDialogAsync( + string message = "已完成", + MessageTypeEnum typeEnum = MessageTypeEnum.Info, + bool canCancel = true, bool isAutoClose = true, int closeTime = 3000); + + Task ShowQRCodeDialogAsync( + IServiceProvider serviceProvider, + string title = "请稍候", + string message = "正在处理...", + MessageTypeEnum typeEnum = MessageTypeEnum.Info, + bool canCancel = true, bool isAutoClose = true, int closeTime = 3000); + } +} diff --git a/AIK.Common/Interface/IQRCodeService.cs b/AIK.Common/Interface/IQRCodeService.cs new file mode 100644 index 0000000..e39536d --- /dev/null +++ b/AIK.Common/Interface/IQRCodeService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Interface +{ + public interface IQRCodeService + { + byte[] GenerateQRCodeWithLogo(string text, string logoPath, int pixelsPerModule = 10); + } +} diff --git a/AIK.Common/Interface/ISettingsService.cs b/AIK.Common/Interface/ISettingsService.cs new file mode 100644 index 0000000..ad8d560 --- /dev/null +++ b/AIK.Common/Interface/ISettingsService.cs @@ -0,0 +1,15 @@ +using AIK.Common.Language; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Interface +{ + public interface ISettingsService + { + UserPreferences Load(); + void Save(UserPreferences settings); + } +} diff --git a/AIK.Common/Interface/IUpdateService.cs b/AIK.Common/Interface/IUpdateService.cs new file mode 100644 index 0000000..8e80335 --- /dev/null +++ b/AIK.Common/Interface/IUpdateService.cs @@ -0,0 +1,38 @@ +using AIK.Common.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Interface +{ + public interface IUpdateService + { + #region 原生更新-界面无自定义 + /// + /// 手动检查更新 + /// + /// 更新结果 + Task CheckForUpdateAsync(); + + /// + /// 原版更新(无自定义界面) + /// + /// + public Task StartUpdateProcess(); + #endregion + #region 新自定义更新-界面可自定义 + /// + /// 新自定义更新(检查更新) + /// + /// + public Task GetUpdateInfoAsync(); + /// + /// 自定义更新 + /// + /// + public Task CheckForUpdate(); + #endregion + } +} diff --git a/AIK.Common/IsExternalInit.cs b/AIK.Common/IsExternalInit.cs new file mode 100644 index 0000000..a869ed6 --- /dev/null +++ b/AIK.Common/IsExternalInit.cs @@ -0,0 +1,6 @@ +// +// Polyfill for record types with init-only setters (required by .NET Framework 4.8) +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/AIK.Common/Language/AiKLanguage.cs b/AIK.Common/Language/AiKLanguage.cs new file mode 100644 index 0000000..603da2d --- /dev/null +++ b/AIK.Common/Language/AiKLanguage.cs @@ -0,0 +1,1951 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Input; +using static System.Net.Mime.MediaTypeNames; + +namespace AIK.Common.Language +{ + /// + /// 多语言 + /// + public class AiKLanguage + { + public static Dictionary LanguageKey = null; + public static void GetLanguage(string language) + { + switch (language) + { + case "zh": // 中文 + LanguageKey = zhlanguage(); + break; + case "en": // 英文 + LanguageKey = enlanguage(); + break; + case "pt": // 葡萄牙(葡萄牙) + LanguageKey = ptlanguage(); + break; + case "pt-BR": // 葡萄牙语(巴西) + LanguageKey = ptBRlanguage(); + break; + case "es": // 西班牙语 + LanguageKey = eslanguage(); + break; + case "id": // 印尼语 + LanguageKey = idlanguage(); + break; + case "in": // 印度 + LanguageKey = inlanguage(); + break; + case "hi": // 印地语 + LanguageKey = hilanguage(); + break; + case "fr": // 法语 + LanguageKey = frlanguage(); + break; + case "fa": // 波斯语 + LanguageKey = falanguage(); + break; + case "he": // 希伯来语 + LanguageKey = helanguage(); + break; + case "it": // 意大利语 + LanguageKey = itlanguage(); + break; + case "ko": // 朝鲜语(韩语) + LanguageKey = kolanguage(); + break; + case "pl": // 波兰语 + LanguageKey = pllanguage(); + break; + case "ru": // 俄语 + LanguageKey = rulanguage(); + break; + case "th": // 泰语 + LanguageKey = thlanguage(); + break; + case "tr": // 土耳其语 + LanguageKey = trlanguage(); + break; + case "vi": // 越南语 + LanguageKey = vilanguage(); + break; + case "de": // 德语 + LanguageKey = delanguage(); + break; + case "uk": // 乌克兰语 + LanguageKey = uklanguage(); + break; + case "ms": // 马来语 + LanguageKey = mslanguage(); + break; + default: // 默认中文 + LanguageKey = zhlanguage(); + break; + } + } + private static Dictionary zhlanguage() + { + #region 中文 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "主页"); + dictionary.Add("Settings", "系统配置"); + dictionary.Add("RemoteGeneration", "遥控生成"); + dictionary.Add("RemoteCopy", "遥控拷贝"); + dictionary.Add("RemoteControlDetector", "遥控检测"); + dictionary.Add("ChipIdentificationCopy", "芯片识别拷贝"); + dictionary.Add("ChipGeneration", "芯片生成"); + dictionary.Add("AccessCardCopy", "门禁卡拷贝"); + dictionary.Add("KeyUnlock", "钥匙解锁"); + dictionary.Add("TirePressure", "胎压功能"); + dictionary.Add("MatchingFunction", "匹配功能"); + dictionary.Add("ChipWriteStart", "芯片写启动"); + dictionary.Add("Programmer", "编程器"); + dictionary.Add("AntiTheftData", "防盗资料"); + dictionary.Add("UpdateCenter", "更新中心"); + + dictionary.Add("ContactWechatCopy", "微信号已复制"); + dictionary.Add("ContactEmailCopy", "邮箱号已复制"); + + dictionary.Add("Tips", "提示"); + dictionary.Add("Connected", "已连接"); + dictionary.Add("Disconnected", "未连接"); + dictionary.Add("DeviceDisconnected", "设备未连接"); + dictionary.Add("K3ToolConnectTips", "请先连接K3Tool设备再进行更新"); + dictionary.Add("FileNotFound", "文件不存在"); + + dictionary.Add("UpToDate", "已是最新版本"); + dictionary.Add("PleaseNote", "请知悉"); + + dictionary.Add("PleasePress", "请按"); + dictionary.Add("Button", "键"); + + dictionary.Add("NoKeyData", "暂无钥匙数据"); + dictionary.Add("Lock", "锁车"); + dictionary.Add("Unlock", "开锁"); + dictionary.Add("Trunk", "后备箱"); + dictionary.Add("Panic", "寻车"); + + dictionary.Add("BurnTips", "烧录文件不存在,请选择需要烧录的钥匙型号"); + dictionary.Add("BurnCSubKeyMobileAppTips", "请使用移动端App烧录C子机"); + dictionary.Add("BurnCFSubKeyMobileAppTips", "请使用移动端App烧录CF子机"); + dictionary.Add("BurnSuccess", "烧录成功"); + dictionary.Add("BurnError", "烧录失败"); + dictionary.Add("NotMoreTips", "没有更多数据了~"); + dictionary.Add("CloudData", "云数据"); + dictionary.Add("CloudDataSyncCompleted", "同步完成"); + dictionary.Add("CloudDataSyncFailed", "同步失败"); + dictionary.Add("CloudDataDownloadFailed", "下载失败"); + dictionary.Add("CloudDataDeviceNotFound", "未读取到K3Tool存储设备"); + dictionary.Add("CloudDataInitCompleted", "初始化K3Tool完成"); + dictionary.Add("HidSendPacketError", "发送数据包发生错误"); + dictionary.Add("DialogPleaseWait", "请稍候"); + dictionary.Add("DialogProcessing", "正在处理..."); + dictionary.Add("DialogCancelled", "已取消"); + dictionary.Add("DialogErrorPrefix", "错误"); + dictionary.Add("DialogElapsedPrefix", "耗时"); + dictionary.Add("DialogBurningTitle", "正在烧录"); + dictionary.Add("DialogBurningMessage", "正在烧录..."); + dictionary.Add("MembershipLoginRequired", "请先登录后再开通会员功能"); + dictionary.Add("MembershipOpening", "正在开通会员功能..."); + dictionary.Add("MembershipOpenSuccess", "开通成功"); + dictionary.Add("MembershipOpenFailed", "开通失败,请稍后重试"); + dictionary.Add("MembershipLoadFailedFallback", "加载会员功能失败,已显示默认功能"); + dictionary.Add("MembershipOpenWindowTitle", "会员开通"); + dictionary.Add("MembershipFeeStandard", "收费标准:"); + dictionary.Add("MembershipIntegralPerYear", "积分/年"); + dictionary.Add("MembershipOpenButton", "开通"); + dictionary.Add("MembershipModuleDataWriteStart", "数据写启动"); + dictionary.Add("MembershipModuleK3GeniePlus", "K3精灵+"); + dictionary.Add("MembershipModuleAiIntelligent", "AI智能功能"); + dictionary.Add("RechargeMinAmountInvalid", "充值金额不能小于0.01"); + dictionary.Add("RechargeLoginRequired", "请先登录后再充值"); + dictionary.Add("RechargeCreatingOrder", "正在创建支付订单..."); + dictionary.Add("RechargePcQrCodeMissing", "后端未返回PC扫码支付二维码"); + dictionary.Add("RechargeWechatScanToPay", "请使用微信扫码支付"); + dictionary.Add("RechargePaypalParametersMissing", "后端未返回PayPal支付参数"); + dictionary.Add("RechargeOpeningPaypal", "正在打开PayPal支付页面..."); + dictionary.Add("RechargeCreateOrderFailed", "创建支付订单失败,请稍后重试"); + + + + + return dictionary; + + #endregion + } + private static Dictionary enlanguage() + { + #region 英文 + Dictionary dictionary = new Dictionary(); + dictionary.Add("HomePage", "HomePage"); + dictionary.Add("Settings", "AppSettings"); + dictionary.Add("RemoteGeneration", "Remote"); + dictionary.Add("RemoteCopy", "RemoteCopy"); + dictionary.Add("RemoteControlDetector", "RemoteControlDetector"); + dictionary.Add("ChipIdentificationCopy", "ChipIdentificationCopy"); + dictionary.Add("ChipGeneration", "ChipGeneration"); + dictionary.Add("AccessCardCopy", "AccessCardCopy"); + dictionary.Add("KeyUnlock", "KeyUnlock"); + dictionary.Add("TirePressure", "TirePressure"); + dictionary.Add("MatchingFunction", "MatchingFunction"); + dictionary.Add("ChipWriteStart", "ChipWriteStart"); + dictionary.Add("Programmer", "Programmer"); + dictionary.Add("AntiTheftData", "AntiTheftData"); + dictionary.Add("UpdateCenter", "UpdateCenter"); + + dictionary.Add("ContactWechatCopy", "Contact us by wechat Copy Success"); + dictionary.Add("ContactEmailCopy", "Contact us by email Copy Success"); + + dictionary.Add("Connected", "Connected"); + dictionary.Add("Disconnected", "Disconnected"); + dictionary.Add("DeviceDisconnected", "Device Disconnected"); + dictionary.Add("K3ToolConnectTips", "Please connect the K3Tool device first before performing the update"); + dictionary.Add("FileNotFound", "File not found"); + + dictionary.Add("UpToDate", "Up to date"); + dictionary.Add("PleaseNote", "Please Note"); + dictionary.Add("PleasePress", "Please press"); + dictionary.Add("Button", "button"); + dictionary.Add("NoKeyData", "No key data available"); + dictionary.Add("Lock", "Lock"); + dictionary.Add("Unlock", "Unlock"); + dictionary.Add("Trunk", "Trunk"); + dictionary.Add("Panic", "Panic"); + dictionary.Add("BurnTips", "Burning file does not exist. Please select the key model to burn."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Please use the mobile app to program the C sub-key."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Please use the mobile app to program the CF sub-key."); + dictionary.Add("BurnSuccess", "Burning successful"); + dictionary.Add("BurnError", "Burning failed"); + dictionary.Add("NotMoreTips", "No more data~"); + dictionary.Add("CloudData", "CloudData"); + dictionary.Add("CloudDataSyncCompleted", "Sync completed"); + dictionary.Add("CloudDataSyncFailed", "Sync failed"); + dictionary.Add("CloudDataDownloadFailed", "Download failed"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool storage device not detected"); + dictionary.Add("CloudDataInitCompleted", "K3Tool initialization completed"); + dictionary.Add("HidSendPacketError", "Error sending data packet"); + dictionary.Add("DialogPleaseWait", "Please wait"); + dictionary.Add("DialogProcessing", "Processing..."); + dictionary.Add("DialogCancelled", "Cancelled"); + dictionary.Add("DialogErrorPrefix", "Error"); + dictionary.Add("DialogElapsedPrefix", "Elapsed"); + dictionary.Add("DialogBurningTitle", "Burning"); + dictionary.Add("DialogBurningMessage", "Burning..."); + dictionary.Add("MembershipLoginRequired", "Please log in before opening membership features"); + dictionary.Add("MembershipOpening", "Opening membership feature..."); + dictionary.Add("MembershipOpenSuccess", " opened successfully"); + dictionary.Add("MembershipOpenFailed", "Opening failed, please try again later"); + dictionary.Add("MembershipLoadFailedFallback", "Failed to load membership features, showing default items"); + dictionary.Add("MembershipOpenWindowTitle", "Membership"); + dictionary.Add("MembershipFeeStandard", "Price:"); + dictionary.Add("MembershipIntegralPerYear", " points/year"); + dictionary.Add("MembershipOpenButton", "Open"); + dictionary.Add("MembershipModuleDataWriteStart", "Data Write Start"); + dictionary.Add("MembershipModuleK3GeniePlus", "K3 Genie+"); + dictionary.Add("MembershipModuleAiIntelligent", "AI Intelligent Feature"); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary ptlanguage() + { + #region 葡萄牙 + + Dictionary dictionary = new Dictionary(); + dictionary.Add("HomePage", "Página Inicial"); + dictionary.Add("Settings", "Configurações do Sistema"); + dictionary.Add("RemoteGeneration", "Geração de Controle Remoto"); + dictionary.Add("RemoteCopy", "Cópia de Controle Remoto"); + dictionary.Add("RemoteControlDetector", "Detector de Controle Remoto"); + dictionary.Add("ChipIdentificationCopy", "Identificação e Cópia de Chip"); + dictionary.Add("ChipGeneration", "Geração de Chip"); + dictionary.Add("AccessCardCopy", "Cópia de Cartão de Acesso"); + dictionary.Add("KeyUnlock", "Desbloqueio de Chave"); + dictionary.Add("TirePressure", "Função de Pressão dos Pneus"); + dictionary.Add("MatchingFunction", "Função de Combinação"); + dictionary.Add("ChipWriteStart", "Iniciar Gravação de Chip"); + dictionary.Add("Programmer", "Programador"); + dictionary.Add("AntiTheftData", "Dados de Antifurto"); + dictionary.Add("UpdateCenter", "Centro de Atualização"); + + dictionary.Add("ContactWechatCopy", "Número do WeChat copiado"); + dictionary.Add("ContactEmailCopy", "Endereço de e-mail copiado"); + + dictionary.Add("Tips", "Dicas"); + dictionary.Add("Connected", "Conectado"); + dictionary.Add("Disconnected", "Desconectado"); + dictionary.Add("DeviceDisconnected", "Dispositivo não conectado"); + dictionary.Add("K3ToolConnectTips", "Conecte o dispositivo K3Tool antes de realizar a atualização"); + dictionary.Add("FileNotFound", "Arquivo não encontrado"); + + dictionary.Add("UpToDate", "Já é a versão mais recente"); + dictionary.Add("PleaseNote", "Atenção"); + dictionary.Add("PleasePress", "Por favor, prima"); + dictionary.Add("Button", "botão"); + dictionary.Add("NoKeyData", "Sem dados da chave disponíveis"); + dictionary.Add("Lock", "Trancar"); + dictionary.Add("Unlock", "Destrancar"); + dictionary.Add("Trunk", "Mala"); + dictionary.Add("Panic", "Panic"); + dictionary.Add("BurnTips", "O ficheiro de gravação não existe. Por favor, selecione o modelo de chave a gravar."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Utilize a app móvel para gravar a subchave C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Utilize a app móvel para gravar a subchave CF."); + dictionary.Add("BurnSuccess", "Gravação concluída com sucesso"); + dictionary.Add("BurnError", "Gravação falhou"); + dictionary.Add("NotMoreTips", "Não há mais dados~"); + dictionary.Add("CloudData", "Dados na Nuvem"); + dictionary.Add("CloudDataSyncCompleted", "Sincronização concluída"); + dictionary.Add("CloudDataSyncFailed", "Falha na sincronização"); + dictionary.Add("CloudDataDownloadFailed", "Falha no download"); + dictionary.Add("CloudDataDeviceNotFound", "Dispositivo de armazenamento K3Tool não detectado"); + dictionary.Add("CloudDataInitCompleted", "Inicialização do K3Tool concluída"); + dictionary.Add("HidSendPacketError", "Erro ao enviar pacote de dados"); + dictionary.Add("DialogPleaseWait", "Aguarde"); + dictionary.Add("DialogProcessing", "Processando..."); + dictionary.Add("DialogCancelled", "Cancelado"); + dictionary.Add("DialogErrorPrefix", "Erro"); + dictionary.Add("DialogElapsedPrefix", "Tempo"); + dictionary.Add("DialogBurningTitle", "Gravação"); + dictionary.Add("DialogBurningMessage", "Gravando..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + + } + private static Dictionary ptBRlanguage() + { + #region 葡萄牙语(巴西) + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Início"); + dictionary.Add("Settings", "Configurações do sistema"); + dictionary.Add("RemoteGeneration", "Geração de controle remoto"); + dictionary.Add("RemoteCopy", "Cópia de controle remoto"); + dictionary.Add("RemoteControlDetector", "Detector de controles"); + dictionary.Add("ChipIdentificationCopy", "Cópia de identificação do chip"); + dictionary.Add("ChipGeneration", "Geração de chip"); + dictionary.Add("AccessCardCopy", "Cópia de cartão de acesso"); + dictionary.Add("KeyUnlock", "Destravamento de chave"); + dictionary.Add("TirePressure", "Funções de pressão dos pneus"); + dictionary.Add("MatchingFunction", "Funções de emparelhamento"); + dictionary.Add("ChipWriteStart", "Início da gravação do chip"); + dictionary.Add("Programmer", "Programador"); + dictionary.Add("AntiTheftData", "Dados antifurto"); + dictionary.Add("UpdateCenter", "Central de atualizações"); + + dictionary.Add("ContactWechatCopy", "ID do WeChat copiado"); + dictionary.Add("ContactEmailCopy", "Endereço de e-mail copiado"); + + dictionary.Add("Tips", "Aviso"); + dictionary.Add("Connected", "Conectado"); + dictionary.Add("Disconnected", "Desconectado"); + dictionary.Add("DeviceDisconnected", "Dispositivo não conectado"); + dictionary.Add("K3ToolConnectTips", "Conecte o dispositivo K3Tool antes de atualizar"); + dictionary.Add("FileNotFound", "Arquivo não encontrado"); + + dictionary.Add("UpToDate", "Já está atualizado"); + dictionary.Add("PleaseNote", "Por favor, observe"); + dictionary.Add("PleasePress", "Por favor, pressione"); + dictionary.Add("Button", "botão"); + dictionary.Add("NoKeyData", "Sem dados da chave disponíveis"); + dictionary.Add("Lock", "Travar"); + dictionary.Add("Unlock", "Destravar"); + dictionary.Add("Trunk", "Porta-malas"); + dictionary.Add("Panic", "Pânico"); + dictionary.Add("BurnTips", "O arquivo de gravação não existe. Por favor, selecione o modelo de chave para gravar."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Use o app móvel para gravar a subchave C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Use o app móvel para gravar a subchave CF."); + dictionary.Add("BurnSuccess", "Gravação bem-sucedida"); + dictionary.Add("BurnError", "Gravação falhou"); + dictionary.Add("NotMoreTips", "Não há mais dados~"); + dictionary.Add("CloudData", "Dados na Nuvem"); + dictionary.Add("CloudDataSyncCompleted", "Sincronização concluída"); + dictionary.Add("CloudDataSyncFailed", "Falha na sincronização"); + dictionary.Add("CloudDataDownloadFailed", "Falha no download"); + dictionary.Add("CloudDataDeviceNotFound", "Dispositivo de armazenamento K3Tool não detectado"); + dictionary.Add("CloudDataInitCompleted", "Inicialização do K3Tool concluída"); + dictionary.Add("HidSendPacketError", "Erro ao enviar pacote de dados"); + dictionary.Add("DialogPleaseWait", "Aguarde"); + dictionary.Add("DialogProcessing", "Processando..."); + dictionary.Add("DialogCancelled", "Cancelado"); + dictionary.Add("DialogErrorPrefix", "Erro"); + dictionary.Add("DialogElapsedPrefix", "Tempo"); + dictionary.Add("DialogBurningTitle", "Gravação"); + dictionary.Add("DialogBurningMessage", "Gravando..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary eslanguage() + { + #region 西班牙 + Dictionary dictionary = new Dictionary(); + dictionary.Add("HomePage", "Página Principal"); + dictionary.Add("Settings", "Configuración del Sistema"); + dictionary.Add("RemoteGeneration", "Generación de Mando a Distancia"); + dictionary.Add("RemoteCopy", "Copia de Mando a Distancia"); + dictionary.Add("RemoteControlDetector", "Detector de Mando a Distancia"); + dictionary.Add("ChipIdentificationCopy", "Identificación y Copia de Chip"); + dictionary.Add("ChipGeneration", "Generación de Chip"); + dictionary.Add("AccessCardCopy", "Copia de Tarjeta de Acceso"); + dictionary.Add("KeyUnlock", "Desbloqueo de Llave"); + dictionary.Add("TirePressure", "Función de Presión de Neumáticos"); + dictionary.Add("MatchingFunction", "Función de Coincidencia"); + dictionary.Add("ChipWriteStart", "Iniciar Escritura de Chip"); + dictionary.Add("Programmer", "Programador"); + dictionary.Add("AntiTheftData", "Datos Antirrobo"); + dictionary.Add("UpdateCenter", "Centro de Actualización"); + + dictionary.Add("ContactWechatCopy", "Número de WeChat copiado"); + dictionary.Add("ContactEmailCopy", "Dirección de correo electrónico copiada"); + + dictionary.Add("Tips", "Consejos"); + dictionary.Add("Connected", "Conectado"); + dictionary.Add("Disconnected", "Desconectado"); + dictionary.Add("DeviceDisconnected", "Dispositivo no conectado"); + dictionary.Add("K3ToolConnectTips", "Conecte el dispositivo K3Tool antes de realizar la actualización"); + dictionary.Add("FileNotFound", "Archivo no encontrado"); + + dictionary.Add("UpToDate", "Ya es la última versión"); + dictionary.Add("PleaseNote", "Nota Importante"); + dictionary.Add("PleasePress", "Por favor, presione"); + dictionary.Add("Button", "botón"); + dictionary.Add("NoKeyData", "Sin datos de la llave disponibles"); + dictionary.Add("Lock", "Bloquear"); + dictionary.Add("Unlock", "Desbloquear"); + dictionary.Add("Trunk", "Maletero"); + dictionary.Add("Panic", "Pánico"); + dictionary.Add("BurnTips", "El archivo de grabación no existe. Por favor, seleccione el modelo de llave a grabar."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Use la app móvil para grabar la subllave C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Use la app móvil para grabar la subllave CF."); + dictionary.Add("BurnSuccess", "Grabación exitosa"); + dictionary.Add("BurnError", "Grabación fallida"); + dictionary.Add("NotMoreTips", "No hay más datos~"); + dictionary.Add("CloudData", "Datos en la Nube"); + dictionary.Add("CloudDataSyncCompleted", "Sincronización completada"); + dictionary.Add("CloudDataSyncFailed", "Sincronización fallida"); + dictionary.Add("CloudDataDownloadFailed", "Descarga fallida"); + dictionary.Add("CloudDataDeviceNotFound", "No se detectó el dispositivo de almacenamiento K3Tool"); + dictionary.Add("CloudDataInitCompleted", "Inicialización de K3Tool completada"); + dictionary.Add("HidSendPacketError", "Error al enviar el paquete de datos"); + dictionary.Add("DialogPleaseWait", "Espere por favor"); + dictionary.Add("DialogProcessing", "Procesando..."); + dictionary.Add("DialogCancelled", "Cancelado"); + dictionary.Add("DialogErrorPrefix", "Error"); + dictionary.Add("DialogElapsedPrefix", "Tiempo"); + dictionary.Add("DialogBurningTitle", "Grabando"); + dictionary.Add("DialogBurningMessage", "Grabando..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary inlanguage() + { + #region 印度 + Dictionary dictionary = new Dictionary(); + dictionary.Add("HomePage", "होमपेज"); + dictionary.Add("Settings", "सिस्टम कॉन्फ़िगरेशन"); + dictionary.Add("RemoteGeneration", "रिमोट जनरेशन"); + dictionary.Add("RemoteCopy", "रिमोट कॉपी"); + dictionary.Add("RemoteControlDetector", "रिमोट डिटेक्टर"); + dictionary.Add("ChipIdentificationCopy", "चिप पहचान कॉपी"); + dictionary.Add("ChipGeneration", "चिप जनरेशन"); + dictionary.Add("AccessCardCopy", "एक्सेस कार्ड कॉपी"); + dictionary.Add("KeyUnlock", "कुंजी अनलॉक"); + dictionary.Add("TirePressure", "टायर प्रेशर फ़ंक्शन"); + dictionary.Add("MatchingFunction", "मैचिंग फ़ंक्शन"); + dictionary.Add("ChipWriteStart", "चिप लेखन प्रारंभ"); + dictionary.Add("Programmer", "प्रोग्रामर"); + dictionary.Add("AntiTheftData", "चोरी रोधी डेटा"); + dictionary.Add("UpdateCenter", "अपडेट सेंटर"); + + dictionary.Add("ContactWechatCopy", "वीचैट आईडी कॉपी की गई"); + dictionary.Add("ContactEmailCopy", "ईमेल पता कॉपी किया गया"); + + dictionary.Add("Tips", "सुझाव"); + dictionary.Add("Connected", "जुड़ा हुआ"); + dictionary.Add("Disconnected", "डिस्कनेक्टेड"); + dictionary.Add("DeviceDisconnected", "उपकरण डिस्कनेक्टेड है"); + dictionary.Add("K3ToolConnectTips", "अपडेट करने से पहले कृपया K3Tool उपकरण कनेक्ट करें"); + dictionary.Add("FileNotFound", "फ़ाइल नहीं मिली"); + + dictionary.Add("UpToDate", "यह पहले से ही नवीनतम संस्करण है"); + dictionary.Add("PleaseNote", "कृपया ध्यान दें"); + dictionary.Add("PleasePress", "कृपया दबाएं"); + dictionary.Add("Button", "बटन"); + dictionary.Add("NoKeyData", "कुंजी डेटा उपलब्ध नहीं है"); + dictionary.Add("Lock", "ताला लगाएं"); + dictionary.Add("Unlock", "ताला खोलें"); + dictionary.Add("Trunk", "ट्रंक"); + dictionary.Add("Panic", "पैनिक"); + dictionary.Add("BurnTips", "बर्निंग फ़ाइल मौजूद नहीं है। कृपया बर्न करने के लिए कुंजी मॉडल चुनें।"); + dictionary.Add("BurnCSubKeyMobileAppTips", "C सब-की को बर्न करने के लिए मोबाइल ऐप का उपयोग करें।"); + dictionary.Add("BurnCFSubKeyMobileAppTips", "CF सब-की को बर्न करने के लिए मोबाइल ऐप का उपयोग करें।"); + dictionary.Add("BurnSuccess", "बर्निंग सफल"); + dictionary.Add("BurnError", "बर्निंग विफल"); + dictionary.Add("NotMoreTips", "कोई अन्य डेटा नहीं~"); + dictionary.Add("CloudData", "क्लाउड डेटा"); + dictionary.Add("CloudDataSyncCompleted", "सिंक पूर्ण"); + dictionary.Add("CloudDataSyncFailed", "सिंक विफल"); + dictionary.Add("CloudDataDownloadFailed", "डाउनलोड विफल"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool स्टोरेज डिवाइस नहीं मिला"); + dictionary.Add("CloudDataInitCompleted", "K3Tool प्रारंभिककरण पूर्ण"); + dictionary.Add("HidSendPacketError", "डेटा पैकेट भेजने में त्रुटि"); + dictionary.Add("DialogPleaseWait", "कृपया प्रतीक्षा करें"); + dictionary.Add("DialogProcessing", "प्रसंस्करण हो रहा है..."); + dictionary.Add("DialogCancelled", "रद्द किया गया"); + dictionary.Add("DialogErrorPrefix", "त्रुटि"); + dictionary.Add("DialogElapsedPrefix", "समय"); + dictionary.Add("DialogBurningTitle", "बर्निंग"); + dictionary.Add("DialogBurningMessage", "बर्निंग..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary hilanguage() + { + #region 印地语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "होम पेज"); + dictionary.Add("Settings", "सिस्टम सेटिंग्स"); + dictionary.Add("RemoteGeneration", "रिमोट जनरेशन"); + dictionary.Add("RemoteCopy", "रिमोट कॉपी"); + dictionary.Add("RemoteControlDetector", "रिमोट कंट्रोल डिटेक्टर"); + dictionary.Add("ChipIdentificationCopy", "चिप पहचान कॉपी"); + dictionary.Add("ChipGeneration", "चिप जनरेशन"); + dictionary.Add("AccessCardCopy", "एक्सेस कार्ड कॉपी"); + dictionary.Add("KeyUnlock", "कुंजी अनलॉक"); + dictionary.Add("TirePressure", "टायर प्रेशर फ़ंक्शन"); + dictionary.Add("MatchingFunction", "मैचिंग फ़ंक्शन"); + dictionary.Add("ChipWriteStart", "चिप लेखन प्रारंभ करें"); + dictionary.Add("Programmer", "प्रोग्रामर"); + dictionary.Add("AntiTheftData", "चोरी रोधी डेटा"); + dictionary.Add("UpdateCenter", "अपडेट सेंटर"); + + dictionary.Add("ContactWechatCopy", "वीचैट आईडी कॉपी किया गया"); + dictionary.Add("ContactEmailCopy", "ईमेल पता कॉपी किया गया"); + + dictionary.Add("Tips", "सुझाव"); + dictionary.Add("Connected", "जुड़ा हुआ"); + dictionary.Add("Disconnected", "डिस्कनेक्ट हो गया"); + dictionary.Add("DeviceDisconnected", "डिवाइस कनेक्ट नहीं है"); + dictionary.Add("K3ToolConnectTips", "कृपया अपडेट करने से पहले K3Tool डिवाइस कनेक्ट करें"); + dictionary.Add("FileNotFound", "फ़ाइल नहीं मिली"); + + dictionary.Add("UpToDate", "पहले से ही अप-टू-डेट"); + dictionary.Add("PleaseNote", "कृपया ध्यान दें"); + dictionary.Add("PleasePress", "कृपया दबाएं"); + dictionary.Add("Button", "बटन"); + dictionary.Add("NoKeyData", "कुंजी डेटा उपलब्ध नहीं है"); + dictionary.Add("Lock", "ताला लगाएं"); + dictionary.Add("Unlock", "ताला खोलें"); + dictionary.Add("Trunk", "ट्रंक"); + dictionary.Add("Panic", "पैनिक"); + dictionary.Add("BurnTips", "बर्निंग फ़ाइल मौजूद नहीं है। कृपया बर्न करने के लिए कुंजी मॉडल चुनें।"); + dictionary.Add("BurnCSubKeyMobileAppTips", "C सब-की को बर्न करने के लिए मोबाइल ऐप का उपयोग करें।"); + dictionary.Add("BurnCFSubKeyMobileAppTips", "CF सब-की को बर्न करने के लिए मोबाइल ऐप का उपयोग करें।"); + dictionary.Add("BurnSuccess", "बर्निंग सफल"); + dictionary.Add("BurnError", "बर्निंग विफल"); + dictionary.Add("NotMoreTips", "कोई अन्य डेटा नहीं~"); + dictionary.Add("CloudData", "क्लाउड डेटा"); + dictionary.Add("CloudDataSyncCompleted", "सिंक पूर्ण"); + dictionary.Add("CloudDataSyncFailed", "सिंक विफल"); + dictionary.Add("CloudDataDownloadFailed", "डाउनलोड विफल"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool स्टोरेज डिवाइस नहीं मिला"); + dictionary.Add("CloudDataInitCompleted", "K3Tool प्रारंभिककरण पूर्ण"); + dictionary.Add("HidSendPacketError", "डेटा पैकेट भेजने में त्रुटि"); + dictionary.Add("DialogPleaseWait", "कृपया प्रतीक्षा करें"); + dictionary.Add("DialogProcessing", "प्रसंस्करण हो रहा है..."); + dictionary.Add("DialogCancelled", "रद्द किया गया"); + dictionary.Add("DialogErrorPrefix", "त्रुटि"); + dictionary.Add("DialogElapsedPrefix", "समय"); + dictionary.Add("DialogBurningTitle", "बर्निंग"); + dictionary.Add("DialogBurningMessage", "बर्निंग..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary frlanguage() + { + #region 法语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Accueil"); + dictionary.Add("Settings", "Paramètres système"); + dictionary.Add("RemoteGeneration", "Génération de télécommande"); + dictionary.Add("RemoteCopy", "Copie de télécommande"); + dictionary.Add("RemoteControlDetector", "Détecteur de télécommandes"); + dictionary.Add("ChipIdentificationCopy", "Copie d'identification de puce"); + dictionary.Add("ChipGeneration", "Génération de puce"); + dictionary.Add("AccessCardCopy", "Copie de carte d'accès"); + dictionary.Add("KeyUnlock", "Déverrouillage de clé"); + dictionary.Add("TirePressure", "Fonctions pression pneus"); + dictionary.Add("MatchingFunction", "Fonctions d'appairage"); + dictionary.Add("ChipWriteStart", "Démarrer l'écriture puce"); + dictionary.Add("Programmer", "Programmeur"); + dictionary.Add("AntiTheftData", "Données antivol"); + dictionary.Add("UpdateCenter", "Centre de mise à jour"); + + dictionary.Add("ContactWechatCopy", "ID WeChat copié"); + dictionary.Add("ContactEmailCopy", "Adresse e-mail copiée"); + + dictionary.Add("Tips", "Astuce"); + dictionary.Add("Connected", "Connecté"); + dictionary.Add("Disconnected", "Déconnecté"); + dictionary.Add("DeviceDisconnected", "Périphérique non connecté"); + dictionary.Add("K3ToolConnectTips", "Veuillez connecter le périphérique K3Tool avant la mise à jour"); + dictionary.Add("FileNotFound", "Fichier introuvable"); + + dictionary.Add("UpToDate", "Déjà à jour"); + dictionary.Add("PleaseNote", "Veuillez noter"); + dictionary.Add("PleasePress", "Veuillez appuyer sur"); + dictionary.Add("Button", "le bouton"); + dictionary.Add("NoKeyData", "Aucune donnée de clé disponible"); + dictionary.Add("Lock", "Verrouiller"); + dictionary.Add("Unlock", "Déverrouiller"); + dictionary.Add("Trunk", "Coffre"); + dictionary.Add("Panic", "Panic"); + dictionary.Add("BurnTips", "Le fichier de programmation n'existe pas. Veuillez sélectionner le modèle de clé à programmer."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Veuillez utiliser l'application mobile pour programmer la sous-clé C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Veuillez utiliser l'application mobile pour programmer la sous-clé CF."); + dictionary.Add("BurnSuccess", "Programmation réussie"); + dictionary.Add("BurnError", "Échec de la programmation"); + dictionary.Add("NotMoreTips", "Plus de données~"); + dictionary.Add("CloudData", "Données Cloud"); + dictionary.Add("CloudDataSyncCompleted", "Synchronisation terminée"); + dictionary.Add("CloudDataSyncFailed", "Échec de la synchronisation"); + dictionary.Add("CloudDataDownloadFailed", "Échec du téléchargement"); + dictionary.Add("CloudDataDeviceNotFound", "Périphérique de stockage K3Tool non détecté"); + dictionary.Add("CloudDataInitCompleted", "Initialisation de K3Tool terminée"); + dictionary.Add("HidSendPacketError", "Erreur lors de l'envoi du paquet de données"); + dictionary.Add("DialogPleaseWait", "Veuillez patienter"); + dictionary.Add("DialogProcessing", "Traitement en cours..."); + dictionary.Add("DialogCancelled", "Annulé"); + dictionary.Add("DialogErrorPrefix", "Erreur"); + dictionary.Add("DialogElapsedPrefix", "Temps"); + dictionary.Add("DialogBurningTitle", "Gravure"); + dictionary.Add("DialogBurningMessage", "Gravure en cours..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary falanguage() + { + #region 波斯语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "صفحه اصلی"); + dictionary.Add("Settings", "تنظیمات سیستم"); + dictionary.Add("RemoteGeneration", "تولید ریموت"); + dictionary.Add("RemoteCopy", "کپی ریموت"); + dictionary.Add("RemoteControlDetector", "تشخیص‌دهنده ریموت"); + dictionary.Add("ChipIdentificationCopy", "کپی شناسه تراشه"); + dictionary.Add("ChipGeneration", "تولید تراشه"); + dictionary.Add("AccessCardCopy", "کپی کارت دسترسی"); + dictionary.Add("KeyUnlock", "باز کردن قفل کلید"); + dictionary.Add("TirePressure", "عملکردهای فشار لاستیک"); + dictionary.Add("MatchingFunction", "عملکردهای هماهنگ‌سازی"); + dictionary.Add("ChipWriteStart", "شروع نوشتن تراشه"); + dictionary.Add("Programmer", "برنامه‌ریز"); + dictionary.Add("AntiTheftData", "داده‌های ضد دزدی"); + dictionary.Add("UpdateCenter", "مرکز به‌روزرسانی"); + + dictionary.Add("ContactWechatCopy", "شناسه وی‌چت کپی شد"); + dictionary.Add("ContactEmailCopy", "آدرس ایمیل کپی شد"); + + dictionary.Add("Tips", "نکته"); + dictionary.Add("Connected", "متصل شد"); + dictionary.Add("Disconnected", "قطع شد"); + dictionary.Add("DeviceDisconnected", "دستگاه متصل نیست"); + dictionary.Add("K3ToolConnectTips", "لطفاً قبل از به‌روزرسانی دستگاه K3Tool را متصل کنید"); + dictionary.Add("FileNotFound", "فایل یافت نشد"); + + dictionary.Add("UpToDate", "هم‌اکنون به‌روز است"); + dictionary.Add("PleaseNote", "لطفاً توجه کنید"); + dictionary.Add("NoKeyData", "داده‌های کلید در دسترس نیست"); + dictionary.Add("Lock", "قفل کردن"); + dictionary.Add("Unlock", "باز کردن قفل"); + dictionary.Add("Trunk", "صندوق عقب"); + dictionary.Add("Panic", "پنیک"); + dictionary.Add("BurnTips", "فایل برنامه‌ریزی وجود ندارد. لطفاً مدل کلید مورد نظر برای برنامه‌ریزی را انتخاب کنید."); + dictionary.Add("BurnCSubKeyMobileAppTips", "لطفاً برای برنامه‌ریزی زیرکلید C از برنامه موبایل استفاده کنید."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "لطفاً برای برنامه‌ریزی زیرکلید CF از برنامه موبایل استفاده کنید."); + dictionary.Add("BurnSuccess", "برنامه‌ریزی با موفقیت انجام شد"); + dictionary.Add("BurnError", "برنامه‌ریزی با شکست مواجه شد"); + dictionary.Add("NotMoreTips", "داده‌ای دیگر وجود ندارد~"); + dictionary.Add("CloudData", "داده‌های ابری"); + dictionary.Add("CloudDataSyncCompleted", "همگام‌سازی کامل شد"); + dictionary.Add("CloudDataSyncFailed", "همگام‌سازی ناموفق بود"); + dictionary.Add("CloudDataDownloadFailed", "دانلود ناموفق بود"); + dictionary.Add("CloudDataDeviceNotFound", "دستگاه ذخیره‌سازی K3Tool شناسایی نشد"); + dictionary.Add("CloudDataInitCompleted", "راه‌اندازی K3Tool کامل شد"); + dictionary.Add("HidSendPacketError", "خطا در ارسال بسته داده"); + dictionary.Add("DialogPleaseWait", "لطفا صبر کنید"); + dictionary.Add("DialogProcessing", "در حال پردازش..."); + dictionary.Add("DialogCancelled", "لغو شد"); + dictionary.Add("DialogErrorPrefix", "خطا"); + dictionary.Add("DialogElapsedPrefix", "زمان"); + dictionary.Add("DialogBurningTitle", "در حال رایت"); + dictionary.Add("DialogBurningMessage", "در حال رایت..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary helanguage() + { + #region 希伯来语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "דף הבית"); + dictionary.Add("Settings", "הגדרות מערכת"); + dictionary.Add("RemoteGeneration", "יצירת שלט רחוק"); + dictionary.Add("RemoteCopy", "העתקת שלט רחוק"); + dictionary.Add("RemoteControlDetector", "גלאי שלטים רחוקים"); + dictionary.Add("ChipIdentificationCopy", "העתקת מזהה שבב"); + dictionary.Add("ChipGeneration", "יצירת שבב"); + dictionary.Add("AccessCardCopy", "העתקת כרטיס גישה"); + dictionary.Add("KeyUnlock", "ביטול נעילת מפתח"); + dictionary.Add("TirePressure", "פונקציות לחץ צמיגים"); + dictionary.Add("MatchingFunction", "פונקציות התאמה"); + dictionary.Add("ChipWriteStart", "התחל כתיבת שבב"); + dictionary.Add("Programmer", "מתכנת"); + dictionary.Add("AntiTheftData", "נתוני הגנה מפני גניבה"); + dictionary.Add("UpdateCenter", "מרכז עדכונים"); + + dictionary.Add("ContactWechatCopy", "מזהה וייצ'אט הועתק"); + dictionary.Add("ContactEmailCopy", "כתובת הדוא\"ל הועתקה"); + + dictionary.Add("Tips", "עצה"); + dictionary.Add("Connected", "מחובר"); + dictionary.Add("Disconnected", "מנותק"); + dictionary.Add("DeviceDisconnected", "המכשיר אינו מחובר"); + dictionary.Add("K3ToolConnectTips", "אנא חבר את מכשיר K3Tool לפני העדכון"); + dictionary.Add("FileNotFound", "הקובץ לא נמצא"); + + dictionary.Add("UpToDate", "כבר מעודכן"); + dictionary.Add("PleaseNote", "שים לב"); + dictionary.Add("NoKeyData", "אין נתוני מפתח זמינים"); + dictionary.Add("Lock", "נעל"); + dictionary.Add("Unlock", "פתח"); + dictionary.Add("Trunk", "מכולה"); + dictionary.Add("Panic", "פאניקה"); + dictionary.Add("BurnTips", "קובץ התכנות לא קיים. בחר את דגם המפתח לכתיבה."); + dictionary.Add("BurnCSubKeyMobileAppTips", "יש להשתמש באפליקציה לנייד כדי לתכנת את תת-המפתח C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "יש להשתמש באפליקציה לנייד כדי לתכנת את תת-המפתח CF."); + dictionary.Add("BurnSuccess", "התכנות הושלם בהצלחה"); + dictionary.Add("BurnError", "התכנות נכשל"); + dictionary.Add("NotMoreTips", "אין עוד נתונים~"); + dictionary.Add("CloudData", "נתונים בענן"); + dictionary.Add("CloudDataSyncCompleted", "הסנכרון הושלם"); + dictionary.Add("CloudDataSyncFailed", "הסנכרון נכשל"); + dictionary.Add("CloudDataDownloadFailed", "ההורדה נכשלה"); + dictionary.Add("CloudDataDeviceNotFound", "התקן האחסון של K3Tool לא זוהה"); + dictionary.Add("CloudDataInitCompleted", "אתחול K3Tool הושלם"); + dictionary.Add("HidSendPacketError", "אירעה שגיאה בשליחת חבילת הנתונים"); + dictionary.Add("DialogPleaseWait", "אנא המתן"); + dictionary.Add("DialogProcessing", "מעבד..."); + dictionary.Add("DialogCancelled", "בוטל"); + dictionary.Add("DialogErrorPrefix", "שגיאה"); + dictionary.Add("DialogElapsedPrefix", "זמן"); + dictionary.Add("DialogBurningTitle", "צריבה"); + dictionary.Add("DialogBurningMessage", "צורב..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary idlanguage() + { + #region 印尼语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Beranda"); + dictionary.Add("Settings", "Pengaturan Sistem"); + dictionary.Add("RemoteGeneration", "Pembuatan Remote"); + dictionary.Add("RemoteCopy", "Salin Remote"); + dictionary.Add("RemoteControlDetector", "Detektor Remote"); + dictionary.Add("ChipIdentificationCopy", "Salin ID Chip"); + dictionary.Add("ChipGeneration", "Pembuatan Chip"); + dictionary.Add("AccessCardCopy", "Salin Kartu Akses"); + dictionary.Add("KeyUnlock", "Buka Kunci Key"); + dictionary.Add("TirePressure", "Fungsi Tekanan Ban"); + dictionary.Add("MatchingFunction", "Fungsi Pencocokan"); + dictionary.Add("ChipWriteStart", "Mulai Menulis Chip"); + dictionary.Add("Programmer", "Programmer"); + dictionary.Add("AntiTheftData", "Data Anti-Pencurian"); + dictionary.Add("UpdateCenter", "Pusat Pembaruan"); + + dictionary.Add("ContactWechatCopy", "ID WeChat disalin"); + dictionary.Add("ContactEmailCopy", "Alamat email disalin"); + + dictionary.Add("Tips", "Tips"); + dictionary.Add("Connected", "Tersambung"); + dictionary.Add("Disconnected", "Terputus"); + dictionary.Add("DeviceDisconnected", "Perangkat tidak tersambung"); + dictionary.Add("K3ToolConnectTips", "Harap sambungkan perangkat K3Tool sebelum memperbarui"); + dictionary.Add("FileNotFound", "File tidak ditemukan"); + + dictionary.Add("UpToDate", "Sudah mutakhir"); + dictionary.Add("PleaseNote", "Harap diperhatikan"); + dictionary.Add("PleasePress", "Silakan tekan"); + dictionary.Add("Button", "tombol"); + dictionary.Add("NoKeyData", "Tidak ada data kunci tersedia"); + dictionary.Add("Lock", "Kunci"); + dictionary.Add("Unlock", "Buka Kunci"); + dictionary.Add("Trunk", "Bagasi"); + dictionary.Add("Panic", "Panic"); + dictionary.Add("BurnTips", "File pemrograman tidak ada. Silakan pilih model kunci yang akan diprogram."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Gunakan aplikasi seluler untuk memprogram sub-kunci C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Gunakan aplikasi seluler untuk memprogram sub-kunci CF."); + dictionary.Add("BurnSuccess", "Pemrograman berhasil"); + dictionary.Add("BurnError", "Pemrograman gagal"); + dictionary.Add("NotMoreTips", "Tidak ada data lagi~"); + dictionary.Add("CloudData", "Data Cloud"); + dictionary.Add("CloudDataSyncCompleted", "Sinkronisasi selesai"); + dictionary.Add("CloudDataSyncFailed", "Sinkronisasi gagal"); + dictionary.Add("CloudDataDownloadFailed", "Unduhan gagal"); + dictionary.Add("CloudDataDeviceNotFound", "Perangkat penyimpanan K3Tool tidak terdeteksi"); + dictionary.Add("CloudDataInitCompleted", "Inisialisasi K3Tool selesai"); + dictionary.Add("HidSendPacketError", "Terjadi kesalahan saat mengirim paket data"); + dictionary.Add("DialogPleaseWait", "Harap tunggu"); + dictionary.Add("DialogProcessing", "Memproses..."); + dictionary.Add("DialogCancelled", "Dibatalkan"); + dictionary.Add("DialogErrorPrefix", "Kesalahan"); + dictionary.Add("DialogElapsedPrefix", "Waktu"); + dictionary.Add("DialogBurningTitle", "Pemrograman"); + dictionary.Add("DialogBurningMessage", "Sedang memprogram..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary itlanguage() + { + #region 意大利语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Home"); + dictionary.Add("Settings", "Impostazioni di sistema"); + dictionary.Add("RemoteGeneration", "Generazione telecomando"); + dictionary.Add("RemoteCopy", "Copia telecomando"); + dictionary.Add("RemoteControlDetector", "Rilevatore telecomandi"); + dictionary.Add("ChipIdentificationCopy", "Copia identificativo chip"); + dictionary.Add("ChipGeneration", "Generazione chip"); + dictionary.Add("AccessCardCopy", "Copia carta d'accesso"); + dictionary.Add("KeyUnlock", "Sblocco chiave"); + dictionary.Add("TirePressure", "Funzioni pressione pneumatici"); + dictionary.Add("MatchingFunction", "Funzioni di abbinamento"); + dictionary.Add("ChipWriteStart", "Avvia scrittura chip"); + dictionary.Add("Programmer", "Programmatore"); + dictionary.Add("AntiTheftData", "Dati antifurto"); + dictionary.Add("UpdateCenter", "Centro aggiornamenti"); + + dictionary.Add("ContactWechatCopy", "ID WeChat copiato"); + dictionary.Add("ContactEmailCopy", "Indirizzo email copiato"); + + dictionary.Add("Tips", "Suggerimento"); + dictionary.Add("Connected", "Connesso"); + dictionary.Add("Disconnected", "Disconnesso"); + dictionary.Add("DeviceDisconnected", "Dispositivo non connesso"); + dictionary.Add("K3ToolConnectTips", "Collegare il dispositivo K3Tool prima dell'aggiornamento"); + dictionary.Add("FileNotFound", "File non trovato"); + + dictionary.Add("UpToDate", "Già aggiornato"); + dictionary.Add("PleaseNote", "Si prega di notare"); + dictionary.Add("PleasePress", "Premere"); + dictionary.Add("Button", "il pulsante"); + dictionary.Add("NoKeyData", "Nessun dato chiave disponibile"); + dictionary.Add("Lock", "Blocca"); + dictionary.Add("Unlock", "Sblocca"); + dictionary.Add("Trunk", "Bagagliaio"); + dictionary.Add("Panic", "Panico"); + dictionary.Add("BurnTips", "Il file di programmazione non esiste. Seleziona il modello di chiave da programmare."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Utilizza l'app mobile per programmare la sottochiave C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Utilizza l'app mobile per programmare la sottochiave CF."); + dictionary.Add("BurnSuccess", "Programmazione completata con successo"); + dictionary.Add("BurnError", "Programmazione fallita"); + dictionary.Add("NotMoreTips", "Non ci sono più dati~"); + dictionary.Add("CloudData", "Dati Cloud"); + dictionary.Add("CloudDataSyncCompleted", "Sincronizzazione completata"); + dictionary.Add("CloudDataSyncFailed", "Sincronizzazione non riuscita"); + dictionary.Add("CloudDataDownloadFailed", "Download non riuscito"); + dictionary.Add("CloudDataDeviceNotFound", "Dispositivo di archiviazione K3Tool non rilevato"); + dictionary.Add("CloudDataInitCompleted", "Inizializzazione K3Tool completata"); + dictionary.Add("HidSendPacketError", "Errore durante l'invio del pacchetto dati"); + dictionary.Add("DialogPleaseWait", "Attendere prego"); + dictionary.Add("DialogProcessing", "Elaborazione in corso..."); + dictionary.Add("DialogCancelled", "Annullato"); + dictionary.Add("DialogErrorPrefix", "Errore"); + dictionary.Add("DialogElapsedPrefix", "Tempo"); + dictionary.Add("DialogBurningTitle", "Programmazione"); + dictionary.Add("DialogBurningMessage", "Programmazione in corso..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary kolanguage() + { + #region 韩语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "홈"); + dictionary.Add("Settings", "시스템 설정"); + dictionary.Add("RemoteGeneration", "리모컨 생성"); + dictionary.Add("RemoteCopy", "리모컨 복사"); + dictionary.Add("RemoteControlDetector", "리모컨 탐지기"); + dictionary.Add("ChipIdentificationCopy", "칩 ID 복사"); + dictionary.Add("ChipGeneration", "칩 생성"); + dictionary.Add("AccessCardCopy", "출입 카드 복사"); + dictionary.Add("KeyUnlock", "키 잠금 해제"); + dictionary.Add("TirePressure", "타이어 공기압 기능"); + dictionary.Add("MatchingFunction", "매칭 기능"); + dictionary.Add("ChipWriteStart", "칩 쓰기 시작"); + dictionary.Add("Programmer", "프로그래머"); + dictionary.Add("AntiTheftData", "도난 방지 데이터"); + dictionary.Add("UpdateCenter", "업데이트 센터"); + + dictionary.Add("ContactWechatCopy", "WeChat ID가 복사되었습니다"); + dictionary.Add("ContactEmailCopy", "이메일 주소가 복사되었습니다"); + + dictionary.Add("Tips", "팁"); + dictionary.Add("Connected", "연결됨"); + dictionary.Add("Disconnected", "연결 끊김"); + dictionary.Add("DeviceDisconnected", "장치가 연결되지 않음"); + dictionary.Add("K3ToolConnectTips", "업데이트 전에 K3Tool 장치를 연결하세요"); + dictionary.Add("FileNotFound", "파일을 찾을 수 없습니다"); + + dictionary.Add("UpToDate", "이미 최신 상태입니다"); + dictionary.Add("PleaseNote", "참고해 주세요"); + dictionary.Add("PleasePress", "눌러 주세요"); + dictionary.Add("Button", "버튼"); + dictionary.Add("NoKeyData", "키 데이터 없음"); + dictionary.Add("Lock", "잠금"); + dictionary.Add("Unlock", "잠금 해제"); + dictionary.Add("Trunk", "트렁크"); + dictionary.Add("Panic", "패닉"); + dictionary.Add("BurnTips", "프로그래밍 파일이 없습니다. 프로그래밍할 키 모델을 선택하세요."); + dictionary.Add("BurnCSubKeyMobileAppTips", "모바일 앱을 사용하여 C 하위 키를 프로그래밍하세요."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "모바일 앱을 사용하여 CF 하위 키를 프로그래밍하세요."); + dictionary.Add("BurnSuccess", "프로그래밍 성공"); + dictionary.Add("BurnError", "프로그래밍 실패"); + dictionary.Add("NotMoreTips", "더 이상 데이터가 없습니다~"); + dictionary.Add("CloudData", "클라우드 데이터"); + dictionary.Add("CloudDataSyncCompleted", "동기화 완료"); + dictionary.Add("CloudDataSyncFailed", "동기화 실패"); + dictionary.Add("CloudDataDownloadFailed", "다운로드 실패"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool 저장 장치를 찾지 못했습니다"); + dictionary.Add("CloudDataInitCompleted", "K3Tool 초기화 완료"); + dictionary.Add("HidSendPacketError", "데이터 패킷 전송 중 오류가 발생했습니다"); + dictionary.Add("DialogPleaseWait", "잠시 기다려 주세요"); + dictionary.Add("DialogProcessing", "처리 중..."); + dictionary.Add("DialogCancelled", "취소됨"); + dictionary.Add("DialogErrorPrefix", "오류"); + dictionary.Add("DialogElapsedPrefix", "시간"); + dictionary.Add("DialogBurningTitle", "프로그래밍"); + dictionary.Add("DialogBurningMessage", "프로그래밍 중..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary pllanguage() + { + #region 波兰语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Strona główna"); + dictionary.Add("Settings", "Ustawienia systemowe"); + dictionary.Add("RemoteGeneration", "Generowanie pilota"); + dictionary.Add("RemoteCopy", "Kopiowanie pilota"); + dictionary.Add("RemoteControlDetector", "Wykrywacz pilotów"); + dictionary.Add("ChipIdentificationCopy", "Kopiowanie identyfikatora chipu"); + dictionary.Add("ChipGeneration", "Generowanie chipu"); + dictionary.Add("AccessCardCopy", "Kopiowanie karty dostępu"); + dictionary.Add("KeyUnlock", "Odblokowanie klucza"); + dictionary.Add("TirePressure", "Funkcje ciśnienia w oponach"); + dictionary.Add("MatchingFunction", "Funkcje dopasowania"); + dictionary.Add("ChipWriteStart", "Rozpocznij zapis chipu"); + dictionary.Add("Programmer", "Programator"); + dictionary.Add("AntiTheftData", "Dane antywłamaniowe"); + dictionary.Add("UpdateCenter", "Centrum aktualizacji"); + + dictionary.Add("ContactWechatCopy", "Skopiowano ID WeChat"); + dictionary.Add("ContactEmailCopy", "Skopiowano adres e-mail"); + + dictionary.Add("Tips", "Wskazówka"); + dictionary.Add("Connected", "Połączono"); + dictionary.Add("Disconnected", "Rozłączono"); + dictionary.Add("DeviceDisconnected", "Urządzenie nie jest podłączone"); + dictionary.Add("K3ToolConnectTips", "Podłącz urządzenie K3Tool przed aktualizacją"); + dictionary.Add("FileNotFound", "Nie znaleziono pliku"); + + dictionary.Add("UpToDate", "Już aktualne"); + dictionary.Add("PleaseNote", "Proszę zauważyć"); + dictionary.Add("PleasePress", "Proszę nacisnąć"); + dictionary.Add("Button", "przycisk"); + dictionary.Add("NoKeyData", "Brak danych klucza"); + dictionary.Add("Lock", "Zablokuj"); + dictionary.Add("Unlock", "Odblokuj"); + dictionary.Add("Trunk", "Bagażnik"); + dictionary.Add("Panic", "Panika"); + dictionary.Add("BurnTips", "Plik programowania nie istnieje. Proszę wybrać model klucza do zaprogramowania."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Użyj aplikacji mobilnej, aby zaprogramować podklucz C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Użyj aplikacji mobilnej, aby zaprogramować podklucz CF."); + dictionary.Add("BurnSuccess", "Programowanie zakończone pomyślnie"); + dictionary.Add("BurnError", "Programowanie nie powiodło się"); + dictionary.Add("NotMoreTips", "Brak więcej danych~"); + dictionary.Add("CloudData", "Dane Chmurowe"); + dictionary.Add("CloudDataSyncCompleted", "Synchronizacja zakończona"); + dictionary.Add("CloudDataSyncFailed", "Synchronizacja nie powiodła się"); + dictionary.Add("CloudDataDownloadFailed", "Pobieranie nie powiodło się"); + dictionary.Add("CloudDataDeviceNotFound", "Nie wykryto urządzenia pamięci K3Tool"); + dictionary.Add("CloudDataInitCompleted", "Inicjalizacja K3Tool zakończona"); + dictionary.Add("HidSendPacketError", "Wystąpił błąd podczas wysyłania pakietu danych"); + dictionary.Add("DialogPleaseWait", "Proszę czekać"); + dictionary.Add("DialogProcessing", "Przetwarzanie..."); + dictionary.Add("DialogCancelled", "Anulowano"); + dictionary.Add("DialogErrorPrefix", "Błąd"); + dictionary.Add("DialogElapsedPrefix", "Czas"); + dictionary.Add("DialogBurningTitle", "Programowanie"); + dictionary.Add("DialogBurningMessage", "Trwa programowanie..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary rulanguage() + { + #region 俄语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Главная"); + dictionary.Add("Settings", "Системные настройки"); + dictionary.Add("RemoteGeneration", "Генерация брелока"); + dictionary.Add("RemoteCopy", "Копирование брелока"); + dictionary.Add("RemoteControlDetector", "Диагностика брелока"); + dictionary.Add("ChipIdentificationCopy", "Копирование чипа (идентификация)"); + dictionary.Add("ChipGeneration", "Генерация чипа"); + dictionary.Add("AccessCardCopy", "Копирование карты доступа"); + dictionary.Add("KeyUnlock", "Разблокировка ключа"); + dictionary.Add("TirePressure", "Функция давления в шинах"); + dictionary.Add("MatchingFunction", "Функция привязки"); + dictionary.Add("ChipWriteStart", "Запись стартового чипа"); + dictionary.Add("Programmer", "Программатор"); + dictionary.Add("AntiTheftData", "Данные иммобилайзера"); + dictionary.Add("UpdateCenter", "Центр обновлений"); + + dictionary.Add("ContactWechatCopy", "Номер WeChat скопирован"); + dictionary.Add("ContactEmailCopy", "Адрес электронной почты скопирован"); + + dictionary.Add("Tips", "Подсказка"); + dictionary.Add("Connected", "Подключено"); + dictionary.Add("Disconnected", "Отключено"); + dictionary.Add("DeviceDisconnected", "Устройство не подключено"); + dictionary.Add("K3ToolConnectTips", "Подключите устройство K3Tool перед началом обновления"); + dictionary.Add("FileNotFound", "Файл не найден"); + + dictionary.Add("UpToDate", "Установлена последняя версия"); + dictionary.Add("PleaseNote", "Обратите внимание"); + dictionary.Add("PleasePress", "Нажмите"); + dictionary.Add("Button", "кнопку"); + dictionary.Add("NoKeyData", "Данные ключа недоступны"); + dictionary.Add("Lock", "Заблокировать"); + dictionary.Add("Unlock", "Разблокировать"); + dictionary.Add("Trunk", "Багажник"); + dictionary.Add("Panic", "Паника"); + dictionary.Add("BurnTips", "Файл прошивки не существует. Пожалуйста, выберите модель ключа для прошивки."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Используйте мобильное приложение для прошивки C-подключа."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Используйте мобильное приложение для прошивки CF-подключа."); + dictionary.Add("BurnSuccess", "Прошивка успешно завершена"); + dictionary.Add("BurnError", "Прошивка не удалась"); + dictionary.Add("NotMoreTips", "Больше нет данных~"); + dictionary.Add("CloudData", "Облачные Данные"); + dictionary.Add("CloudDataSyncCompleted", "Синхронизация завершена"); + dictionary.Add("CloudDataSyncFailed", "Синхронизация не удалась"); + dictionary.Add("CloudDataDownloadFailed", "Не удалось загрузить"); + dictionary.Add("CloudDataDeviceNotFound", "Накопитель K3Tool не обнаружен"); + dictionary.Add("CloudDataInitCompleted", "Инициализация K3Tool завершена"); + dictionary.Add("HidSendPacketError", "Ошибка при отправке пакета данных"); + dictionary.Add("DialogPleaseWait", "Пожалуйста, подождите"); + dictionary.Add("DialogProcessing", "Обработка..."); + dictionary.Add("DialogCancelled", "Отменено"); + dictionary.Add("DialogErrorPrefix", "Ошибка"); + dictionary.Add("DialogElapsedPrefix", "Время"); + dictionary.Add("DialogBurningTitle", "Программирование"); + dictionary.Add("DialogBurningMessage", "Идет программирование..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary thlanguage() + { + #region 泰语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "หน้าหลัก"); + dictionary.Add("Settings", "การตั้งค่าระบบ"); + dictionary.Add("RemoteGeneration", "สร้างรีโมท"); + dictionary.Add("RemoteCopy", "คัดลอกข้อมูลรีโมท"); + dictionary.Add("RemoteControlDetector", "เครื่องตรวจจับรีโมท"); + dictionary.Add("ChipIdentificationCopy", "คัดลอกไอดีชิป"); + dictionary.Add("ChipGeneration", "สร้างชิป"); + dictionary.Add("AccessCardCopy", "คัดลอกบัตรเข้าออก"); + dictionary.Add("KeyUnlock", "ปลดล็อกกุญแจ"); + dictionary.Add("TirePressure", "ฟังก์ชันวัดแรงดันยาง"); + dictionary.Add("MatchingFunction", "ฟังก์ชันจับคู่"); + dictionary.Add("ChipWriteStart", "เริ่มเขียนข้อมูลชิป"); + dictionary.Add("Programmer", "โปรแกรมเมอร์"); + dictionary.Add("AntiTheftData", "ข้อมูลป้องกันการโจรกรรม"); + dictionary.Add("UpdateCenter", "ศูนย์อัปเดต"); + + dictionary.Add("ContactWechatCopy", "คัดลอก ID WeChat แล้ว"); + dictionary.Add("ContactEmailCopy", "คัดลอกที่อยู่อีเมลแล้ว"); + + dictionary.Add("Tips", "คำแนะนำ"); + dictionary.Add("Connected", "เชื่อมต่อแล้ว"); + dictionary.Add("Disconnected", "ตัดการเชื่อมต่อ"); + dictionary.Add("DeviceDisconnected", "ไม่ได้เชื่อมต่ออุปกรณ์"); + dictionary.Add("K3ToolConnectTips", "กรุณาเชื่อมต่ออุปกรณ์ K3Tool ก่อนอัปเดต"); + dictionary.Add("FileNotFound", "ไม่พบไฟล์"); + + dictionary.Add("UpToDate", "อัปเดตแล้ว"); + dictionary.Add("PleaseNote", "โปรดทราบ"); + dictionary.Add("PleasePress", "กรุณากด"); + dictionary.Add("Button", "ปุ่ม"); + dictionary.Add("NoKeyData", "ไม่มีข้อมูลกุญแจ"); + dictionary.Add("Lock", "ล็อก"); + dictionary.Add("Unlock", "ปลดล็อก"); + dictionary.Add("Trunk", "ห้องเก็บของ"); + dictionary.Add("Panic", "แพนิก"); + dictionary.Add("BurnTips", "ไม่มีไฟล์โปรแกรม กรุณาเลือกรุ่นกุญแจที่ต้องการโปรแกรม"); + dictionary.Add("BurnCSubKeyMobileAppTips", "โปรดใช้แอปมือถือเพื่อโปรแกรมกุญแจย่อย C"); + dictionary.Add("BurnCFSubKeyMobileAppTips", "โปรดใช้แอปมือถือเพื่อโปรแกรมกุญแจย่อย CF"); + dictionary.Add("BurnSuccess", "โปรแกรมสำเร็จ"); + dictionary.Add("BurnError", "โปรแกรมล้มเหลว"); + dictionary.Add("NotMoreTips", "ไม่มีข้อมูลอีกแล้ว~"); + dictionary.Add("CloudData", "ข้อมูลคลาวด์"); + dictionary.Add("CloudDataSyncCompleted", "ซิงค์เสร็จสมบูรณ์"); + dictionary.Add("CloudDataSyncFailed", "ซิงค์ล้มเหลว"); + dictionary.Add("CloudDataDownloadFailed", "ดาวน์โหลดล้มเหลว"); + dictionary.Add("CloudDataDeviceNotFound", "ไม่พบอุปกรณ์จัดเก็บข้อมูล K3Tool"); + dictionary.Add("CloudDataInitCompleted", "เริ่มต้น K3Tool เสร็จสมบูรณ์"); + dictionary.Add("HidSendPacketError", "เกิดข้อผิดพลาดในการส่งแพ็กเก็ตข้อมูล"); + dictionary.Add("DialogPleaseWait", "กรุณารอสักครู่"); + dictionary.Add("DialogProcessing", "กำลังประมวลผล..."); + dictionary.Add("DialogCancelled", "ยกเลิกแล้ว"); + dictionary.Add("DialogErrorPrefix", "ข้อผิดพลาด"); + dictionary.Add("DialogElapsedPrefix", "เวลา"); + dictionary.Add("DialogBurningTitle", "กำลังโปรแกรม"); + dictionary.Add("DialogBurningMessage", "กำลังโปรแกรม..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary trlanguage() + { + #region 土耳其语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Ana Sayfa"); + dictionary.Add("Settings", "Sistem Ayarları"); + dictionary.Add("RemoteGeneration", "Uzaktan Kumanda Oluşturma"); + dictionary.Add("RemoteCopy", "Uzaktan Kumanda Kopyalama"); + dictionary.Add("RemoteControlDetector", "Uzaktan Kumanda Dedektörü"); + dictionary.Add("ChipIdentificationCopy", "Çip Kimlik Kopyalama"); + dictionary.Add("ChipGeneration", "Çip Oluşturma"); + dictionary.Add("AccessCardCopy", "Erişim Kartı Kopyalama"); + dictionary.Add("KeyUnlock", "Anahtar Kilidini Açma"); + dictionary.Add("TirePressure", "Lastik Basıncı İşlevleri"); + dictionary.Add("MatchingFunction", "Eşleştirme İşlevleri"); + dictionary.Add("ChipWriteStart", "Çip Yazmaya Başla"); + dictionary.Add("Programmer", "Programlayıcı"); + dictionary.Add("AntiTheftData", "Hırsızlık Önleme Verileri"); + dictionary.Add("UpdateCenter", "Güncelleme Merkezi"); + + dictionary.Add("ContactWechatCopy", "WeChat ID'si kopyalandı"); + dictionary.Add("ContactEmailCopy", "E-posta adresi kopyalandı"); + + dictionary.Add("Tips", "İpucu"); + dictionary.Add("Connected", "Bağlı"); + dictionary.Add("Disconnected", "Bağlantı Kesildi"); + dictionary.Add("DeviceDisconnected", "Cihaz bağlı değil"); + dictionary.Add("K3ToolConnectTips", "Güncellemeden önce lütfen K3Tool cihazını bağlayın"); + dictionary.Add("FileNotFound", "Dosya bulunamadı"); + + dictionary.Add("UpToDate", "Zaten güncel"); + dictionary.Add("PleaseNote", "Lütfen dikkat edin"); + dictionary.Add("PleasePress", "Lütfen basın"); + dictionary.Add("Button", "düğme"); + dictionary.Add("NoKeyData", "Anahtar verisi yok"); + dictionary.Add("Lock", "Kilitle"); + dictionary.Add("Unlock", "Kilidi Aç"); + dictionary.Add("Trunk", "Bagaj"); + dictionary.Add("Panic", "Panik"); + dictionary.Add("BurnTips", "Yazma dosyası mevcut değil. Lütfen yazılacak anahtar modelini seçin."); + dictionary.Add("BurnCSubKeyMobileAppTips", "C alt anahtarını programlamak için mobil uygulamayı kullanın."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "CF alt anahtarını programlamak için mobil uygulamayı kullanın."); + dictionary.Add("BurnSuccess", "Yazma işlemi başarılı"); + dictionary.Add("BurnError", "Yazma işlemi başarısız"); + dictionary.Add("NotMoreTips", "Daha fazla veri yok~"); + dictionary.Add("CloudData", "Bulut Verileri"); + dictionary.Add("CloudDataSyncCompleted", "Senkronizasyon tamamlandı"); + dictionary.Add("CloudDataSyncFailed", "Senkronizasyon başarısız"); + dictionary.Add("CloudDataDownloadFailed", "İndirme başarısız"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool depolama aygıtı algılanmadı"); + dictionary.Add("CloudDataInitCompleted", "K3Tool başlatma tamamlandı"); + dictionary.Add("HidSendPacketError", "Veri paketi gönderilirken hata oluştu"); + dictionary.Add("DialogPleaseWait", "Lütfen bekleyin"); + dictionary.Add("DialogProcessing", "İşleniyor..."); + dictionary.Add("DialogCancelled", "İptal edildi"); + dictionary.Add("DialogErrorPrefix", "Hata"); + dictionary.Add("DialogElapsedPrefix", "Süre"); + dictionary.Add("DialogBurningTitle", "Programlama"); + dictionary.Add("DialogBurningMessage", "Programlanıyor..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary vilanguage() + { + #region 越南语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Trang chủ"); + dictionary.Add("Settings", "Cài đặt hệ thống"); + dictionary.Add("RemoteGeneration", "Tạo điều khiển từ xa"); + dictionary.Add("RemoteCopy", "Sao chép điều khiển từ xa"); + dictionary.Add("RemoteControlDetector", "Máy dò điều khiển từ xa"); + dictionary.Add("ChipIdentificationCopy", "Sao chép ID chip"); + dictionary.Add("ChipGeneration", "Tạo chip"); + dictionary.Add("AccessCardCopy", "Sao chép thẻ truy cập"); + dictionary.Add("KeyUnlock", "Mở khóa chìa"); + dictionary.Add("TirePressure", "Chức năng áp suất lốp"); + dictionary.Add("MatchingFunction", "Chức năng ghép nối"); + dictionary.Add("ChipWriteStart", "Bắt đầu ghi chip"); + dictionary.Add("Programmer", "Thiết bị lập trình"); + dictionary.Add("AntiTheftData", "Dữ liệu chống trộm"); + dictionary.Add("UpdateCenter", "Trung tâm cập nhật"); + + dictionary.Add("ContactWechatCopy", "Đã sao chép ID WeChat"); + dictionary.Add("ContactEmailCopy", "Đã sao chép địa chỉ email"); + + dictionary.Add("Tips", "Mẹo"); + dictionary.Add("Connected", "Đã kết nối"); + dictionary.Add("Disconnected", "Ngắt kết nối"); + dictionary.Add("DeviceDisconnected", "Thiết bị chưa kết nối"); + dictionary.Add("K3ToolConnectTips", "Vui lòng kết nối thiết bị K3Tool trước khi cập nhật"); + dictionary.Add("FileNotFound", "Không tìm thấy tệp"); + + dictionary.Add("UpToDate", "Đã là phiên bản mới nhất"); + dictionary.Add("PleaseNote", "Vui lòng lưu ý"); + dictionary.Add("PleasePress", "Vui lòng nhấn"); + dictionary.Add("Button", "nút"); + dictionary.Add("NoKeyData", "Không có dữ liệu chìa khóa"); + dictionary.Add("Lock", "Khóa"); + dictionary.Add("Unlock", "Mở khóa"); + dictionary.Add("Trunk", "Cốp xe"); + dictionary.Add("Panic", "Hoảng loạn"); + dictionary.Add("BurnTips", "Tệp lập trình không tồn tại. Vui lòng chọn mẫu chìa khóa cần lập trình."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Vui lòng sử dụng ứng dụng di động để lập trình chìa khóa phụ C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Vui lòng sử dụng ứng dụng di động để lập trình chìa khóa phụ CF."); + dictionary.Add("BurnSuccess", "Lập trình thành công"); + dictionary.Add("BurnError", "Lập trình thất bại"); + dictionary.Add("NotMoreTips", "Không còn dữ liệu nào nữa~"); + dictionary.Add("CloudData", "Dữ liệu Cloud"); + dictionary.Add("CloudDataSyncCompleted", "Đồng bộ hoàn tất"); + dictionary.Add("CloudDataSyncFailed", "Đồng bộ thất bại"); + dictionary.Add("CloudDataDownloadFailed", "Tải xuống thất bại"); + dictionary.Add("CloudDataDeviceNotFound", "Không phát hiện thiết bị lưu trữ K3Tool"); + dictionary.Add("CloudDataInitCompleted", "Khởi tạo K3Tool hoàn tất"); + dictionary.Add("HidSendPacketError", "Đã xảy ra lỗi khi gửi gói dữ liệu"); + dictionary.Add("DialogPleaseWait", "Vui lòng chờ"); + dictionary.Add("DialogProcessing", "Đang xử lý..."); + dictionary.Add("DialogCancelled", "Đã hủy"); + dictionary.Add("DialogErrorPrefix", "Lỗi"); + dictionary.Add("DialogElapsedPrefix", "Thời gian"); + dictionary.Add("DialogBurningTitle", "Lập trình"); + dictionary.Add("DialogBurningMessage", "Đang lập trình..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary delanguage() + { + #region 德语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Startseite"); + dictionary.Add("Settings", "Systemeinstellungen"); + dictionary.Add("RemoteGeneration", "Fernbedienung generieren"); + dictionary.Add("RemoteCopy", "Fernbedienung kopieren"); + dictionary.Add("RemoteControlDetector", "Fernbedienungs-Detektor"); + dictionary.Add("ChipIdentificationCopy", "Chip-ID kopieren"); + dictionary.Add("ChipGeneration", "Chip generieren"); + dictionary.Add("AccessCardCopy", "Zugangskarte kopieren"); + dictionary.Add("KeyUnlock", "Schlüssel entsperren"); + dictionary.Add("TirePressure", "Reifendruckfunktionen"); + dictionary.Add("MatchingFunction", "Anlernfunktionen"); + dictionary.Add("ChipWriteStart", "Chipbeschreibung starten"); + dictionary.Add("Programmer", "Programmiergerät"); + dictionary.Add("AntiTheftData", "Diebstahlschutzdaten"); + dictionary.Add("UpdateCenter", "Update-Center"); + + dictionary.Add("ContactWechatCopy", "WeChat-ID kopiert"); + dictionary.Add("ContactEmailCopy", "E-Mail-Adresse kopiert"); + + dictionary.Add("Tips", "Hinweis"); + dictionary.Add("Connected", "Verbunden"); + dictionary.Add("Disconnected", "Getrennt"); + dictionary.Add("DeviceDisconnected", "Gerät nicht verbunden"); + dictionary.Add("K3ToolConnectTips", "Bitte verbinden Sie das K3Tool-Gerät vor dem Update"); + dictionary.Add("FileNotFound", "Datei nicht gefunden"); + + dictionary.Add("UpToDate", "Bereits aktuell"); + dictionary.Add("PleaseNote", "Bitte beachten Sie"); + dictionary.Add("PleasePress", "Bitte drücken Sie"); + dictionary.Add("Button", "die Taste"); + dictionary.Add("NoKeyData", "Keine Schlüsseldaten verfügbar"); + dictionary.Add("Lock", "Verriegeln"); + dictionary.Add("Unlock", "Entriegeln"); + dictionary.Add("Trunk", "Kofferraum"); + dictionary.Add("Panic", "Panik"); + dictionary.Add("BurnTips", "Die Programmierdatei existiert nicht. Bitte wählen Sie das zu programmierende Schlüsselmodell aus."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Bitte verwenden Sie die mobile App, um den C-Unterschlüssel zu programmieren."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Bitte verwenden Sie die mobile App, um den CF-Unterschlüssel zu programmieren."); + dictionary.Add("BurnSuccess", "Programmierung erfolgreich abgeschlossen"); + dictionary.Add("BurnError", "Programmierung fehlgeschlagen"); + dictionary.Add("NotMoreTips", "Keine weiteren Daten~"); + dictionary.Add("CloudData", "Cloud-Daten"); + dictionary.Add("CloudDataSyncCompleted", "Synchronisierung abgeschlossen"); + dictionary.Add("CloudDataSyncFailed", "Synchronisierung fehlgeschlagen"); + dictionary.Add("CloudDataDownloadFailed", "Download fehlgeschlagen"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool-Speichergerät nicht erkannt"); + dictionary.Add("CloudDataInitCompleted", "K3Tool-Initialisierung abgeschlossen"); + dictionary.Add("HidSendPacketError", "Fehler beim Senden des Datenpakets"); + dictionary.Add("DialogPleaseWait", "Bitte warten"); + dictionary.Add("DialogProcessing", "Wird verarbeitet..."); + dictionary.Add("DialogCancelled", "Abgebrochen"); + dictionary.Add("DialogErrorPrefix", "Fehler"); + dictionary.Add("DialogElapsedPrefix", "Zeit"); + dictionary.Add("DialogBurningTitle", "Programmierung"); + dictionary.Add("DialogBurningMessage", "Wird programmiert..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary uklanguage() + { + #region 乌克兰语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Головна сторінка"); + dictionary.Add("Settings", "Налаштування системи"); + dictionary.Add("RemoteGeneration", "Генерація брелока"); + dictionary.Add("RemoteCopy", "Копіювання брелока"); + dictionary.Add("RemoteControlDetector", "Детектор брелоків"); + dictionary.Add("ChipIdentificationCopy", "Копіювання ID чіпа"); + dictionary.Add("ChipGeneration", "Генерація чіпа"); + dictionary.Add("AccessCardCopy", "Копіювання картки доступу"); + dictionary.Add("KeyUnlock", "Розблокування ключа"); + dictionary.Add("TirePressure", "Функції тиску в шинах"); + dictionary.Add("MatchingFunction", "Функції прив’язки"); + dictionary.Add("ChipWriteStart", "Почати запис чіпа"); + dictionary.Add("Programmer", "Програматор"); + dictionary.Add("AntiTheftData", "Дані проти викрадення"); + dictionary.Add("UpdateCenter", "Центр оновлень"); + + dictionary.Add("ContactWechatCopy", "ID WeChat скопійовано"); + dictionary.Add("ContactEmailCopy", "Адресу електронної пошти скопійовано"); + + dictionary.Add("Tips", "Порада"); + dictionary.Add("Connected", "Підключено"); + dictionary.Add("Disconnected", "Відключено"); + dictionary.Add("DeviceDisconnected", "Пристрій не підключено"); + dictionary.Add("K3ToolConnectTips", "Будь ласка, підключіть пристрій K3Tool перед оновленням"); + dictionary.Add("FileNotFound", "Файл не знайдено"); + + dictionary.Add("UpToDate", "Вже оновлено"); + dictionary.Add("PleaseNote", "Зверніть увагу"); + dictionary.Add("PleasePress", "Натисніть"); + dictionary.Add("Button", "кнопку"); + dictionary.Add("NoKeyData", "Дані ключа недоступні"); + dictionary.Add("Lock", "Заблокувати"); + dictionary.Add("Unlock", "Розблокувати"); + dictionary.Add("Trunk", "Багажник"); + dictionary.Add("Panic", "Паніка"); + dictionary.Add("BurnTips", "Файл прошивки не існує. Будь ласка, виберіть модель ключа для прошивки."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Використовуйте мобільний застосунок для прошивки C-підключа."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Використовуйте мобільний застосунок для прошивки CF-підключа."); + dictionary.Add("BurnSuccess", "Прошивку успішно завершено"); + dictionary.Add("BurnError", "Помилка прошивки"); + dictionary.Add("NotMoreTips", "Немає більше даних~"); + dictionary.Add("CloudData", "Хмарні Дані"); + dictionary.Add("CloudDataSyncCompleted", "Синхронізацію завершено"); + dictionary.Add("CloudDataSyncFailed", "Синхронізація не вдалася"); + dictionary.Add("CloudDataDownloadFailed", "Не вдалося завантажити"); + dictionary.Add("CloudDataDeviceNotFound", "Не виявлено пристрій зберігання K3Tool"); + dictionary.Add("CloudDataInitCompleted", "Ініціалізацію K3Tool завершено"); + dictionary.Add("HidSendPacketError", "Сталася помилка під час надсилання пакета даних"); + dictionary.Add("DialogPleaseWait", "Будь ласка, зачекайте"); + dictionary.Add("DialogProcessing", "Обробка..."); + dictionary.Add("DialogCancelled", "Скасовано"); + dictionary.Add("DialogErrorPrefix", "Помилка"); + dictionary.Add("DialogElapsedPrefix", "Час"); + dictionary.Add("DialogBurningTitle", "Програмування"); + dictionary.Add("DialogBurningMessage", "Триває програмування..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + private static Dictionary mslanguage() + { + #region 马来语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Halaman Utama"); + dictionary.Add("Settings", "Tetapan Sistem"); + dictionary.Add("RemoteGeneration", "Penjanaan Remote"); + dictionary.Add("RemoteCopy", "Salinan Remote"); + dictionary.Add("RemoteControlDetector", "Pengesan Remote"); + dictionary.Add("ChipIdentificationCopy", "Salinan ID Cip"); + dictionary.Add("ChipGeneration", "Penjanaan Cip"); + dictionary.Add("AccessCardCopy", "Salinan Kad Akses"); + dictionary.Add("KeyUnlock", "Nyahkunci Kunci"); + dictionary.Add("TirePressure", "Fungsi Tekanan Tayar"); + dictionary.Add("MatchingFunction", "Fungsi Pemadanan"); + dictionary.Add("ChipWriteStart", "Mula Menulis Cip"); + dictionary.Add("Programmer", "Pemprogram"); + dictionary.Add("AntiTheftData", "Data Anti-Kecurian"); + dictionary.Add("UpdateCenter", "Pusat Kemas Kini"); + + dictionary.Add("ContactWechatCopy", "ID WeChat disalin"); + dictionary.Add("ContactEmailCopy", "Alamat e-mel disalin"); + + dictionary.Add("Tips", "Petua"); + dictionary.Add("Connected", "Bersambung"); + dictionary.Add("Disconnected", "Terputus"); + dictionary.Add("DeviceDisconnected", "Peranti tidak bersambung"); + dictionary.Add("K3ToolConnectTips", "Sila sambungkan peranti K3Tool sebelum mengemas kini"); + dictionary.Add("FileNotFound", "Fail tidak dijumpai"); + + dictionary.Add("UpToDate", "Sudah terkini"); + dictionary.Add("PleaseNote", "Sila ambil perhatian"); + dictionary.Add("PleasePress", "Sila tekan"); + dictionary.Add("Button", "butang"); + dictionary.Add("NoKeyData", "Tiada data kunci tersedia"); + dictionary.Add("Lock", "Kunci"); + dictionary.Add("Unlock", "Buka Kunci"); + dictionary.Add("Trunk", "But"); + dictionary.Add("Panic", "Panic"); + dictionary.Add("BurnTips", "Fail pengaturcaraan tidak wujud. Sila pilih model kunci yang perlu diprogram."); + dictionary.Add("BurnCSubKeyMobileAppTips", "Sila gunakan aplikasi mudah alih untuk memprogram subkunci C."); + dictionary.Add("BurnCFSubKeyMobileAppTips", "Sila gunakan aplikasi mudah alih untuk memprogram subkunci CF."); + dictionary.Add("BurnSuccess", "Pengaturcaraan berjaya"); + dictionary.Add("BurnError", "Pengaturcaraan gagal"); + dictionary.Add("NotMoreTips", "Tiada lagi data~"); + dictionary.Add("CloudData", "Data Awan"); + dictionary.Add("CloudDataSyncCompleted", "Penyegerakan selesai"); + dictionary.Add("CloudDataSyncFailed", "Penyegerakan gagal"); + dictionary.Add("CloudDataDownloadFailed", "Muat turun gagal"); + dictionary.Add("CloudDataDeviceNotFound", "Peranti storan K3Tool tidak dikesan"); + dictionary.Add("CloudDataInitCompleted", "Penyediaan K3Tool selesai"); + dictionary.Add("HidSendPacketError", "Ralat semasa menghantar paket data"); + dictionary.Add("DialogPleaseWait", "Sila tunggu"); + dictionary.Add("DialogProcessing", "Sedang diproses..."); + dictionary.Add("DialogCancelled", "Dibatalkan"); + dictionary.Add("DialogErrorPrefix", "Ralat"); + dictionary.Add("DialogElapsedPrefix", "Masa"); + dictionary.Add("DialogBurningTitle", "Pengaturcaraan"); + dictionary.Add("DialogBurningMessage", "Sedang memprogram..."); + dictionary.Add("RechargeMinAmountInvalid", "Recharge amount cannot be less than 0.01"); + dictionary.Add("RechargeLoginRequired", "Please log in before recharging"); + dictionary.Add("RechargeCreatingOrder", "Creating payment order..."); + dictionary.Add("RechargePcQrCodeMissing", "The backend did not return a PC QR payment code"); + dictionary.Add("RechargeWechatScanToPay", "Please scan with WeChat to pay"); + dictionary.Add("RechargePaypalParametersMissing", "The backend did not return PayPal payment parameters"); + dictionary.Add("RechargeOpeningPaypal", "Opening PayPal payment page..."); + dictionary.Add("RechargeCreateOrderFailed", "Failed to create payment order, please try again later"); + + + + + return dictionary; + + #endregion + } + + + + private static Dictionary ellanguage() + { + #region 希腊语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Αρχική σελίδα"); + dictionary.Add("Settings", "Ρυθμίσεις συστήματος"); + dictionary.Add("RemoteGeneration", "Δημιουργία τηλεχειριστηρίου"); + dictionary.Add("RemoteCopy", "Αντιγραφή τηλεχειριστηρίου"); + dictionary.Add("RemoteControlDetector", "Ανιχνευτής τηλεχειριστηρίων"); + dictionary.Add("ChipIdentificationCopy", "Αντιγραφή αναγνωριστικού τσιπ"); + dictionary.Add("ChipGeneration", "Δημιουργία τσιπ"); + dictionary.Add("AccessCardCopy", "Αντιγραφή κάρτας πρόσβασης"); + dictionary.Add("KeyUnlock", "Ξεκλείδωμα κλειδιού"); + dictionary.Add("TirePressure", "Λειτουργίες πίεσης ελαστικών"); + dictionary.Add("MatchingFunction", "Λειτουργίες ταύτισης"); + dictionary.Add("ChipWriteStart", "Έναρξη εγγραφής τσιπ"); + dictionary.Add("Programmer", "Προγραμματιστής"); + dictionary.Add("AntiTheftData", "Δεδομένα αντικλεπτικού"); + dictionary.Add("UpdateCenter", "Κέντρο ενημερώσεων"); + + dictionary.Add("ContactWechatCopy", "Το ID WeChat αντιγράφηκε"); + dictionary.Add("ContactEmailCopy", "Η διεύθυνση email αντιγράφηκε"); + + dictionary.Add("Tips", "Συμβουλή"); + dictionary.Add("Connected", "Συνδέθηκε"); + dictionary.Add("Disconnected", "Αποσυνδέθηκε"); + dictionary.Add("DeviceDisconnected", "Η συσκευή δεν είναι συνδεδεμένη"); + dictionary.Add("K3ToolConnectTips", "Συνδέστε τη συσκευή K3Tool πριν την ενημέρωση"); + dictionary.Add("FileNotFound", "Το αρχείο δεν βρέθηκε"); + + dictionary.Add("UpToDate", "Ήδη ενημερωμένο"); + dictionary.Add("PleaseNote", "Παρακαλούμε σημειώστε"); + dictionary.Add("CloudDataSyncCompleted", "Ο συγχρονισμός ολοκληρώθηκε"); + dictionary.Add("CloudDataSyncFailed", "Ο συγχρονισμός απέτυχε"); + dictionary.Add("CloudDataDownloadFailed", "Η λήψη απέτυχε"); + dictionary.Add("CloudDataDeviceNotFound", "Η συσκευή αποθήκευσης K3Tool δεν εντοπίστηκε"); + dictionary.Add("CloudDataInitCompleted", "Η αρχικοποίηση του K3Tool ολοκληρώθηκε"); + dictionary.Add("HidSendPacketError", "Παρουσιάστηκε σφάλμα κατά την αποστολή πακέτου δεδομένων"); + dictionary.Add("DialogPleaseWait", "Παρακαλώ περιμένετε"); + dictionary.Add("DialogProcessing", "Σε επεξεργασία..."); + dictionary.Add("DialogCancelled", "Ακυρώθηκε"); + dictionary.Add("DialogErrorPrefix", "Σφάλμα"); + dictionary.Add("DialogElapsedPrefix", "Χρόνος"); + dictionary.Add("DialogBurningTitle", "Προγραμματισμός"); + dictionary.Add("DialogBurningMessage", "Γίνεται προγραμματισμός..."); + + + + + + return dictionary; + + #endregion + } + private static Dictionary hulanguage() + { + #region 匈牙利语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "Kezdőlap"); + dictionary.Add("Settings", "Rendszerbeállítások"); + dictionary.Add("RemoteGeneration", "Távirányító generálása"); + dictionary.Add("RemoteCopy", "Távirányító másolása"); + dictionary.Add("RemoteControlDetector", "Távirányító érzékelő"); + dictionary.Add("ChipIdentificationCopy", "Chip azonosító másolása"); + dictionary.Add("ChipGeneration", "Chip generálása"); + dictionary.Add("AccessCardCopy", "Hozzáférési kártya másolása"); + dictionary.Add("KeyUnlock", "Kulcs feloldása"); + dictionary.Add("TirePressure", "Gumiabroncs-nyomás funkciók"); + dictionary.Add("MatchingFunction", "Párosítási funkciók"); + dictionary.Add("ChipWriteStart", "Chip írásának indítása"); + dictionary.Add("Programmer", "Programozó"); + dictionary.Add("AntiTheftData", "Lopásgátló adatok"); + dictionary.Add("UpdateCenter", "Frissítési központ"); + + dictionary.Add("ContactWechatCopy", "WeChat-azonosító másolva"); + dictionary.Add("ContactEmailCopy", "E-mail cím másolva"); + + dictionary.Add("Tips", "Tipp"); + dictionary.Add("Connected", "Csatlakozva"); + dictionary.Add("Disconnected", "Leválasztva"); + dictionary.Add("DeviceDisconnected", "Az eszköz nincs csatlakoztatva"); + dictionary.Add("K3ToolConnectTips", "Kérjük, csatlakoztassa a K3Tool eszközt a frissítés előtt"); + dictionary.Add("FileNotFound", "Fájl nem található"); + + dictionary.Add("UpToDate", "Már naprakész"); + dictionary.Add("PleaseNote", "Kérjük, vegye figyelembe"); + dictionary.Add("CloudDataSyncCompleted", "Szinkronizálás befejeződött"); + dictionary.Add("CloudDataSyncFailed", "Szinkronizálás sikertelen"); + dictionary.Add("CloudDataDownloadFailed", "A letöltés sikertelen"); + dictionary.Add("CloudDataDeviceNotFound", "A K3Tool tárolóeszköz nem található"); + dictionary.Add("CloudDataInitCompleted", "A K3Tool inicializálása befejeződött"); + dictionary.Add("HidSendPacketError", "Hiba történt az adatcsomag elküldésekor"); + dictionary.Add("DialogPleaseWait", "Kérjük, várjon"); + dictionary.Add("DialogProcessing", "Feldolgozás..."); + dictionary.Add("DialogCancelled", "Megszakítva"); + dictionary.Add("DialogErrorPrefix", "Hiba"); + dictionary.Add("DialogElapsedPrefix", "Idő"); + dictionary.Add("DialogBurningTitle", "Programozás"); + dictionary.Add("DialogBurningMessage", "Programozás folyamatban..."); + + + + + + return dictionary; + + #endregion + } + private static Dictionary arlanguage() + { + #region 阿拉伯语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "الصفحة الرئيسية"); + dictionary.Add("Settings", "إعدادات النظام"); + dictionary.Add("RemoteGeneration", "إنشاء جهاز تحكم عن بعد"); + dictionary.Add("RemoteCopy", "نسخ جهاز التحكم عن بعد"); + dictionary.Add("RemoteControlDetector", "كاشف أجهزة التحكم"); + dictionary.Add("ChipIdentificationCopy", "نسخ معرف الشريحة"); + dictionary.Add("ChipGeneration", "إنشاء شريحة"); + dictionary.Add("AccessCardCopy", "نسخ بطاقة الوصول"); + dictionary.Add("KeyUnlock", "فتح قفل المفتاح"); + dictionary.Add("TirePressure", "وظائف ضغط الإطارات"); + dictionary.Add("MatchingFunction", "وظائف المطابقة"); + dictionary.Add("ChipWriteStart", "بدء كتابة الشريحة"); + dictionary.Add("Programmer", "المبرمج"); + dictionary.Add("AntiTheftData", "بيانات منع السرقة"); + dictionary.Add("UpdateCenter", "مركز التحديث"); + + dictionary.Add("ContactWechatCopy", "تم نسخ معرف وي تشات"); + dictionary.Add("ContactEmailCopy", "تم نسخ عنوان البريد الإلكتروني"); + + dictionary.Add("Tips", "تلميح"); + dictionary.Add("Connected", "متصل"); + dictionary.Add("Disconnected", "غير متصل"); + dictionary.Add("DeviceDisconnected", "الجهاز غير متصل"); + dictionary.Add("K3ToolConnectTips", "يرجى توصيل جهاز K3Tool قبل التحديث"); + dictionary.Add("FileNotFound", "الملف غير موجود"); + + dictionary.Add("UpToDate", "محدث بالفعل"); + dictionary.Add("PleaseNote", "يرجى الملاحظة"); + dictionary.Add("CloudDataSyncCompleted", "اكتملت المزامنة"); + dictionary.Add("CloudDataSyncFailed", "فشلت المزامنة"); + dictionary.Add("CloudDataDownloadFailed", "فشل التنزيل"); + dictionary.Add("CloudDataDeviceNotFound", "لم يتم اكتشاف جهاز تخزين K3Tool"); + dictionary.Add("CloudDataInitCompleted", "اكتمل تهيئة K3Tool"); + dictionary.Add("HidSendPacketError", "حدث خطأ أثناء إرسال حزمة البيانات"); + dictionary.Add("DialogPleaseWait", "يرجى الانتظار"); + dictionary.Add("DialogProcessing", "جارٍ المعالجة..."); + dictionary.Add("DialogCancelled", "تم الإلغاء"); + dictionary.Add("DialogErrorPrefix", "خطأ"); + dictionary.Add("DialogElapsedPrefix", "الوقت"); + dictionary.Add("DialogBurningTitle", "البرمجة"); + dictionary.Add("DialogBurningMessage", "جارٍ البرمجة..."); + + + + + + return dictionary; + + #endregion + } + private static Dictionary jalanguage() + { + #region 日语 + + Dictionary dictionary = new Dictionary(); + + dictionary.Add("HomePage", "ホーム"); + dictionary.Add("Settings", "システム設定"); + dictionary.Add("RemoteGeneration", "リモコン生成"); + dictionary.Add("RemoteCopy", "リモコンコピー"); + dictionary.Add("RemoteControlDetector", "リモコン検出器"); + dictionary.Add("ChipIdentificationCopy", "チップIDコピー"); + dictionary.Add("ChipGeneration", "チップ生成"); + dictionary.Add("AccessCardCopy", "アクセスカードコピー"); + dictionary.Add("KeyUnlock", "キーアンロック"); + dictionary.Add("TirePressure", "タイヤ空気圧機能"); + dictionary.Add("MatchingFunction", "マッチング機能"); + dictionary.Add("ChipWriteStart", "チップ書き込み開始"); + dictionary.Add("Programmer", "プログラマー"); + dictionary.Add("AntiTheftData", "盗難防止データ"); + dictionary.Add("UpdateCenter", "アップデートセンター"); + + dictionary.Add("ContactWechatCopy", "WeChat IDをコピーしました"); + dictionary.Add("ContactEmailCopy", "メールアドレスをコピーしました"); + + dictionary.Add("Tips", "ヒント"); + dictionary.Add("Connected", "接続済み"); + dictionary.Add("Disconnected", "未接続"); + dictionary.Add("DeviceDisconnected", "デバイスが接続されていません"); + dictionary.Add("K3ToolConnectTips", "アップデート前にK3Toolデバイスを接続してください"); + dictionary.Add("FileNotFound", "ファイルが見つかりません"); + + dictionary.Add("UpToDate", "最新バージョンです"); + dictionary.Add("PleaseNote", "ご注意ください"); + dictionary.Add("CloudDataSyncCompleted", "同期完了"); + dictionary.Add("CloudDataSyncFailed", "同期失敗"); + dictionary.Add("CloudDataDownloadFailed", "ダウンロード失敗"); + dictionary.Add("CloudDataDeviceNotFound", "K3Tool ストレージデバイスが検出されませんでした"); + dictionary.Add("CloudDataInitCompleted", "K3Tool の初期化が完了しました"); + dictionary.Add("HidSendPacketError", "データパケットの送信中にエラーが発生しました"); + dictionary.Add("DialogPleaseWait", "しばらくお待ちください"); + dictionary.Add("DialogProcessing", "処理中..."); + dictionary.Add("DialogCancelled", "キャンセルされました"); + dictionary.Add("DialogErrorPrefix", "エラー"); + dictionary.Add("DialogElapsedPrefix", "時間"); + dictionary.Add("DialogBurningTitle", "書き込み"); + dictionary.Add("DialogBurningMessage", "書き込み中..."); + + + + + + return dictionary; + + #endregion + } + } +} diff --git a/AIK.Common/Language/UserPreferences.cs b/AIK.Common/Language/UserPreferences.cs new file mode 100644 index 0000000..713d6fe --- /dev/null +++ b/AIK.Common/Language/UserPreferences.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Language +{ + public class UserPreferences + { + public string Language { get; set; } = "zh"; + + /// + /// 设备唯一标识(UUID V4,卸载重装保持不变)。 + /// 用于 AI 客服 JSBridge 的 sid 参数。 + /// + public string DeviceId { get; set; } = string.Empty; + + // 可扩展其他设置:Theme, WindowSize 等 + } +} diff --git a/AIK.Common/Models/UpdateInfoResult.cs b/AIK.Common/Models/UpdateInfoResult.cs new file mode 100644 index 0000000..4ced678 --- /dev/null +++ b/AIK.Common/Models/UpdateInfoResult.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Models +{ + /// + /// 自定义更新界面使用类 + /// + public class UpdateInfoResult + { + public bool IsUpdateAvailable { get; set; } + public string LatestVersion { get; set; } + public string CurrentVersion { get; set; } + public string DownloadUrl { get; set; } + public string ChangeLog { get; set; } + public bool IsMandatory { get; set; } + + public string VersionEnContent { get; set; } + + public string VersionContent { get; set; } + + } +} diff --git a/AIK.Common/Models/UpdateProgressDto.cs b/AIK.Common/Models/UpdateProgressDto.cs new file mode 100644 index 0000000..b1054f8 --- /dev/null +++ b/AIK.Common/Models/UpdateProgressDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.Models +{ + /// + /// 更新进度传输对象(跨程序集传递,无第三方依赖)(AutoUpdater.Net 不暴露下载进度,暂无进度条) + /// + public class UpdateProgressDto + { + public bool IsAvailable { get; set; } + public string? CurrentVersion { get; set; } + public string? LatestVersion { get; set; } + public string? Status { get; set; } + public bool HasError { get; set; } + public string? ErrorMessage { get; set; } + + public string? DownloadUrl { get; set; } + } +} diff --git a/AIK.Common/Properties/Settings.Designer.cs b/AIK.Common/Properties/Settings.Designer.cs new file mode 100644 index 0000000..5eaa644 --- /dev/null +++ b/AIK.Common/Properties/Settings.Designer.cs @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace AIK.Common.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")] + public sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("zh")] + public string Language { + get { + return ((string)(this["Language"])); + } + set { + this["Language"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string DeviceId { + get { + return ((string)(this["DeviceId"])); + } + set { + this["DeviceId"] = value; + } + } + } +} diff --git a/AIK.Common/Properties/Settings.settings b/AIK.Common/Properties/Settings.settings new file mode 100644 index 0000000..aa7c499 --- /dev/null +++ b/AIK.Common/Properties/Settings.settings @@ -0,0 +1,12 @@ + + + + + + zh + + + + + + \ No newline at end of file diff --git a/AIK.Common/Resources/Close the U disk.bin b/AIK.Common/Resources/Close the U disk.bin new file mode 100644 index 0000000..581fee0 Binary files /dev/null and b/AIK.Common/Resources/Close the U disk.bin differ diff --git a/AIK.Common/Resources/Open the U disk.bin b/AIK.Common/Resources/Open the U disk.bin new file mode 100644 index 0000000..268d354 Binary files /dev/null and b/AIK.Common/Resources/Open the U disk.bin differ diff --git a/AIK.Common/Resources/eot.bin b/AIK.Common/Resources/eot.bin new file mode 100644 index 0000000..599c7cc Binary files /dev/null and b/AIK.Common/Resources/eot.bin differ diff --git a/AIK.Common/Resources/usb connect.bin b/AIK.Common/Resources/usb connect.bin new file mode 100644 index 0000000..5a2ecc3 Binary files /dev/null and b/AIK.Common/Resources/usb connect.bin differ diff --git a/AIK.Common/Resources/usb create remote.bin b/AIK.Common/Resources/usb create remote.bin new file mode 100644 index 0000000..a6e7db8 Binary files /dev/null and b/AIK.Common/Resources/usb create remote.bin differ diff --git a/AIK.Common/StompNet/StompFrameHelper.cs b/AIK.Common/StompNet/StompFrameHelper.cs new file mode 100644 index 0000000..95d1198 --- /dev/null +++ b/AIK.Common/StompNet/StompFrameHelper.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.StompNet +{ + /// + /// STOMP帧结构 + /// + public static class StompFrameHelper + { + private static readonly byte[] NullByte = { 0 }; + + public static byte[] BuildFrame(string command, Dictionary? headers = null, string? body = null) + { + var sb = new StringBuilder(); + sb.Append(command).Append('\n'); + + if (headers != null) + { + foreach (var header in headers) + { + sb.Append($"{header.Key}:{header.Value}").Append("\n"); + } + } + + sb.Append("\n"); // 空行 + sb.Append("\u0000"); // 结束符 + var headerBytes = Encoding.UTF8.GetBytes(sb.ToString()); + + return headerBytes; + } + + public static (string command, Dictionary headers, string body)? ParseFrame(byte[] data) + { + if (data.Length == 0 || data[data.Length - 1] != 0) return null; + + var text = Encoding.UTF8.GetString(data, 0, data.Length - 1); // 去掉 + var parts = text.Split(new[] { "\r\n\r\n", "\n\n" }, 2, StringSplitOptions.None); + + if (parts.Length < 1) return null; + + var headerSection = parts[0]; + var body = parts.Length > 1 ? parts[1] : ""; + + var lines = headerSection.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length == 0) return null; + + var command = lines[0].Trim(); + var headers = new Dictionary(); + + for (int i = 1; i < lines.Length; i++) + { + var line = lines[i]; + var idx = line.IndexOf(':'); + if (idx > 0) + { + var key = line.Substring(0, idx).Trim(); + var value = line.Substring(idx + 1).Trim(); + headers[key] = value; + } + } + + return (command, headers, body); + } + } +} diff --git a/AIK.Common/SysCommon/ComStringHelper.cs b/AIK.Common/SysCommon/ComStringHelper.cs new file mode 100644 index 0000000..ca0fd82 --- /dev/null +++ b/AIK.Common/SysCommon/ComStringHelper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.SysCommon +{ + public class ComStringHelper + { + public static string K3ToolDirectory = "received_data"; + } +} diff --git a/AIK.Common/SysCommon/MacAddressHelper.cs b/AIK.Common/SysCommon/MacAddressHelper.cs new file mode 100644 index 0000000..c21159b --- /dev/null +++ b/AIK.Common/SysCommon/MacAddressHelper.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.SysCommon +{ + public static class MacAddressHelper + { + /// + /// 获取主物理网卡的 MAC 地址(格式:00-1A-2B-3C-4D-5E) + /// + public static string GetPrimaryMacAddress() + { + try + { + var activeAdapters = NetworkInterface + .GetAllNetworkInterfaces() + .Where(nic => + nic.OperationalStatus == OperationalStatus.Up && + nic.NetworkInterfaceType is NetworkInterfaceType.Ethernet or NetworkInterfaceType.Wireless80211 && + !IsVirtualOrLoopback(nic)) + .OrderByDescending(nic => nic.Speed) // 优先有线 > 无线 + .ToList(); + + var primary = activeAdapters.FirstOrDefault() + ?? NetworkInterface.GetAllNetworkInterfaces() + .FirstOrDefault(nic => !IsVirtualOrLoopback(nic)); + + if (primary?.GetPhysicalAddress().GetAddressBytes().Length > 0 is true) + { + return FormatMacAddress(primary.GetPhysicalAddress().GetAddressBytes()); + } + + return "未检测到有效物理网卡"; + } + catch (Exception ex) + { + return $"获取失败: {ex.Message}"; + } + } + + private static bool IsVirtualOrLoopback(NetworkInterface nic) + { + if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback) + return true; + + string desc = nic.Description.ToLowerInvariant(); + return desc.Contains("virtual") || + desc.Contains("vmware") || + desc.Contains("vbox") || + desc.Contains("hyper-v") || + desc.Contains("bluetooth") || + desc.Contains("tunnel") || + desc.Contains("isatap") || + desc.Contains("6to4") || + desc.Contains("teredo"); + } + + private static string FormatMacAddress(ReadOnlySpan macBytes) + { + // 使用 Span 高效构建格式化字符串(.NET 8 推荐) + var result = new StringBuilder(macBytes.Length * 3 - 1); + for (int i = 0; i < macBytes.Length; i++) + { + if (i > 0) result.Append('-'); + result.Append($"{macBytes[i]:X2}"); + } + return result.ToString(); + } + } +} diff --git a/AIK.Common/SysCommon/UserCommon/UserTokenHelper.cs b/AIK.Common/SysCommon/UserCommon/UserTokenHelper.cs new file mode 100644 index 0000000..6e316f6 --- /dev/null +++ b/AIK.Common/SysCommon/UserCommon/UserTokenHelper.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace AIK.Common.SysCommon.UserCommon +{ + public static class UserTokenHelper + { + public static (bool Success, long? UserId, long? Exp) ParseJwtPayload(string jwt) + { + try + { + var parts = jwt.Split('.'); + if (parts.Length != 3) return (false, null, null); + + var payloadJson = Base64UrlDecode(parts[1]); + using var doc = JsonDocument.Parse(payloadJson); + var root = doc.RootElement; + + long? userId = root.TryGetProperty("userId", out var uid) ? uid.GetInt64() : null; + long? exp = root.TryGetProperty("exp", out var e) ? e.GetInt64() : null; + + return (true, userId, exp); + } + catch + { + return (false, null, null); + } + } + + private static string Base64UrlDecode(string input) + { + // 补齐 padding + string padded = input.Replace('-', '+').Replace('_', '/'); + switch (padded.Length % 4) + { + case 2: padded += "=="; break; + case 3: padded += "="; break; + } + byte[] data = Convert.FromBase64String(padded); + return System.Text.Encoding.UTF8.GetString(data); + } + } +} diff --git a/AIK.Common/SysCommon/WebApiAddress.cs b/AIK.Common/SysCommon/WebApiAddress.cs new file mode 100644 index 0000000..da85f2f --- /dev/null +++ b/AIK.Common/SysCommon/WebApiAddress.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.SysCommon +{ + /// + /// 自带/前缀的WebApi地址 + /// + public static class WebApiAddress + { + public static string Vehicle_keyBrand_List = "/prod-api/work_key/vehicle_key/brand_list"; //获取品牌列表 + public static string Vehicle_keyBrand_Detail = "/prod-api/work_key/vehicle_key/brand_detail"; //获取品牌详情 + public static string CommonProfile = "/prod-api/work_main/common/profile"; //获取用户信息 + public static string AccountLogin = "/prod-api/work_main/login";//账密登录 + public static string PhoneRegister = "/prod-api/work_main/user/phone_register";//手机号注册 + public static string EmailRegister = "/prod-api/work_main/user/user/email_register";//邮箱注册(路径按接口文档原样,含双 user) + + #region 短信/邮箱服务(work_sms) + public static string SendCode = "/prod-api/work_sms/sms/send_code";//发送注册验证码(GET,参数 phone) + public static string SendLoginCode = "/prod-api/work_sms/sms/send_login";//发送登录验证码(GET,参数 phone) + public static string CheckCode = "/prod-api/work_sms/sms/check_code";//验证验证码(GET,参数 code/phone/type) + + // 邮箱服务(GET,成功码 code=1) + public static string EmailSendCode = "/prod-api/work_sms/email/send_code";//发送邮箱验证码(参数 address/type) + public static string EmailCheckCode = "/prod-api/work_sms/email/check_code";//验证邮箱验证码(参数 address/code) + #endregion + public static string BannerAll = "/prod-api/work_key/banner/all"; //获取首页轮播图 + public static string AppNew = "/prod-api/work_key/app/new"; //获取最新版本信息 + public static string LogList = "/prod-api/work_key/d_info/log_list";//烧录记录 + public static string Integral = "/prod-api/work_key/integral/get";//用户积分 + public static string IntegralHistroy = "/prod-api/work_key/integral/get_history";//积分记录 + public static string KeyAddInfo = "/prod-api/work_key/d_info/add_info";//用户添加烧录记录 + public static string BurnAdd = "/prod-api/work_key/burn/add";//用户添加烧录次数 + + #region 会员功能与支付积分 + public static string MembershipModuleList = "/prod-api/work_key/func/module_list"; + public static string MembershipFind = "/prod-api/work_key/func/find"; + public static string MembershipOpen = "/prod-api/work_key/func/open"; + public static string MembershipBind = "/prod-api/work_key/func/bind"; + public static string MembershipCheck = "/prod-api/work_key/func/check"; + public static string MembershipActivate = "/prod-api/work_key/func/act"; + public static string MembershipFindCount = "/prod-api/work_key/func/find_count"; + public static string MembershipAddCount = "/prod-api/work_key/func/add_count"; + public static string PayIntegralOrder = "/prod-api/work_app_mall/info/pay_integral_order"; + public static string OrderPayStatus = "/prod-api/work_app_mall/info/order_pay_status"; + public static string OrderPaypal = "/prod-api/work_key/info/order_paypal"; + #endregion + + public static string OssDataCache = "/prod-api/work_main/oss_data/cache/type";//K3tool云数据缓存地址 + + public static string TpmsType = "/prod-api/work_key/tpms_type/type";//获取胎压类型 + + #region 工具类型(用于版本更新) + public static string K3ToolType = "i2"; + public static string PCType = "pcwin7"; + public static string PCZipType = "pcwin7_zip"; + #endregion + + #region oss数据缓存类型 + public static string K3ToolBin = "K3tool_bin"; + #endregion + } +} diff --git a/AIK.Common/WebHelper/UrlHelper.cs b/AIK.Common/WebHelper/UrlHelper.cs new file mode 100644 index 0000000..bde2159 --- /dev/null +++ b/AIK.Common/WebHelper/UrlHelper.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Common.WebHelper +{ + public static class UrlHelper + { + /// + /// 从完整URL中提取文件名(保留URL编码,如 %28 不会转成 () + /// + /// 完整的下载链接 + /// URL编码的文件名,如 "20251126055829_721_%28K3_TOOL%291.2.9.bin" + public static string GetEncodedFileNameFromUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + return string.Empty; + try + { + var uri = new Uri(url); + // Segments 是路径按 '/' 分割的数组,最后一个元素即为文件名(含原始编码) + string[] segments = uri.Segments; + if (segments.Length == 0) + return string.Empty; + + string fileName = segments[segments.Length - 1]; + // 移除末尾可能的斜杠(虽然文件名通常不会有) + return fileName.TrimEnd('/'); + } + catch (UriFormatException) + { + // URL 格式无效时返回空或抛异常,根据需求调整 + return string.Empty; + } + } + } +} diff --git a/AIK.Common/WebHelper/WebHelper.cs b/AIK.Common/WebHelper/WebHelper.cs new file mode 100644 index 0000000..2625755 --- /dev/null +++ b/AIK.Common/WebHelper/WebHelper.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Common.WebHelper +{ + public class WebHelper + { + + //private static string weburlTest = "https://keytest.aik518.com"; + ////public static string fileurl = "https://keytest.aik518.com/prod-api/work_file/file/file_download?objectName="; + ////接口地址 + //private static string weburl = " https://key.aik518.com"; + ////文件下载地址 + //public static string fileurl = " https://key.aik518.com/prod-api/work_file/file/file_download?objectName="; + ////当前平台语言 + //public static string language = "zh"; + ////当前平台升级地址 + //private static string updateurl = "http://124.221.137.201:8168/update.zip"; + + //public static System.Collections.Hashtable ImgListKey = System.Collections.Hashtable.Synchronized(new System.Collections.Hashtable()); + + + + #region HttpWebRequest(请求网络资源) + + /// + /// 请求网络资源,返回响应的文本 + /// + /// 网络资源地址 + public static string HttpWebRequest(string url, string parameters, int timeout, bool isPost) + { + return HttpWebRequest(url, parameters, Encoding.GetEncoding("utf-8"), timeout, isPost); + } + + /// + /// 请求网络资源,返回响应的文本 + /// + /// 网络资源地址 + /// 提交的参数,格式:参数1=参数值1&参数2=参数值2 + /// 字符编码 + /// 是否Post提交 + /// 内容类型 + /// Cookie容器 + /// 超时时间 + public static string HttpWebRequest(string url, string parameters, Encoding encoding, int timeout, bool isPost = false, + string contentType = "application/x-www-form-urlencoded", CookieContainer cookie = null) + { + var request = (HttpWebRequest)WebRequest.Create(url); + request.Timeout = timeout; + request.CookieContainer = cookie; + if (isPost) + { + byte[] postData = encoding.GetBytes(parameters); + request.Method = "POST"; + request.ContentType = contentType; + request.ContentLength = postData.Length; + using (Stream stream = request.GetRequestStream()) + { + stream.Write(postData, 0, postData.Length); + } + } + var response = (HttpWebResponse)request.GetResponse(); + string result; + using (Stream stream = response.GetResponseStream()) + { + if (stream == null) + return string.Empty; + using (var reader = new StreamReader(stream, encoding)) + { + result = reader.ReadToEnd(); + } + } + return result; + } + + #endregion + + #region Post json请求 + public static string PostJson(string Url, string jsonParas) + { + string strURL = Url; + //创建一个HTTP请求 + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(strURL); + //Post请求方式 + request.Method = "POST"; + //内容类型 + request.ContentType = "application/json"; + //设置参数,并进行URL编码 + string paraUrlCoded = jsonParas;//System.Web.HttpUtility.UrlEncode(jsonParas); + byte[] payload; + //将Json字符串转化为字节 + payload = System.Text.Encoding.UTF8.GetBytes(paraUrlCoded); + //设置请求的ContentLength + request.ContentLength = payload.Length; + //发送请求,获得请求流 + Stream writer; + try + { + using (writer = request.GetRequestStream()) + { + //将请求参数写入流 + writer.Write(payload, 0, payload.Length); + writer.Close();//关闭请求流 + } + ;//获取用于写入请求数据的Stream对象 + } + catch (Exception ex) + { + writer = null; + } + + HttpWebResponse response; + string postContent = ""; + try + { + //获得响应流 + using (response = (HttpWebResponse)request.GetResponse()) + { + Stream s = response.GetResponseStream(); + // Stream postData = Request.InputStream; + StreamReader sRead = new StreamReader(s); + postContent = sRead.ReadToEnd(); + sRead.Close(); + } + ; + } + catch (WebException ex) + { + response = ex.Response as HttpWebResponse; + } + return postContent;//返回Json数据 + } + #endregion + + #region 异步请求 + /// + /// 异步POST请求 + /// + public static async Task PostJsonAsync(string url, string jsonData, CancellationToken cancellationToken = default) + { + using var client = new HttpClient(); + using var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); + using var response = await client.PostAsync(url, content, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// 异步HTTP请求 + /// + public static async Task HttpWebRequestAsync(string url, string data, int timeoutMs, bool isPost, CancellationToken cancellationToken = default) + { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromMilliseconds(timeoutMs); + + if (isPost && !string.IsNullOrEmpty(data)) + { + using var content = new StringContent(data, Encoding.UTF8, "application/json"); + using var response = await client.PostAsync(url, content, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + else + { + using var response = await client.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } + #endregion + + } +} diff --git a/AIK.DataAccess/AIK.DataAccess.csproj b/AIK.DataAccess/AIK.DataAccess.csproj new file mode 100644 index 0000000..a796bd4 --- /dev/null +++ b/AIK.DataAccess/AIK.DataAccess.csproj @@ -0,0 +1,23 @@ + + + + net48 + enable + true + 12 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.DataAccess/DbCore.cs b/AIK.DataAccess/DbCore.cs new file mode 100644 index 0000000..63d719e --- /dev/null +++ b/AIK.DataAccess/DbCore.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.DataAccess +{ + public static class DbCore + { + public const string CacheKey = "veh_car_data"; + public const string CacheModelKey = "veh_carmodel_data"; + public const string CacheVehModelKey = $"vehModel"; + } +} diff --git a/AIK.DataAccess/SQLiteCore.cs b/AIK.DataAccess/SQLiteCore.cs new file mode 100644 index 0000000..da084c3 --- /dev/null +++ b/AIK.DataAccess/SQLiteCore.cs @@ -0,0 +1,194 @@ +using System.Data.SQLite; +using System; +using System.Threading; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.DataAccess +{ + public static class SQLiteCore + { + private static string _databasePath; + private static bool _isInitialized = false; + private static readonly object _initLock = new object(); + // 连接池(线程安全队列),最大连接数控制为 5(SQLite 推荐) + private static readonly ConcurrentQueue _connectionPool = new ConcurrentQueue(); + private const int MaxConnections = 5; + private static int _activeConnections = 0; + /// + /// 初始化 SQLitePCL 和数据库路径 + /// + private static void Initialize() + { + lock (_initLock) + { + if (!_isInitialized) + { + InitializeDatabasePath(); + _isInitialized = true; + } + } + } + + /// + /// 获取池化连接(返回包装类,支持 using 自动归还) + /// + public static SqlitePooledConnection GetConnection() + { + Initialize(); + + // 尝试从池子里取空闲连接 + if (_connectionPool.TryDequeue(out var connection) && connection.State == System.Data.ConnectionState.Open) + { + Interlocked.Decrement(ref _activeConnections); + return new SqlitePooledConnection(connection); // 用包装类包裹 + } + + // 池子里没有空闲连接,创建新连接 + if (Interlocked.Increment(ref _activeConnections) > MaxConnections) + { + Interlocked.Decrement(ref _activeConnections); + throw new InvalidOperationException("SQLite 连接池已达最大连接数,建议优化事务执行速度或调整 MaxConnections"); + } + + var newConn = new SQLiteConnection($"Data Source={_databasePath}"); + newConn.Open(); + using (var cmd = newConn.CreateCommand()) + { + cmd.CommandText = @" + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA foreign_keys = ON; + PRAGMA busy_timeout = 3000; + "; + cmd.ExecuteNonQuery(); + } + + return new SqlitePooledConnection(newConn); // 用包装类包裹 + } + + + /// + /// 将连接归还到连接池(避免重复创建) + /// + public static void ReturnConnection(SQLiteConnection connection) + { + try + { + if (connection == null || connection.State != System.Data.ConnectionState.Open) + { + connection?.Dispose(); + Interlocked.Decrement(ref _activeConnections); + return; + } + + // 归还到池子里(供下次复用) + _connectionPool.Enqueue(connection); + Interlocked.Decrement(ref _activeConnections); + } + catch (Exception) + { + + } + + } + + /// + /// 释放所有连接(程序退出时调用) + /// + public static void DisposePool() + { + while (_connectionPool.TryDequeue(out var connection)) + { + connection.Dispose(); + } + _activeConnections = 0; + } + #region 单例模式 + /// + /// 创建配置好的数据库连接 + /// + //public static SQLiteConnection CreateConnection() + //{ + // InitializeSQLitePCL(); + // // 数据库文件路径 + // _databasePath = DatabasePath; + + // // 确保目录存在 + // Directory.CreateDirectory(Path.GetDirectoryName(_databasePath)); + // var connection = new SQLiteConnection($"Data Source={_databasePath}"); + // connection.Open(); + + // // 基本优化设置 + // using (var cmd = connection.CreateCommand()) + // { + // cmd.CommandText = @" + // PRAGMA journal_mode = WAL; + // PRAGMA synchronous = NORMAL; + // PRAGMA foreign_keys = ON; + // "; + // cmd.ExecuteNonQuery(); + // } + + // return connection; + //} + //private static void InitializeSQLitePCL() + //{ + // lock (_initLock) + // { + // if (!_isInitialized) + // { + // try + // { + // _isInitialized = true; + // Console.WriteLine("SQLitePCL initialized successfully."); + // } + // catch (Exception ex) + // { + // Console.WriteLine($"SQLitePCL initialization failed: {ex.Message}"); + // throw; + // } + // } + // } + //} + #endregion + private static void InitializeDatabasePath() + { + #region(旧版本路径,未区分国内外版本) + //_databasePath = Path.Combine( + // Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + // "AIKKey", "Cache", "aikcache.db"); + #endregion + // 1. 获取 AppData 基础路径 + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + + // 2. 定义公司和应用的基础目录名 + string companyName = "AIKKey"; + string baseFolderName = "Cache"; + string databaseFileName = "aikcache.db"; + string versionFolderName; +#if CN + versionFolderName = "CN"; // 国内版使用 "CN" 子目录 +#elif TEST + versionFolderName = "TEST"; +#else + versionFolderName = "Global"; // 国外版使用 "Global" 子目录 +#endif + _databasePath = Path.Combine(appDataPath, companyName, baseFolderName, versionFolderName, databaseFileName); + // 确保目录存在 + Directory.CreateDirectory(Path.GetDirectoryName(_databasePath)); + } + public static string DatabasePath + { + get + { + InitializeDatabasePath(); + return _databasePath; + } + } + } +} diff --git a/AIK.DataAccess/SqlitePooledConnection.cs b/AIK.DataAccess/SqlitePooledConnection.cs new file mode 100644 index 0000000..93a68f3 --- /dev/null +++ b/AIK.DataAccess/SqlitePooledConnection.cs @@ -0,0 +1,50 @@ +using System.Data.SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.DataAccess +{ + public class SqlitePooledConnection : IDisposable + { + // 内部持有真实的 SQLite 连接 + private readonly SQLiteConnection _innerConnection; + // 标记是否已释放 + private bool _disposed = false; + + /// + /// 暴露内部连接供 Dapper 调用(核心) + /// + public SQLiteConnection InnerConnection => _innerConnection; + + /// + /// 构造函数(仅由 SQLiteCore 内部创建,外部不直接实例化) + /// + internal SqlitePooledConnection(SQLiteConnection innerConnection) + { + _innerConnection = innerConnection ?? throw new ArgumentNullException(nameof(innerConnection)); + } + + /// + /// using 结束时自动执行,归还连接到池 + /// + public void Dispose() + { + if (!_disposed) + { + // 关键:归还连接到连接池,而非销毁 + SQLiteCore.ReturnConnection(_innerConnection); + _disposed = true; + } + } + + // 可选:暴露常用方法,简化调用(如创建命令) + public SQLiteCommand CreateCommand() + { + if (_disposed) throw new ObjectDisposedException(nameof(SqlitePooledConnection)); + return _innerConnection.CreateCommand(); + } + } +} diff --git a/AIK.Models/AIK.Models.csproj b/AIK.Models/AIK.Models.csproj new file mode 100644 index 0000000..f3ec584 --- /dev/null +++ b/AIK.Models/AIK.Models.csproj @@ -0,0 +1,26 @@ + + + + net48 + enable + true + 12 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Models/AlphabetGroup.cs b/AIK.Models/AlphabetGroup.cs new file mode 100644 index 0000000..1cbc8e2 --- /dev/null +++ b/AIK.Models/AlphabetGroup.cs @@ -0,0 +1,32 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models +{ + public partial class AlphabetGroup : ObservableObject + { + [ObservableProperty] + private string letter; + + [ObservableProperty] + private ObservableCollection items; + + public void AddRange(string[] names) + { + foreach (var name in names) + { + Items.Add(new AlphabetItem(name, Letter)); + } + } + public AlphabetGroup(string letter) + { + this.letter = letter; + Items = new ObservableCollection(); + } + } +} diff --git a/AIK.Models/AlphabetItem.cs b/AIK.Models/AlphabetItem.cs new file mode 100644 index 0000000..923e93a --- /dev/null +++ b/AIK.Models/AlphabetItem.cs @@ -0,0 +1,25 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace AIK.Models +{ + public partial class AlphabetItem : ObservableObject + { + public AlphabetItem(string name, string letter) + { + Name = name; + Letter = letter; + } + + [ObservableProperty] + private string name; + + [ObservableProperty] + private string letter; + } +} diff --git a/AIK.Models/ApiModels/ApiPageResult.cs b/AIK.Models/ApiModels/ApiPageResult.cs new file mode 100644 index 0000000..f1cd9c2 --- /dev/null +++ b/AIK.Models/ApiModels/ApiPageResult.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace AIK.Models.ApiModels +{ + public class ApiPageResult + { + public List Records { get; set; } = new(); + public long Total { get; set; } + public long Current { get; set; } + public long Size { get; set; } + public long Pages { get; set; } + } +} diff --git a/AIK.Models/ApiModels/ApiResponse.cs b/AIK.Models/ApiModels/ApiResponse.cs new file mode 100644 index 0000000..e3c9198 --- /dev/null +++ b/AIK.Models/ApiModels/ApiResponse.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels +{ + public partial class ApiResponse + { + public int Code { get; set; } + public string? Msg { get; set; } + public string? I18nMsg { get; set; } + public object? Data { get; set; } + public Dictionary? Dict { get; set; } + } + + public class ApiResponse + { + public int Code { get; set; } + public string? Msg { get; set; } + public string? I18nMsg { get; set; } + public T? Data { get; set; } + public Dictionary? Dict { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Banner/BannerModel.cs b/AIK.Models/ApiModels/Banner/BannerModel.cs new file mode 100644 index 0000000..763d745 --- /dev/null +++ b/AIK.Models/ApiModels/Banner/BannerModel.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Banner +{ + public class BannerModel + { + /// + /// 唯一标识 + /// + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// + /// 图片路径 + /// + [JsonPropertyName("imgUrl")] + public string? ImgUrl { get; set; } + + /// + /// 支持的语言(如 "zh-CN", "en-US") + /// + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// + /// 排序序号(值越小越靠前) + /// + [JsonPropertyName("orderNo")] + public int OrderNo { get; set; } + + /// + /// 跳转目标内容(URL 或内部路由) + /// + [JsonPropertyName("targetUrl")] + public string? TargetUrl { get; set; } + + /// + /// 跳转类型: + /// 0 - 无跳转 + /// 1 - 外部链接 + /// 2 - 内部链接 + /// + [JsonPropertyName("type")] + public int? Type { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Brand/KzInfo.cs b/AIK.Models/ApiModels/Brand/KzInfo.cs new file mode 100644 index 0000000..731cf8a --- /dev/null +++ b/AIK.Models/ApiModels/Brand/KzInfo.cs @@ -0,0 +1,112 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Brand +{ + public partial class KzInfo : ObservableObject + { + /// + /// bin文件地址 + /// + public string binAddress { get; set; } + + /// + /// 修改bin文件位置数组 + /// + public List binAddressArray { get; set; } + + /// + /// 芯片类型 + /// + public string chipType { get; set; } + + /// + /// 是否云遥控 + /// + public bool cloudRemote { get; set; } + + /// + /// 是否为电子芯片 + /// + public bool dzxp { get; set; } + + /// + /// 固件编号 + /// + public string firmwareNo { get; set; } + + /// + /// 是否特殊C子机bin文件 + /// + public bool fixedByte { get; set; } + + /// + /// 特殊C子机bin文件修改位置数组 + /// + public List fixedByteArray { get; set; } + + /// + /// 特殊C子机bin文件修改固定数据 + /// + public string fixedByteArrays { get; set; } + + /// + /// 固件信息包下载地址 + /// + public string sourceUrl { get; set; } + + /// + /// 是否特殊子机 + /// + public bool sp { get; set; } + + /// + /// 子机code + /// + public string spCode { get; set; } + + /// + /// 是否使用特殊随机数 + /// + public bool tsRand { get; set; } + + /// + /// 特殊随机数字符 + /// + public string tsRandStr { get; set; } + + /// + /// 所属子机类型 + /// + public string type { get; set; } + + /// + /// 是否修改bin文件 + /// + public bool updateByte { get; set; } + + /// + /// 可用子机(可对应多个子机) + /// + public string usable { get; set; } + + /// + /// 未知用途字段(保留) + /// + public string ysSource { get; set; } + + /// + /// 未知用途字段(保留) + /// + public string ysSourceNo { get; set; } + + /// + /// 遥控再生类型ID + /// + public string zsId { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Burn/BurnItem.cs b/AIK.Models/ApiModels/Burn/BurnItem.cs new file mode 100644 index 0000000..5f2fe18 --- /dev/null +++ b/AIK.Models/ApiModels/Burn/BurnItem.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Burn +{ + public class BurnItem + { + public string DeviceId { get; set; } = string.Empty; + public int KeyId { get; set; } + public string MacAddress { get; set; } = string.Empty; + public string ToolName { get; set; } = string.Empty; + } +} diff --git a/AIK.Models/ApiModels/Burn/BurnKeyItem.cs b/AIK.Models/ApiModels/Burn/BurnKeyItem.cs new file mode 100644 index 0000000..8f0214f --- /dev/null +++ b/AIK.Models/ApiModels/Burn/BurnKeyItem.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Burn +{ + public class BurnKeyItem + { + public int KeyId { get; set; } + public string[]? KeyNoList { get; set; } + } +} diff --git a/AIK.Models/ApiModels/CloudData/ApiResponseOssData.cs b/AIK.Models/ApiModels/CloudData/ApiResponseOssData.cs new file mode 100644 index 0000000..291ce45 --- /dev/null +++ b/AIK.Models/ApiModels/CloudData/ApiResponseOssData.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.CloudData +{ + public class ApiResponseOssData : ApiResponse + { + public new List? Data { get; set; } + } +} diff --git a/AIK.Models/ApiModels/CloudData/CloudPackageInfo.cs b/AIK.Models/ApiModels/CloudData/CloudPackageInfo.cs new file mode 100644 index 0000000..45458b6 --- /dev/null +++ b/AIK.Models/ApiModels/CloudData/CloudPackageInfo.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.CloudData +{ + public class CloudPackageInfo + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + [JsonPropertyName("esaUrl")] + public string EsaUrl { get; set; } = string.Empty; + + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("updateTime")] + public string UpdateTime { get; set; } = string.Empty; + } +} diff --git a/AIK.Models/ApiModels/Integral/ApiResponseIntegralRecord.cs b/AIK.Models/ApiModels/Integral/ApiResponseIntegralRecord.cs new file mode 100644 index 0000000..fcc83db --- /dev/null +++ b/AIK.Models/ApiModels/Integral/ApiResponseIntegralRecord.cs @@ -0,0 +1,20 @@ +using AIK.Models.ApiModels.Integral; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Record +{ + public class ApiResponseIntegralRecord : ApiResponse + { + public List Records { get; set; } + public int Total { get; set; } + public int Size { get; set; } + public int Current { get; set; } + public List Orders { get; set; } + public bool SearchCount { get; set; } + public int Pages { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Integral/IntegralModel.cs b/AIK.Models/ApiModels/Integral/IntegralModel.cs new file mode 100644 index 0000000..c81dfae --- /dev/null +++ b/AIK.Models/ApiModels/Integral/IntegralModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Integral +{ + public class IntegralModel + { + public int? Id { get; set; } + + public int Integral { get; set; } + + public int? UsedIntegral { get; set; } + + public int? UserId { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Integral/IntegralRecord.cs b/AIK.Models/ApiModels/Integral/IntegralRecord.cs new file mode 100644 index 0000000..43cf4f3 --- /dev/null +++ b/AIK.Models/ApiModels/Integral/IntegralRecord.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Integral +{ + public class IntegralRecord + { + [JsonPropertyName("craeteTime")] // 注意:JSON 中拼写为 "craeteTime"(应为 createTime) + public string CraeteTime { get; set; } = string.Empty; + + [JsonPropertyName("createUser")] + public string CreateUser { get; set; } = string.Empty; + + [JsonPropertyName("fh")] + public string Fh { get; set; } = string.Empty; + + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("remark")] + public string Remark { get; set; } = string.Empty; + + [JsonPropertyName("score")] + public int Score { get; set; } + + [JsonPropertyName("userId")] + public int UserId { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Membership/MembershipModels.cs b/AIK.Models/ApiModels/Membership/MembershipModels.cs new file mode 100644 index 0000000..2f3caca --- /dev/null +++ b/AIK.Models/ApiModels/Membership/MembershipModels.cs @@ -0,0 +1,74 @@ +using System; + +namespace AIK.Models.ApiModels.Membership +{ + public class MembershipModule + { + public int Id { get; set; } + public string? Model { get; set; } + public string? Code { get; set; } + public string? Name { get; set; } + public int Num { get; set; } + public int NumType { get; set; } + public DateTime? CreateTime { get; set; } + } + + public class MembershipFindRequest + { + public string? Model { get; set; } = "key"; + public string? FuncCode { get; set; } + public int? FuncId { get; set; } + public string? SerNo { get; set; } + public string? Language { get; set; } + public int Page { get; set; } = 1; + public int Size { get; set; } = 10; + } + + public class MembershipRecord + { + public int Id { get; set; } + public int FuncId { get; set; } + public string? FuncCode { get; set; } + public string? FuncName { get; set; } + public string? SerNo { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public bool Expire { get; set; } + } + + public class OpenMembershipRequest + { + public string? FuncCode { get; set; } + public string? SerNo { get; set; } + public string? Language { get; set; } + } + + public class BindMembershipDeviceRequest + { + public int Id { get; set; } + public string? SerNo { get; set; } + public string? Language { get; set; } + } + + public class CheckMembershipPermissionRequest + { + public string? Code { get; set; } + public string? SerNo { get; set; } + public int Type { get; set; } + public string? Language { get; set; } + } + + public class ActivateDeviceRequest + { + public string? MacAddress { get; set; } + public string? Language { get; set; } + } + + public class BurnCountInfo + { + public int Count { get; set; } + public int DefCount { get; set; } + public bool Vip { get; set; } + public bool CanWrite { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Payment/PaymentModels.cs b/AIK.Models/ApiModels/Payment/PaymentModels.cs new file mode 100644 index 0000000..8c788bf --- /dev/null +++ b/AIK.Models/ApiModels/Payment/PaymentModels.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AIK.Models.ApiModels.Payment +{ + public class CreateIntegralOrderRequest + { + public decimal Amount { get; set; } + public int PayType { get; set; } = 2; + public string? Channel { get; set; } + public int? QrCode { get; set; } + } + + public class PaymentParameters + { + public long Id { get; set; } + public string? AppId { get; set; } + public string? PartnerId { get; set; } + public string? PrepayId { get; set; } + public string? Package { get; set; } + public string? NonceStr { get; set; } + public string? TimeStamp { get; set; } + public string? Sign { get; set; } + public string? Body { get; set; } + public string? Token { get; set; } + public string? CodeUrl { get; set; } + public string? QrCode { get; set; } + public string? PayUrl { get; set; } + public object? PaymentData { get; set; } + } + + + public class PaymentParametersPC + { + public long id { get; set; } + public string? code_url { get; set; } + public string? status { get; set; } + public List? links { get; set; } + + public string? PaypalApproveUrl => links? + .FirstOrDefault(link => string.Equals(link.rel, "approve", StringComparison.OrdinalIgnoreCase)) + ?.href; + } + + public class PaypalPaymentLink + { + public string? href { get; set; } + public string? rel { get; set; } + public string? method { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Record/ApiResponseRecord.cs b/AIK.Models/ApiModels/Record/ApiResponseRecord.cs new file mode 100644 index 0000000..c79201d --- /dev/null +++ b/AIK.Models/ApiModels/Record/ApiResponseRecord.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Record +{ + public class ApiResponseRecord : ApiResponse + { + public List Records { get; set; } + public int Total { get; set; } + public int Size { get; set; } + public int Current { get; set; } + public List Orders { get; set; } + public bool SearchCount { get; set; } + public int Pages { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Record/OrderItem.cs b/AIK.Models/ApiModels/Record/OrderItem.cs new file mode 100644 index 0000000..1ae185e --- /dev/null +++ b/AIK.Models/ApiModels/Record/OrderItem.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Record +{ + public class OrderItem + { + public string Column { get; set; } + public bool Asc { get; set; } + } +} diff --git a/AIK.Models/ApiModels/Record/RecordModel.cs b/AIK.Models/ApiModels/Record/RecordModel.cs new file mode 100644 index 0000000..4536b88 --- /dev/null +++ b/AIK.Models/ApiModels/Record/RecordModel.cs @@ -0,0 +1,26 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Record +{ + public partial class RecordModel : ObservableObject + { + public int Id { get; set; } + public string DeviceId { get; set; } + public int KeyId { get; set; } + public string Brand { get; set; } + public string Type { get; set; } + public string Name { get; set; } + public string Img { get; set; } + public DateTime CreatedTime { get; set; } + public string Zj { get; set; } + public string MacAddress { get; set; } + public string ToolName { get; set; } + [ObservableProperty] + private string _localImagePath; + } +} diff --git a/AIK.Models/ApiModels/TpmsModels/TpmsType.cs b/AIK.Models/ApiModels/TpmsModels/TpmsType.cs new file mode 100644 index 0000000..a617f3f --- /dev/null +++ b/AIK.Models/ApiModels/TpmsModels/TpmsType.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.TpmsModels +{ + public partial class TpmsType : ObservableObject + { + public int id { get; set; } + public string name { get; set; } + public string? type { get; set; } + public List children { get; set; }=new List(); + public bool haveChildren { get; set; } + public string? py { get; set; } + public string? fzm { get; set; } + public string? area { get; set; } + + [ObservableProperty] + private bool _isSelected; + } + public partial class TpmsTypeChildren : ObservableObject + { + public int id { get; set; } + public string name { get; set; } + public string? type { get; set; } + public TpmsTypeChildren children { get; set; } + public bool haveChildren { get; set; } + public string? py { get; set; } + public string? fzm { get; set; } + public string? area { get; set; } + + [ObservableProperty] + private bool _isSelected; + } +} diff --git a/AIK.Models/ApiModels/TpmsModels/TpmsTypeDetail.cs b/AIK.Models/ApiModels/TpmsModels/TpmsTypeDetail.cs new file mode 100644 index 0000000..eadffe2 --- /dev/null +++ b/AIK.Models/ApiModels/TpmsModels/TpmsTypeDetail.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.TpmsModels +{ + [Obsolete] + public class TpmsTypeDetail + { + public string area { get; set; } + public string blueName { get; set; } + public string createTime { get; set; } + public string fwType { get; set; } + public string fzm { get; set; } + public int id { get; set; } + public string name { get; set; } + public string nameEn { get; set; } + public string nameIn { get; set; } + public string namePu { get; set; } + public string nameXi { get; set; } + public int parentId { get; set; } + public string py { get; set; } + public string type { get; set; } + public string updateTime { get; set; } + } +} diff --git a/AIK.Models/ApiModels/UserModel.cs b/AIK.Models/ApiModels/UserModel.cs new file mode 100644 index 0000000..b2d7ec0 --- /dev/null +++ b/AIK.Models/ApiModels/UserModel.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels +{ + public class UserModel + { + [JsonPropertyName("createTime")] + public string CreateTime { get; init; } = string.Empty; + + [JsonPropertyName("deptId")] + public int DeptId { get; init; } + + [JsonPropertyName("email")] + public string Email { get; init; } = string.Empty; + + [JsonPropertyName("id")] + public int Id { get; init; } + + [JsonPropertyName("image")] + public string Image { get; init; } = string.Empty; + + [JsonPropertyName("local")] + public string Local { get; init; } = string.Empty; + + [JsonPropertyName("nickName")] + public string NickName { get; init; } = string.Empty; + + [JsonPropertyName("phone")] + public string Phone { get; init; } = string.Empty; + + [JsonPropertyName("remark")] + public string Remark { get; init; } = string.Empty; + + [JsonPropertyName("sex")] + public string Sex { get; init; } = string.Empty; + + [JsonPropertyName("updateTime")] + public string UpdateTime { get; init; } = string.Empty; + + [JsonPropertyName("userName")] + public string UserName { get; init; } = string.Empty; + + [JsonPropertyName("userStatus")] + public string UserStatus { get; init; } = string.Empty; + + [JsonPropertyName("userType")] + public string UserType { get; init; } = string.Empty; + + [JsonPropertyName("uuid")] + public string Uuid { get; init; } = string.Empty; + + [JsonPropertyName("workerAccount")] + public string WorkerAccount { get; init; } = string.Empty; + + [JsonPropertyName("workerId")] + public int WorkerId { get; init; } + } +} diff --git a/AIK.Models/ApiModels/Version/AppVersionInfo.cs b/AIK.Models/ApiModels/Version/AppVersionInfo.cs new file mode 100644 index 0000000..9bc8746 --- /dev/null +++ b/AIK.Models/ApiModels/Version/AppVersionInfo.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AIK.Models.ApiModels.Version +{ + public class AppVersionInfo + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("versionName")] + public string VersionName { get; set; } = string.Empty; + + [JsonPropertyName("fileUrl")] + public string FileUrl { get; set; } = string.Empty; + + [JsonPropertyName("createUser")] + public string CreateUser { get; set; } = string.Empty; + + [JsonPropertyName("createTime")] + public string CreateTime { get; set; } = string.Empty; // 也可转为 DateTime,见下方说明 + + [JsonPropertyName("force")] + public bool Force { get; set; } + + [JsonPropertyName("versionCode")] + public string VersionCode { get; set; } = string.Empty; // 注意:虽然是数字字符串,但字段名为 "code",常用于比对 + + [JsonPropertyName("versionContent")] + public string VersionContent { get; set; } = string.Empty; + + [JsonPropertyName("versionEnContent")] + public string VersionEnContent { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("fileUrlB")] + public string? FileUrlB { get; set; } // 允许 null + } +} diff --git a/AIK.Models/BannerModels/BannerItem.cs b/AIK.Models/BannerModels/BannerItem.cs new file mode 100644 index 0000000..15d451e --- /dev/null +++ b/AIK.Models/BannerModels/BannerItem.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Models.BannerModels +{ + public partial class BannerItem : ObservableObject + { + [ObservableProperty] private bool _isSelected; + [ObservableProperty] private string? _imagePath; + [ObservableProperty] private BitmapImage? _bitmapImage; + public int Index { get; set; } // 仅用于命令参数,不参与 UI 触发 + } +} diff --git a/AIK.Models/CarTaiYa/CarfactoryTaiYa.cs b/AIK.Models/CarTaiYa/CarfactoryTaiYa.cs new file mode 100644 index 0000000..b574fb0 --- /dev/null +++ b/AIK.Models/CarTaiYa/CarfactoryTaiYa.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.CarTaiYa +{ + /// + /// 胎压类型 + /// + public class CarfactoryTaiYa + { + public int id { get; set; } + /// + /// 车厂名称 + /// + public string name { get; set; } + /// + /// 拼音 + /// + public string py { get; set; } + + /// + /// 子集节点 + /// + + public List children { get; set; } + /// + /// 所属区域 + /// + public string area { get; set; } + } + + public class Carchildren + { + public int id { get; set; } + /// + /// 车辆类型名称 + /// + public string name { get; set; } + public List children { get; set; } + } + + public class CarTypechildren + { + public int id { get; set; } + /// + /// 车辆类型名称 + /// + public string name { get; set; } + } +} diff --git a/AIK.Models/CarfactoryClass.cs b/AIK.Models/CarfactoryClass.cs new file mode 100644 index 0000000..ba77fa2 --- /dev/null +++ b/AIK.Models/CarfactoryClass.cs @@ -0,0 +1,46 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models +{ + /// + /// 车牌品牌类 + /// + public partial class CarfactoryClass : ObservableObject + { + public string id { get; set; } + /// + /// 中文名称 + /// + public string brandName { get; set; } + /// + /// 英文 + /// + public string engName { get; set; } + /// + /// 葡萄牙 + /// + public string puName { get; set; } + /// + /// 西班牙 + /// + public string xiName { get; set; } + + /// + /// 印度 + /// + public string inName { get; set; } + + /// + /// 名称拼音 + /// + public string py { get; set; } + + [ObservableProperty] + private bool _isSelected; + } +} diff --git a/AIK.Models/FileModels/FileCacheStats.cs b/AIK.Models/FileModels/FileCacheStats.cs new file mode 100644 index 0000000..59b7262 --- /dev/null +++ b/AIK.Models/FileModels/FileCacheStats.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.FileModels +{ + public class FileCacheStats + { + public int TotalCachedFiles { get; set; } + public long TotalCacheSizeBytes { get; set; } + public string TotalCacheSizeFormatted => FormatFileSize(TotalCacheSizeBytes); + public int ExpiredFilesCount { get; set; } + public string CacheDirectory { get; set; } + public DateTime LastCleanupTime { get; set; } + public long AvailableDiskSpaceBytes { get; set; } + public string AvailableDiskSpaceFormatted => FormatFileSize(AvailableDiskSpaceBytes); + public Dictionary FileTypeCounts { get; set; } = new Dictionary(); + + private static string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + int order = 0; + double len = bytes; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len /= 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + } + public class CachedFileInfo + { + public string FileUrl { get; set; } + public string CacheKey { get; set; } + public string LocalFilePath { get; set; } + public string FileName { get; set; } + public string FileExtension { get; set; } + public string MimeType { get; set; } + public long FileSizeBytes { get; set; } + public string FileSizeFormatted => FormatFileSize(FileSizeBytes); + public DateTime CreatedTime { get; set; } + public DateTime LastAccessTime { get; set; } + public int AccessCount { get; set; } + public DateTime ExpiryTime { get; set; } + public bool IsExpired => ExpiryTime < DateTime.Now; + public string FileHash { get; set; } + public TimeSpan DownloadTime { get; set; } + public bool IsValid { get; set; } = true; + + private static string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + int order = 0; + double len = bytes; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len /= 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + } + public class FileCacheConfig + { + public TimeSpan DefaultCacheExpiry { get; set; } = TimeSpan.FromDays(30); + public long MaxCacheSizeBytes { get; set; } = 2L * 1024 * 1024 * 1024; // 2GB + public long MinFreeDiskSpaceBytes { get; set; } = 500L * 1024 * 1024; // 500MB + public int MaxRetryCount { get; set; } = 3; + public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(2); + public TimeSpan HttpClientTimeout { get; set; } = TimeSpan.FromSeconds(60); + public string CacheDirectory { get; set; } + public bool EnableAutoCleanup { get; set; } = true; + public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromHours(1); + } +} diff --git a/AIK.Models/GuageModels/GaugeModel.cs b/AIK.Models/GuageModels/GaugeModel.cs new file mode 100644 index 0000000..29083e5 --- /dev/null +++ b/AIK.Models/GuageModels/GaugeModel.cs @@ -0,0 +1,27 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.GuageModels +{ + public partial class GaugeModel : ObservableObject + { + [ObservableProperty] + private double value; + + [ObservableProperty] + private double minValue = 0; + + [ObservableProperty] + private double maxValue = 1000; + + [ObservableProperty] + private string unit = "HZ"; + + [ObservableProperty] + private string title = "频率"; + } +} diff --git a/AIK.Models/HidModels/BigPhaseTask.cs b/AIK.Models/HidModels/BigPhaseTask.cs new file mode 100644 index 0000000..0beed47 --- /dev/null +++ b/AIK.Models/HidModels/BigPhaseTask.cs @@ -0,0 +1,19 @@ +using AIK.Common.Enum; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.HidModels +{ + /// + /// 大阶段任务模型(存储每个大阶段的执行参数) + /// + public class BigPhaseTask + { + public BigTransferPhase Phase { get; set; } + public bool IsCompleted { get; set; } + public string ErrorMessage { get; set; } = string.Empty; + } +} diff --git a/AIK.Models/HidModels/HidDeviceStatus.cs b/AIK.Models/HidModels/HidDeviceStatus.cs new file mode 100644 index 0000000..f89b91f --- /dev/null +++ b/AIK.Models/HidModels/HidDeviceStatus.cs @@ -0,0 +1,29 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.HidModels +{ + public partial class HidDeviceStatus : ObservableObject + { + [ObservableProperty] + private string _deviceStatusText; + [ObservableProperty] + private string _deviceStatusColor; // 默认灰色 + [ObservableProperty] + private string _deviceStatusIcon; // 默认未连接图标 + [ObservableProperty] + private bool _deviceStatus; // 默认未连接(连接状态) + + public HidDeviceStatus() + { + DeviceStatus = false; + DeviceStatusText = "未连接"; + DeviceStatusColor = "#FFF44336"; + DeviceStatusIcon = "\uE6f9"; + } + } +} diff --git a/AIK.Models/HidModels/ReconnectStatus.cs b/AIK.Models/HidModels/ReconnectStatus.cs new file mode 100644 index 0000000..e1842cb --- /dev/null +++ b/AIK.Models/HidModels/ReconnectStatus.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.HidModels +{ + /// + /// 重连状态信息 + /// + public class ReconnectStatus + { + public bool IsAutoReconnectEnabled { get; set; } + public int ReconnectAttempts { get; set; } + public int MaxReconnectAttempts { get; set; } + public bool IsMonitoring { get; set; } + } +} diff --git a/AIK.Models/HidModels/TransferState.cs b/AIK.Models/HidModels/TransferState.cs new file mode 100644 index 0000000..69768df --- /dev/null +++ b/AIK.Models/HidModels/TransferState.cs @@ -0,0 +1,51 @@ +using AIK.Common.Enum; +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.HidModels +{ + public partial class TransferState : ObservableObject + { + [ObservableProperty] + private TransferPhase _currentPhase = TransferPhase.Idle; + + [ObservableProperty] + private int _totalPackets; + + [ObservableProperty] + private int _currentPacketIndex; + + [ObservableProperty] + private int _receivedAckCount; + + [ObservableProperty] + private bool _isLastDataFrameSent; + + [ObservableProperty] + private string _statusMessage = "空闲"; + + [ObservableProperty] + private bool _isConnected; + + [ObservableProperty] + private bool _isTransferring; + + [ObservableProperty] + private bool _isDataConnected; + + public void Reset() + { + CurrentPhase = TransferPhase.Idle; + TotalPackets = 0; + CurrentPacketIndex = 0; + ReceivedAckCount = 0; + IsLastDataFrameSent = false; + StatusMessage = "空闲"; + IsTransferring = false; + } + } +} diff --git a/AIK.Models/HttpPolicy/RetryPolicyConfig.cs b/AIK.Models/HttpPolicy/RetryPolicyConfig.cs new file mode 100644 index 0000000..0aa09f2 --- /dev/null +++ b/AIK.Models/HttpPolicy/RetryPolicyConfig.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.HttpPolicy +{ + public class RetryPolicyConfig + { + public int MaxRetryCount { get; set; } = 3; + public TimeSpan RetryDelay { get; set; } = TimeSpan.FromSeconds(1); + public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(30); + } +} diff --git a/AIK.Models/IsExternalInit.cs b/AIK.Models/IsExternalInit.cs new file mode 100644 index 0000000..a869ed6 --- /dev/null +++ b/AIK.Models/IsExternalInit.cs @@ -0,0 +1,6 @@ +// +// Polyfill for record types with init-only setters (required by .NET Framework 4.8) +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/AIK.Models/Menus/MenuModel.cs b/AIK.Models/Menus/MenuModel.cs new file mode 100644 index 0000000..efae1c4 --- /dev/null +++ b/AIK.Models/Menus/MenuModel.cs @@ -0,0 +1,39 @@ +using AIK.Common.Enum; +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace AIK.Models.Menus +{ + public partial class MenuModel : ObservableObject + { + [ObservableProperty] + private bool _isSelected; + public int Key { get; set; } + private string _menuHeader; + public string MenuHeader + { + get => _menuHeader; + set => SetProperty(ref _menuHeader, value); + } + public string TargetView { get; set; } + public string MenuIcon { get; set; } + public string ResourceKey { get; set; } + [ObservableProperty] + private bool _isAuthorized = false; + [ObservableProperty] + private bool _isShow = true; + + // 添加子菜单集合 + public ObservableCollection SubItems { get; set; } = new ObservableCollection(); + + [ObservableProperty] + private bool _isExpanded; + public bool HasSubItems => SubItems.Any(); + } +} diff --git a/AIK.Models/SQLiteModels/SQLiteCacheItem.cs b/AIK.Models/SQLiteModels/SQLiteCacheItem.cs new file mode 100644 index 0000000..49eb000 --- /dev/null +++ b/AIK.Models/SQLiteModels/SQLiteCacheItem.cs @@ -0,0 +1,31 @@ +using Dapper.Contrib.Extensions; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SQLiteModels +{ + [Dapper.Contrib.Extensions.Table("CacheItems")] + public class CacheItem + { + [ExplicitKey] // 因为Key是字符串,不是自增ID + public string CacheKey { get; set; } + + public string Data { get; set; } + + public string DataType { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime ExpiresAt { get; set; } + + public DateTime LastAccessed { get; set; } + + public int AccessCount { get; set; } + + public string Metadata { get; set; } // 存储额外信息,如数据大小等 + } +} diff --git a/AIK.Models/SQLiteModels/SplitCacheKzInfoListItem.cs b/AIK.Models/SQLiteModels/SplitCacheKzInfoListItem.cs new file mode 100644 index 0000000..e419806 --- /dev/null +++ b/AIK.Models/SQLiteModels/SplitCacheKzInfoListItem.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SQLiteModels +{ + public class SplitCacheKzInfoListItem + { + public int ModelListId { get; set; } // 外键:指向 SplitCache_ModelList.Id + + public string binAddress { get; set; } = string.Empty; + public string binAddressArray { get; set; } = "[]"; + public string chipType { get; set; } = string.Empty; + public bool cloudRemote { get; set; } + public bool dzxp { get; set; } + public string firmwareNo { get; set; } = string.Empty; + public bool fixedByte { get; set; } + public string fixedByteArray { get; set; } = "[]"; + public string fixedByteArrays { get; set; } = string.Empty; + public string sourceUrl { get; set; } = string.Empty; + public bool sp { get; set; } + public string spCode { get; set; } = string.Empty; + public bool tsRand { get; set; } + public string tsRandStr { get; set; } = string.Empty; + public string type { get; set; } = string.Empty; + public bool updateByte { get; set; } + public string usable { get; set; } = string.Empty; + public string ysSource { get; set; } = string.Empty; + public string ysSourceNo { get; set; } = string.Empty; + public string zsId { get; set; } = string.Empty; + public string CreateTime { get; set; } = string.Empty; + } +} diff --git a/AIK.Models/SQLiteModels/SplitCacheModelListItem.cs b/AIK.Models/SQLiteModels/SplitCacheModelListItem.cs new file mode 100644 index 0000000..7307a1a --- /dev/null +++ b/AIK.Models/SQLiteModels/SplitCacheModelListItem.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SQLiteModels +{ + public class SplitCacheModelListItem + { + public int Id { get; set; } // 子表自增主键 + public int ParentTableId { get; set; } // 关联父表 SplitCache_Parent.Id + public string CacheKey { get; set; } = string.Empty; // 关联父表 SplitCache_Parent.CacheKey + public int ParentID { get; set; } // 关联 VehModelClass.ID(原外层 ID) + + // VehKeyClass 基础字段 + public int KeyId { get; set; } // 对应 VehKeyClass.id(注意:原实体 id 对应 KeyId,避免与子表自增 Id 冲突) + public string KeyKeyId { get; set; } = string.Empty; // 对应 VehKeyClass.keyId(字段名加前缀避免关键字冲突) + public string Name { get; set; } = string.Empty; + public string ModelName { get; set; } = string.Empty; + public string ModelEngName { get; set; } = string.Empty; + public string ModelPuName { get; set; } = string.Empty; + public string ModelXiName { get; set; } = string.Empty; + public string ModelInName { get; set; } = string.Empty; + public string Rate { get; set; } = string.Empty; + public string Usable { get; set; } = string.Empty; + public string UsableEn { get; set; } = string.Empty; + public string UsablePu { get; set; } = string.Empty; + public string UsableXi { get; set; } = string.Empty; + public string Information { get; set; } = string.Empty; + public string PicUrl { get; set; } = string.Empty; + public string FirmwareNo { get; set; } = string.Empty; + public string SourceUrl { get; set; } = string.Empty; + public string SpCode { get; set; } = string.Empty; + public string Support { get; set; } = string.Empty; + public string ChipType { get; set; } = string.Empty; + public string CreateTime { get; set; } = string.Empty; + + // keyMatch 嵌套类展开字段 + public string KeyMatchContent { get; set; } = string.Empty; // keyMatch.content + public string KeyMatchContentEn { get; set; } = string.Empty; // keyMatch.contentEn + public string KeyMatchContentPu { get; set; } = string.Empty; // keyMatch.contentPu + public string KeyMatchContentXi { get; set; } = string.Empty; // keyMatch.contentXi + public string KeyMatchContentIn { get; set; } = string.Empty; // keyMatch.contentIn + + // 其他字段(BitmapImage 为UI字段,不存储;LocalImagePath 存储本地路径) + public bool IsSelected { get; set; } = false; + public string LocalImagePath { get; set; } = string.Empty; + } +} diff --git a/AIK.Models/SQLiteModels/SplitCacheParent.cs b/AIK.Models/SQLiteModels/SplitCacheParent.cs new file mode 100644 index 0000000..d1be1a6 --- /dev/null +++ b/AIK.Models/SQLiteModels/SplitCacheParent.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SQLiteModels +{ + public class SplitCacheParent + { + public string CacheKey { get; set; } = string.Empty; + public string ParentData { get; set; } = string.Empty; + public int SubItemCount { get; set; } = 0; + public string DataType { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime ExpiresAt { get; set; } + public DateTime LastAccessed { get; set; } + public int AccessCount { get; set; } = 0; + public string Metadata { get; set; } = string.Empty; + } +} diff --git a/AIK.Models/SystemSettings/ApiSettings.cs b/AIK.Models/SystemSettings/ApiSettings.cs new file mode 100644 index 0000000..92920a1 --- /dev/null +++ b/AIK.Models/SystemSettings/ApiSettings.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SystemSettings +{ + public class ApiSettings + { + // [修改] 私有字段存储原始配置 + private string _baseUrl = string.Empty; + + // [修改] 公开属性 + public string BaseUrl + { + get => _baseUrl; + set + { +#if CN + //适应域名更改逻辑 + if (value == "https://key.aik518.com") + { + _baseUrl = "https://key.aikkey.cn"; + } + else + { + _baseUrl = value; + } +#else + _baseUrl = value; // 非CN环境保持原样 +#endif + } + } + //public string BaseUrl { get; set; } = string.Empty; + //public string FileUrl { get; set; } = string.Empty; + //public string QRCodeUrl { get; set; } = string.Empty; + //public string WSUrl { get; set; } = string.Empty; + // --- FileUrl (新增逻辑) --- + private string _fileUrl = string.Empty; + public string FileUrl + { + get => _fileUrl; + set + { +#if CN + // 假设文件服务也在同一个新域名下,如果是不同域名请修改判断条件 + if (value.Contains("key.aik518.com")) + _fileUrl = value.Replace("key.aik518.com", "key.aikkey.cn"); + else + _fileUrl = value; +#else + _fileUrl = value; +#endif + } + } + + // --- QRCodeUrl (新增逻辑) --- + private string _qrCodeUrl = string.Empty; + public string QRCodeUrl + { + get => _qrCodeUrl; + set + { +#if CN + if (value.Contains("key.aik518.com")) + _qrCodeUrl = value.Replace("key.aik518.com", "key.aikkey.cn"); + else + _qrCodeUrl = value; +#else + _qrCodeUrl = value; +#endif + } + } + + // --- WSUrl (新增逻辑) --- + private string _wsUrl = string.Empty; + public string WSUrl + { + get => _wsUrl; + set + { +#if CN + if (value.Contains("key.aik518.com")) + _wsUrl = value.Replace("key.aik518.com", "key.aikkey.cn"); + else + _wsUrl = value; +#else + _wsUrl = value; +#endif + } + } + + public int Timeout { get; set; } = 30; + public int RetryCount { get; set; } = 3; + public ApiEndpoints Endpoints { get; set; } = new(); + public string Language { get; set; } = string.Empty; + + /// + /// AI 客服 Web 聊天页 URL(JSBridge 方式加载) + /// + public string AiWebChatUrl { get; set; } = "http://localhost:1001/chat"; + } + + public class ApiEndpoints + { + public string GetCarData { get; set; } = string.Empty; + public string GetCarModels { get; set; } = string.Empty; + public string GetCarImages { get; set; } = string.Empty; + public string Authentication { get; set; } = string.Empty; + } + public class CacheSettings + { + public int DataCacheExpiryHours { get; set; } = 3; + public int ImageCacheExpiryDays { get; set; } = 7; + public int MaxCacheSizeMB { get; set; } = 100; + + public TimeSpan DataCacheExpiry => TimeSpan.FromHours(DataCacheExpiryHours); + public TimeSpan ImageCacheExpiry => TimeSpan.FromDays(ImageCacheExpiryDays); + public long MaxCacheSizeBytes => MaxCacheSizeMB * 1024L * 1024L; + } +} diff --git a/AIK.Models/SystemSettings/AppConfig.cs b/AIK.Models/SystemSettings/AppConfig.cs new file mode 100644 index 0000000..b0d9983 --- /dev/null +++ b/AIK.Models/SystemSettings/AppConfig.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SystemSettings +{ + public class AppConfig + { + public string Environment { get; set; } = "Production"; + public ApiSettings ApiSettings { get; set; } = new(); + public CacheSettings CacheSettings { get; set; } = new(); + } +} diff --git a/AIK.Models/SystemSettings/LanguageItems.cs b/AIK.Models/SystemSettings/LanguageItems.cs new file mode 100644 index 0000000..553ccf7 --- /dev/null +++ b/AIK.Models/SystemSettings/LanguageItems.cs @@ -0,0 +1,23 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.SystemSettings +{ + public partial class LanguageItems : ObservableObject + { + [ObservableProperty] + private string _code; + [ObservableProperty] + private string _name; + [ObservableProperty] + private string _nativeName; + [ObservableProperty] + private string _flagEmoji; + + public override string ToString() => $"{FlagEmoji} {NativeName}"; + } +} diff --git a/AIK.Models/SystemSettings/UserSessionState.cs b/AIK.Models/SystemSettings/UserSessionState.cs new file mode 100644 index 0000000..57afe51 --- /dev/null +++ b/AIK.Models/SystemSettings/UserSessionState.cs @@ -0,0 +1,17 @@ +using AIK.Common.Enum; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Integral; + +namespace AIK.Models.SystemSettings; + +public sealed class UserSessionState +{ + public bool IsLoggedIn { get; init; } + public bool IsQrCodeExpired { get; init; } + public string StatusText { get; init; } = string.Empty; + public UserStatus UserStatus { get; init; } = UserStatus.NotLoggedIn; + public UserModel? UserInfo { get; init; } + public IntegralModel? IntegralInfo { get; init; } + public byte[]? QrCodeUserBytes { get; init; } + public bool CanShowAuthorizedMenus { get; init; } +} diff --git a/AIK.Models/SystemSettings/UserSessionStateChangedEventArgs.cs b/AIK.Models/SystemSettings/UserSessionStateChangedEventArgs.cs new file mode 100644 index 0000000..c4f6336 --- /dev/null +++ b/AIK.Models/SystemSettings/UserSessionStateChangedEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace AIK.Models.SystemSettings; + +public sealed class UserSessionStateChangedEventArgs : EventArgs +{ + public UserSessionStateChangedEventArgs(UserSessionState state) + { + State = state; + } + + public UserSessionState State { get; } +} diff --git a/AIK.Models/UtilsModels/Step/StepItem.cs b/AIK.Models/UtilsModels/Step/StepItem.cs new file mode 100644 index 0000000..6f5a2bd --- /dev/null +++ b/AIK.Models/UtilsModels/Step/StepItem.cs @@ -0,0 +1,31 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.UtilsModels.Step +{ + public partial class StepItem : ObservableObject + { + [ObservableProperty] + private string _stepName; + + [ObservableProperty] + private string _iconCode; + + [ObservableProperty] + private bool _isActive; + + [ObservableProperty] + private bool _isLastStep; + + public StepItem(string name, string icon, bool isLast = false) + { + StepName = name; + IconCode = icon; + IsLastStep = isLast; + } + } +} diff --git a/AIK.Models/VehKeyClass.cs b/AIK.Models/VehKeyClass.cs new file mode 100644 index 0000000..9941588 --- /dev/null +++ b/AIK.Models/VehKeyClass.cs @@ -0,0 +1,141 @@ +using AIK.Models.ApiModels.Brand; +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Models +{ + /// + /// 车钥匙数据类 + /// + public partial class VehKeyClass : ObservableObject + { + public int parentId { get; set; } + public int id { get; set; } + public string keyId { get; set; } + /// + /// 中文名称 + /// + private string name; + + public string Name + { + get => name; + set => SetProperty(ref name, value); + } + /// + /// 中文名称 + /// + public string modelName { get; set; } + /// + /// 英文名称 + /// + public string modelEngName { get; set; } + public string modelPuName { get; set; } + public string modelXiName { get; set; } + public string modelInName { get; set; } + /// + /// 频率 + /// + public string rate { get; set; } + /// + /// 子机中文描述 + /// + public string usable { get; set; } + /// + /// 子机英文描述 + /// + public string usableEn { get; set; } + public string usablePu { get; set; } + public string usableXi { get; set; } + + /// + /// 参考信息 + /// + public string information { get; set; } + + /// + /// 图片路径 分割符,号取第一张 + /// + + public string picUrl { get; set; } + /// + /// Bin文件名称 分割符,号取多个 + /// + public string firmwareNo { get; set; } + /// + /// Bin文件路径 分割符,号取多个 + /// + public string sourceUrl { get; set; } + + /// + /// 烧录完成 提示内容 分割符,号 一个文件提示2个比如(1,1 , 1,1) 提示 1,1一起 + /// + public string spCode { get; set; } + /// + /// (1是K3 MINI 2是 K3 TOOL)只查询包含1和2的 + /// + public string support { get; set; } + + /// + /// 芯片 + /// + public string chipType { get; set; } + + /// + /// 创建时间 + /// + public string createTime { get; set; } + /// + /// 匹配方法描述 + /// + public keyMatch keyMatch { get; set; } + + /// + /// 扩展子机 + /// + public List kzInfoList { get; set; } + + private BitmapImage _bitmapImage; + + public BitmapImage BitmapImage + { + get => _bitmapImage; + set => SetProperty(ref _bitmapImage, value); + } + [ObservableProperty] + private bool _isSelected; + + [ObservableProperty] + private string _localImagePath; + + [ObservableProperty] + private bool _isShowLabel = false; + + [ObservableProperty] + private bool _isShowCloudLabel = false; + + [ObservableProperty] + private bool _isShowCFLabel = false; + + [ObservableProperty] + private string _cloudLabel; + } + + /// + /// 匹配方法描述 + /// + public class keyMatch + { + public string content { get; set; } + public string contentEn { get; set; } + public string contentPu { get; set; } + public string contentXi { get; set; } + public string contentIn { get; set; } + + } +} diff --git a/AIK.Models/VehModelClass.cs b/AIK.Models/VehModelClass.cs new file mode 100644 index 0000000..5e87153 --- /dev/null +++ b/AIK.Models/VehModelClass.cs @@ -0,0 +1,34 @@ +using AIK.Models.ApiModels.Brand; +using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models +{ + /// + /// 车辆品牌钥匙类型以及详情获取 + /// + public partial class VehModelClass : ObservableObject + { + public int ID { get; set; } + /// + /// 中文名称 + /// + public string typeName { get; set; } + public string typeEngName { get; set; } + public string typePuName { get; set; } + public string typeXiName { get; set; } + public string typeInName { get; set; } + + /// + /// 车钥匙详情集合数据 + /// + public List modelList { get; set; } + + [ObservableProperty] + private bool _isSelected; + } +} diff --git a/AIK.Models/VersionClass.cs b/AIK.Models/VersionClass.cs new file mode 100644 index 0000000..dea2caa --- /dev/null +++ b/AIK.Models/VersionClass.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models +{ + /// + /// 设备固件版本 + /// + public class VersionClass + { + /// + /// 设备固件版本id + /// + public int id { get; set; } + /// + /// 固件版本名称 + /// + public string versionName { get; set; } + /// + /// 固件版本下载地址 + /// + public string fileUrl { get; set; } + /// + /// 固件版本下载地址B + /// + public string fileUrlB { get; set; } + } +} diff --git a/AIK.Models/VideoModels/ShowVideoMessage.cs b/AIK.Models/VideoModels/ShowVideoMessage.cs new file mode 100644 index 0000000..62421d2 --- /dev/null +++ b/AIK.Models/VideoModels/ShowVideoMessage.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.VideoModels +{ + public class ShowVideoMessage + { + public string VideoPath { get; set; } + public string Title { get; set; } + } +} diff --git a/AIK.Models/VideoModels/VideoControlMessage.cs b/AIK.Models/VideoModels/VideoControlMessage.cs new file mode 100644 index 0000000..fcbfe34 --- /dev/null +++ b/AIK.Models/VideoModels/VideoControlMessage.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.VideoModels +{ + public class VideoControlMessage + { + public enum ControlAction + { + Play, + Pause, + Stop, + Seek, + ToggleFullScreen, + SetVolume, + } + + public ControlAction Action { get; set; } + public TimeSpan? Position { get; set; } + public double? Volume { get; set; } // 新增音量参数 + } +} diff --git a/AIK.Models/VideoModels/VolumeChangedMessage.cs b/AIK.Models/VideoModels/VolumeChangedMessage.cs new file mode 100644 index 0000000..7d8db74 --- /dev/null +++ b/AIK.Models/VideoModels/VolumeChangedMessage.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Models.VideoModels +{ + public class VolumeChangedMessage + { + public double Volume { get; set; } + public bool IsMuted { get; set; } + } +} diff --git a/AIK.Service/AIK.Service.csproj b/AIK.Service/AIK.Service.csproj new file mode 100644 index 0000000..c3a0ba9 --- /dev/null +++ b/AIK.Service/AIK.Service.csproj @@ -0,0 +1,40 @@ + + + + net48 + enable + true + 12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Service/Extensions/FileServiceFactory.cs b/AIK.Service/Extensions/FileServiceFactory.cs new file mode 100644 index 0000000..7dc2b45 --- /dev/null +++ b/AIK.Service/Extensions/FileServiceFactory.cs @@ -0,0 +1,33 @@ +using AIK.Common.Enum; +using AIK.Service.IService; +using AIK.Service.Service; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Extensions +{ + public class FileServiceFactory : IFileServiceFactory + { + private readonly FileOBDService _obdService; + private readonly FileBinService _binService; + private readonly FileCloudDataService _cloudDataService; + + public FileServiceFactory(FileOBDService obdService, FileBinService binService, FileCloudDataService cloudDataService) + { + _obdService = obdService; + _binService = binService; + _cloudDataService = cloudDataService; + } + + public IFileCacheService GetService(FileServiceType type) => type switch + { + FileServiceType.BIN => _binService, + FileServiceType.OBD => _obdService, + FileServiceType.CloudData => _cloudDataService, + _ => throw new ArgumentException("Unsupported type") + }; + } +} diff --git a/AIK.Service/Extensions/ICloseRequestAware.cs b/AIK.Service/Extensions/ICloseRequestAware.cs new file mode 100644 index 0000000..56bd227 --- /dev/null +++ b/AIK.Service/Extensions/ICloseRequestAware.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Extensions +{ + public interface ICloseRequestAware + { + event EventHandler RequestClose; + } +} diff --git a/AIK.Service/Extensions/IFileServiceFactory.cs b/AIK.Service/Extensions/IFileServiceFactory.cs new file mode 100644 index 0000000..0d9df7a --- /dev/null +++ b/AIK.Service/Extensions/IFileServiceFactory.cs @@ -0,0 +1,15 @@ +using AIK.Common.Enum; +using AIK.Service.IService; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Extensions +{ + public interface IFileServiceFactory + { + IFileCacheService GetService(FileServiceType type); + } +} diff --git a/AIK.Service/Extensions/IWindowFactory.cs b/AIK.Service/Extensions/IWindowFactory.cs new file mode 100644 index 0000000..4345d04 --- /dev/null +++ b/AIK.Service/Extensions/IWindowFactory.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Service.Extensions +{ + public interface IWindowFactory + { + // 新增:基于Type的方法 + Window CreateWindow(Type windowType); + Window CreateWindow(Type windowType, Type viewModelType, object viewModel = null); + } +} diff --git a/AIK.Service/Extensions/WindowFactory.cs b/AIK.Service/Extensions/WindowFactory.cs new file mode 100644 index 0000000..b075a2f --- /dev/null +++ b/AIK.Service/Extensions/WindowFactory.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Service.Extensions +{ + public class WindowFactory : IWindowFactory + { + private readonly IServiceProvider _serviceProvider; + + public WindowFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public Window CreateWindow(Type windowType) + { + return (Window)_serviceProvider.GetRequiredService(windowType); + } + + public Window CreateWindow(Type windowType, Type viewModelType, object viewModel = null) + { + var window = (Window)_serviceProvider.GetRequiredService(windowType); + + //if (viewModel != null && window.DataContext == null) + //{ + // window.DataContext = viewModel; + //} + + return window; + } + + } +} diff --git a/AIK.Service/IService/IAikDataService.cs b/AIK.Service/IService/IAikDataService.cs new file mode 100644 index 0000000..1e38916 --- /dev/null +++ b/AIK.Service/IService/IAikDataService.cs @@ -0,0 +1,67 @@ +using AIK.Common.SysCommon; +using AIK.Models; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Banner; +using AIK.Models.ApiModels.Burn; +using AIK.Models.ApiModels.CloudData; +using AIK.Models.ApiModels.Integral; +using AIK.Models.ApiModels.Record; +using AIK.Models.ApiModels.TpmsModels; +using AIK.Models.ApiModels.Version; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IAikDataService + { + Task GetUserInfo(string token); + + Task?> GetBanner(string languagecode = ""); + + Task GetUserIntegral(string token); + + Task GetUserIntegralHistroy(string token); + + Task GetAppVersionInfo(string type); + + /// 账密登录:成功时 Message 为 JWT Token,失败时 Message 为错误信息。 + Task<(bool Success, string Message)> LoginByAccountAsync(string account, string code, string password, string type); + + /// 发送注册验证码(手机号):GET /work_sms/sms/send_code,成功(code=1)Message 为空。 + Task<(bool Success, string Message)> SendRegisterCodeAsync(string phone); + + /// 发送登录验证码(手机号):GET /work_sms/sms/send_login,成功(code=1)Message 为空。 + Task<(bool Success, string Message)> SendLoginCodeAsync(string phone); + + /// 验证验证码(手机号):GET /work_sms/sms/check_code,参数 code/phone/type(可选)。 + Task<(bool Success, string Message)> CheckVerifyCodeAsync(string phone, string code, string type = ""); + + /// 发送邮箱验证码:GET /work_sms/email/send_code,参数 address/type(语言,可选),成功(code=1)。 + Task<(bool Success, string Message)> SendEmailCodeAsync(string address, string type = ""); + + /// 验证邮箱验证码:GET /work_sms/email/check_code,参数 address/code,成功(code=1)。 + Task<(bool Success, string Message)> CheckEmailCodeAsync(string address, string code); + + /// 手机号注册(code 验证码 + phone + psw 必填):成功(code=1)返回 true,失败 Message 为错误信息。 + Task<(bool Success, string Message)> RegisterByPhoneAsync(string code, string phone, string psw, string nickName = "", string sex = "", string local = "", string image = ""); + + /// 邮箱注册(code 验证码 + email + psw 必填):成功(code=1)返回 true,失败 Message 为错误信息。 + Task<(bool Success, string Message)> RegisterByEmailAsync(string code, string email, string psw, string nickName = "", string sex = "", string local = "", string image = ""); + + Task> GetBannerCache(); + + Task GetKeyLogList(string token, int currentindex = 1, int pagesize = 20); + + Task BurnAddInfo(string token, BurnItem burnItem); + + Task AddIntegral(string token, BurnKeyItem burnKeyItem); + + Task GetOssDataCache(string token, string ossType="K3tool_bin"); + + Task> GetTpmsType(string token,int? id); + } +} diff --git a/AIK.Service/IService/ICarfactoryService.cs b/AIK.Service/IService/ICarfactoryService.cs new file mode 100644 index 0000000..86a9d09 --- /dev/null +++ b/AIK.Service/IService/ICarfactoryService.cs @@ -0,0 +1,46 @@ +using AIK.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.IService +{ + public interface ICarfactoryService + { + /// + /// 获取车辆品牌数据 + /// + /// + /// + public List GetCarfactoryClassList(string vhetype); + + public Task> GetCarfactoryClassListAsync(string vhetype); + /// + /// 获取车辆钥匙数据 + /// + /// + /// + public List GetVehKeyClassList(int id); + + public Task> GetVehKeyClassListAsync(int id); + + /// + /// 获取K3TOOL和K3Genie固件版本数据 copying(k3mini),i2(k3tool),k3_jingling + /// + /// + public List GetVersionClass(string type); + + /// + /// 获取钥匙图片 + /// + /// + /// + public Task GetVehKeyImg(string url); + + } +} diff --git a/AIK.Service/IService/IConfigurationService.cs b/AIK.Service/IService/IConfigurationService.cs new file mode 100644 index 0000000..d7c8e88 --- /dev/null +++ b/AIK.Service/IService/IConfigurationService.cs @@ -0,0 +1,25 @@ +using AIK.Models.SystemSettings; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IConfigurationService + { + string Environment { get; } + ApiSettings ApiSettings { get; } + CacheSettings CacheSettings { get; } + T GetSection(string sectionName) where T : new(); + string GetConnectionString(string name); + + // 新增方法 + Task UpdateLanguageAsync(string languageCode); + Task UpdateApiSettingsAsync(Action updateAction); + Task UpdateCacheSettingsAsync(Action updateAction); + Task SaveConfigurationAsync(); + Task ReloadConfigurationAsync(); + } +} diff --git a/AIK.Service/IService/IDataCacheService.cs b/AIK.Service/IService/IDataCacheService.cs new file mode 100644 index 0000000..318df12 --- /dev/null +++ b/AIK.Service/IService/IDataCacheService.cs @@ -0,0 +1,42 @@ +using AIK.Models; +using AIK.Models.ApiModels; +using AIK.Models.SQLiteModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IDataCacheService + { + Task GetOrCreateAsync(string key, Func> dataFetchFunc, TimeSpan expiry) where T : class; + Task GetAsync(string key) where T : class; + Task SetAsync(string key, T data, TimeSpan expiry) where T : class; + Task ExistsAsync(string key); + Task RemoveAsync(string key); + Task ClearExpiredAsync(); + Task ClearAllAsync(); + Task> GetCacheStatsAsync(); + Task GetCacheSizeAsync(); + + #region 钥匙模型缓存专用 + Task> GetSplitVehModelDataAsync(string cacheKey); + Task SetSplitVehModelDataAsync(string cacheKey, VehModelClass vehModel, TimeSpan expiry); + Task DeleteSplitVehModelDataAsync(string cacheKey); + Task ClearSplitCacheExpiredAsync(); + #endregion + + #region 用户Token缓存专用 + Task StoreUserTokenAsync(string token, TimeSpan? expiry = null); + Task IsUserTokenValidAsync(string token); + Task ClearUserTokenAsync(); + Task ClearExpiredUserTokensAsync(); + Task GetLatestUserTokenAsync(); + Task StoreUserInfoAsync(UserModel user); + Task ClearUserInfoAsync(); + Task GetUserInfoAsync(); + #endregion + } +} diff --git a/AIK.Service/IService/IFileCacheService.cs b/AIK.Service/IService/IFileCacheService.cs new file mode 100644 index 0000000..11e03d3 --- /dev/null +++ b/AIK.Service/IService/IFileCacheService.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.IService +{ + public interface IFileCacheService + { + /// + /// 异步获取文件。优先从缓存中获取,若不存在或已过期则从网络下载。 + /// + /// 第三方文件地址 + /// 自定义缓存键(可选) + /// 缓存的文件数据 + Task GetFileByteAsync(string fileUrl, string cacheKey = null, CancellationToken cancellationToken = default); + + /// + /// 带进度的文件下载方法。优先从缓存中获取,若不存在或已过期则从网络下载。 + /// + /// + /// + /// + /// + /// + Task<(bool success, string localFilePath)> DownloadFileWithProgressAsync(string fileUrl, string cacheKey, IProgress progress = null, CancellationToken cancellationToken = default); + + /// + /// 清除所有过期的缓存文件。(bin文件默认无过期期限) + /// + Task ClearExpiredCacheAsync(); + + /// + /// 强制清除所有缓存(内存和磁盘)。 + /// + Task ClearAllCacheAsync(); + } +} diff --git a/AIK.Service/IService/IFileOperationService.cs b/AIK.Service/IService/IFileOperationService.cs new file mode 100644 index 0000000..2ef58c0 --- /dev/null +++ b/AIK.Service/IService/IFileOperationService.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IFileOperationService + { + Task ExtractSingleArchiveAsync( + string archivePath, + string destinationDir, + bool overwrite = true, + CancellationToken cancellationToken = default); + + Task ExtractMultipleArchivesAsyncNotPoress( + IEnumerable archivePaths, + string destinationDir, + bool overwrite = true, + CancellationToken cancellationToken = default); + + Task ExtractMultipleArchivesAsync( + IReadOnlyList archivePaths, + string destinationDir, + IProgress progress, + bool overwrite = true, + CancellationToken cancellationToken = default); + } +} diff --git a/AIK.Service/IService/IHidCommunicationService.cs b/AIK.Service/IService/IHidCommunicationService.cs new file mode 100644 index 0000000..e9a7e89 --- /dev/null +++ b/AIK.Service/IService/IHidCommunicationService.cs @@ -0,0 +1,124 @@ +using AIK.Common.Enum; +using AIK.Common.Event; +using AIK.Models.HidModels; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IHidCommunicationService + { + bool IsConnected { get; } + TransferState State { get; } + + event EventHandler? ResponseReceived; + event EventHandler? LogMessage; + event EventHandler? ConnectionStateChanged; + + Task ConnectAsync(int vendorId = 0x28E9, int productId = 0xFFF0); + Task ReconnectNowAsync(); + Task DisconnectAsync(); + /// + /// 发送设备分包基础帧(头帧/数据帧/结束帧等) + /// + /// + /// + Task SendAsync(byte[] data); + + /// + /// 根据本地路径烧录文件 + /// + /// + /// + /// + /// + Task<(bool, string)> SendFileAsync(string filePath, IProgress progress, CancellationToken ct); + /// + /// 根据文件名称和文件二进制数据烧录文件 + /// + /// + /// + /// + /// + /// + Task<(bool, string)> SendFileAsync(string fileName, byte[] filebin, IProgress progress, CancellationToken ct); + + /// + /// 附加烧录流程阶段(连接/生成/写入) + /// + /// + /// + /// + /// + /// + Task<(bool, string)> SendFileProcessAsync(string fileName, byte[] filebin, IProgress progress, CancellationToken ct); + + /// + /// 附加烧录流程阶段-固件升级(连接/生成/写入) + /// + /// + /// + /// + /// + /// + Task<(bool, string)> SendFileProcessAsync(string fileName, byte[] filebin, HidOperationTypeEnum hidOperationType, IProgress progress, CancellationToken ct); + + Task<(bool, string)> GetDeviceVersion(HidOperationTypeEnum hidOperationType); + + /// + /// K3Tool盘符获取 + /// + /// + /// + /// + Task FindAssociatedUsbDisk(int vendorId = 0x28E9, int productId = 0xFFF0); + + /// + /// 复制文件或文件夹到U盘 + /// + /// + /// + /// + Task<(bool, string)> CopyToUdiskAsync(string sourcePath, bool isDirectory, IProgress progress, CancellationToken cts); + /// + /// 打开U盘并检测盘符 + /// + /// + /// + /// + /// + Task OpenUDiskAndDetectDriveAsync(int maxWaitSeconds = 15, int pollIntervalMs = 500, CancellationToken cancellationToken = default); + + /// + /// 关闭U盘 + /// + /// + /// + Task CloseUDiskSafeDriveAsync(string targetDrive = ""); + + Task KeepDeviceAliveAsync(); + + public Task CopyDeltaDirectoryToUdiskAsync( + string localRootDir, + IProgress progress, + CancellationToken ct); + + public Task> ScanUdiskFilesAsync(CancellationToken ct); + + public Dictionary ScanLocalFiles(string localRootDir); + + public List GetChangedFiles( + Dictionary localFiles, + Dictionary udiskFiles); + + public Task<(string zipPath, int count)> CreateDeltaZipAsync( + string localRootDir, + List changedRelativePaths, + CancellationToken ct); + } +} diff --git a/AIK.Service/IService/IHttpClientService.cs b/AIK.Service/IService/IHttpClientService.cs new file mode 100644 index 0000000..1afb5bb --- /dev/null +++ b/AIK.Service/IService/IHttpClientService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IHttpClientService + { + Task GetWithTimeoutAsync(string url, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task PostWithTimeoutAsync(string url, HttpContent content, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task GetStringWithTimeoutAsync(string url, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task PostJsonWithTimeoutAsync(string url, string jsonData, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task GetByteArrayWithTimeoutAsync(string url, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + + Task GetStringWithTimeoutAndTokenAsync(string url, string token, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task PostJsonWithTimeoutAndTokenAsync(string url, string jsonData, string token, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + + /// + /// 流式下载文件到指定路径。内部通过 BouncyCastleTlsHandler 的流式通道逐段读取, + /// 每段读取带空闲超时、无总时长限制,适合大文件下载。失败抛出异常(由调用方决定重试)。 + /// 支持断点续传:传入 resumeOffset(已下载字节数)时自动携带 Range 头并从文件末尾追加写入。 + /// + /// 文件下载地址 + /// 目标文件路径(不存在则创建;resumeOffset>0 时追加写入) + /// 下载进度 0-100 + /// 取消令牌 + /// 已下载字节数(断点续传偏移,0 表示从头下载) + Task DownloadToFileWithProgressAsync(string url, string filePath, IProgress progress = null, CancellationToken cancellationToken = default, long resumeOffset = 0); + } +} diff --git a/AIK.Service/IService/IImageCacheService.cs b/AIK.Service/IService/IImageCacheService.cs new file mode 100644 index 0000000..0d5f34e --- /dev/null +++ b/AIK.Service/IService/IImageCacheService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.IService +{ + public interface IImageCacheService + { + /// + /// 异步获取图片。优先从缓存中获取,若不存在或已过期则从网络下载。 + /// + /// 第三方图片地址 + /// 自定义缓存键(可选) + /// 缓存的图片 + Task GetImageAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default); + + /// + /// 异步获取图片。优先从缓存中获取,若不存在或已过期则从网络下载。 + /// + /// 第三方图片地址 + /// 自定义缓存键(可选) + /// 缓存的图片 + Task GetImageByLocalPathAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default); + /// + /// 返回本地存储的文件路径 + /// + /// + /// + /// + /// 缓存的图片文件本地路径 + Task GetImagePathAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default); + /// + /// 清除所有过期的缓存文件。 + /// + Task ClearExpiredCacheAsync(); + + /// + /// 强制清除所有缓存(内存和磁盘)。 + /// + Task ClearAllCacheAsync(); + } +} diff --git a/AIK.Service/IService/ILoadAware.cs b/AIK.Service/IService/ILoadAware.cs new file mode 100644 index 0000000..effdc25 --- /dev/null +++ b/AIK.Service/IService/ILoadAware.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface ILoadAware + { + /// + /// 页面加载完成后异步初始化 + /// + Task OnLoadedAsync(); + } +} diff --git a/AIK.Service/IService/ILocalizationService.cs b/AIK.Service/IService/ILocalizationService.cs new file mode 100644 index 0000000..540c5ba --- /dev/null +++ b/AIK.Service/IService/ILocalizationService.cs @@ -0,0 +1,23 @@ +using AIK.Models.SystemSettings; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface ILocalizationService + { + CultureInfo CurrentCulture { get; } + IEnumerable SupportedCultures { get; } + IEnumerable SupportedLanguages { get; } + event EventHandler LanguageChanged; + Task InitializeAsync(string language); + void SetLanguage(string cultureName); + string GetString(string key); + string GetString(string key, CultureInfo culture); + } + +} diff --git a/AIK.Service/IService/ILogService.cs b/AIK.Service/IService/ILogService.cs new file mode 100644 index 0000000..f3e5ad6 --- /dev/null +++ b/AIK.Service/IService/ILogService.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface ILogService + { + void LogDebug(string message, params object[] args); + void LogInformation(string message, params object[] args); + void LogWarning(string message, params object[] args); + void LogError(Exception? exception, string message, params object[] args); + void LogError(string message, params object[] args); + + IReadOnlyList GetRecentLogs(int maxCount = 100); + Task UploadLogsAsync(); + void ClearBuffer(); + } +} diff --git a/AIK.Service/IService/IMembershipService.cs b/AIK.Service/IService/IMembershipService.cs new file mode 100644 index 0000000..49da9c0 --- /dev/null +++ b/AIK.Service/IService/IMembershipService.cs @@ -0,0 +1,20 @@ +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Membership; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IMembershipService + { + Task> GetModuleListAsync(string model = "key", CancellationToken cancellationToken = default); + Task> FindOpenedFunctionsAsync(string token, MembershipFindRequest request, CancellationToken cancellationToken = default); + Task> OpenFunctionAsync(string token, OpenMembershipRequest request, CancellationToken cancellationToken = default); + Task BindDeviceAsync(string token, BindMembershipDeviceRequest request, CancellationToken cancellationToken = default); + Task CheckPermissionAsync(string token, CheckMembershipPermissionRequest request, CancellationToken cancellationToken = default); + Task ActivateDeviceAsync(string token, ActivateDeviceRequest request, CancellationToken cancellationToken = default); + Task GetBurnCountInfoAsync(string token, CancellationToken cancellationToken = default); + Task AddBurnCountAsync(string token, string type = "ysjs", CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/AIK.Service/IService/IPaymentOrderService.cs b/AIK.Service/IService/IPaymentOrderService.cs new file mode 100644 index 0000000..3819372 --- /dev/null +++ b/AIK.Service/IService/IPaymentOrderService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Models.ApiModels.Payment; + +namespace AIK.Service.IService +{ + public interface IPaymentOrderService + { + Task CreateIntegralOrderAsync(string token, CreateIntegralOrderRequest request, CancellationToken cancellationToken = default); + Task GetOrderPayStatusAsync(long orderId, int type = 2, CancellationToken cancellationToken = default); + Task RefreshPaypalOrderAsync(long orderId, int type = 2, CancellationToken cancellationToken = default); + } +} diff --git a/AIK.Service/IService/IRetryPolicyService.cs b/AIK.Service/IService/IRetryPolicyService.cs new file mode 100644 index 0000000..d946591 --- /dev/null +++ b/AIK.Service/IService/IRetryPolicyService.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using System.Threading; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.IService +{ + public interface IRetryPolicyService + { + Task ExecuteWithRetryAsync(Func> operation, string operationName, CancellationToken cancellationToken = default); + Task ExecuteWithRetryAsync(Func operation, string operationName, CancellationToken cancellationToken = default); + } +} diff --git a/AIK.Service/IService/IUserSessionCoordinator.cs b/AIK.Service/IService/IUserSessionCoordinator.cs new file mode 100644 index 0000000..e64a971 --- /dev/null +++ b/AIK.Service/IService/IUserSessionCoordinator.cs @@ -0,0 +1,18 @@ +using AIK.Models.SystemSettings; +using System; +using System.Threading.Tasks; + +namespace AIK.Service.IService; + +public interface IUserSessionCoordinator +{ + event EventHandler? StateChanged; + + Task InitializeAsync(); + Task LogoutAsync(); + Task RefreshQrCodeAsync(); + Task RefreshIntegralAsync(); + + /// 账密登录成功后写入会话(保存 Token、拉取用户信息与积分并发布登录状态),返回是否成功。 + Task LoginWithTokenAsync(string token); +} diff --git a/AIK.Service/IService/IVehDataService.cs b/AIK.Service/IService/IVehDataService.cs new file mode 100644 index 0000000..f3df692 --- /dev/null +++ b/AIK.Service/IService/IVehDataService.cs @@ -0,0 +1,36 @@ +using AIK.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.IService +{ + public interface IVehDataService + { + Task> GetVehDataAsync(string vhetype, bool forceRefresh = false); + Task> GetVehDataFromApiAsync(string vhetype); + Task IsCacheValidAsync(); + Task ClearCacheAsync(); + Task GetCacheInfoAsync(); + + Task> GetVehKeyDataAsync(int id, bool forceRefresh = false); + Task> GetVehKeyDataFromApiAsync(int id); + + Task GetVehKeyImgAsync(string url, bool forceRefresh = false); + Task GetVehKeyImgDataFromApiAsync(string url); + + Task> GetVehKeyDataByIdAsync(int id); + + } + public class CacheInfo + { + public bool IsValid { get; set; } + public DateTime? LastUpdateTime { get; set; } + public int DataCount { get; set; } + public long CacheSize { get; set; } + public TimeSpan? TimeToExpire { get; set; } + } +} diff --git a/AIK.Service/IService/IWindowService.cs b/AIK.Service/IService/IWindowService.cs new file mode 100644 index 0000000..07f2797 --- /dev/null +++ b/AIK.Service/IService/IWindowService.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Service.IService +{ + public interface IWindowService + { + + void Show(string windowKey); + void Show(string windowKey, TViewModel viewModel) where TViewModel : class; + void Show(string windowKey, TViewModel viewModel, Action initializeAction) where TViewModel : class; + bool? ShowDialog(string windowKey); + bool? ShowDialog(string windowKey, TViewModel viewModel) where TViewModel : class; + + void RegisterWindow(string windowKey, Type windowType, Type viewModelType = null); + } + + public static class WindowKey + { + public const string VideoPopup = "VideoPopup"; + // 添加更多窗口键... + } +} diff --git a/AIK.Service/Interop/WindowsVolumeEjector.cs b/AIK.Service/Interop/WindowsVolumeEjector.cs new file mode 100644 index 0000000..6e4c0c6 --- /dev/null +++ b/AIK.Service/Interop/WindowsVolumeEjector.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Interop +{ + /// + /// P/Invoke 方法实现 Windows 卷弹出功能 + /// + internal static class WindowsVolumeEjector + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr CreateFile( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool DeviceIoControl( + IntPtr hDevice, + uint dwIoControlCode, + IntPtr lpInBuffer, + uint nInBufferSize, + IntPtr lpOutBuffer, + uint nOutBufferSize, + out uint lpBytesReturned, + IntPtr lpOverlapped); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr hObject); + + private const uint GENERIC_READ = 0x80000000; + private const uint FILE_SHARE_READ = 0x1; + private const uint FILE_SHARE_WRITE = 0x2; + private const uint OPEN_EXISTING = 3; + private const uint FSCTL_DISMOUNT_VOLUME = 0x00090020; + private const uint IOCTL_STORAGE_EJECT_MEDIA = 0x2D4808; + + public static async Task EjectVolumeAsync(string driveLetter) + { + return await Task.Run(() => + { + if (string.IsNullOrEmpty(driveLetter)) + return false; + + // 标准化输入:确保是 "X:" 或 "X:\" + string cleanDrive = driveLetter.TrimEnd('\\'); + if (!cleanDrive.EndsWith(":")) + return false; + + string devicePath = @"\\.\" + cleanDrive + @"\"; + IntPtr handle = IntPtr.Zero; + + try + { + handle = CreateFile(devicePath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero); + + if (handle == IntPtr.Zero || handle == new IntPtr(-1)) + { + var errorCode = Marshal.GetLastWin32Error(); + // 可记录日志:$"CreateFile failed with error {errorCode}" + return false; + } + + // 尝试卸载卷(可选,提高成功率) + DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, + IntPtr.Zero, 0, IntPtr.Zero, 0, out _, IntPtr.Zero); + + // 弹出介质 + bool result = DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, + IntPtr.Zero, 0, IntPtr.Zero, 0, out _, IntPtr.Zero); + + return result; + } + catch (Exception ex) + { + // 可记录异常 + return false; + } + finally + { + if (handle != IntPtr.Zero && handle != new IntPtr(-1)) + { + CloseHandle(handle); + } + } + }); + } + } +} diff --git a/AIK.Service/IsExternalInit.cs b/AIK.Service/IsExternalInit.cs new file mode 100644 index 0000000..a869ed6 --- /dev/null +++ b/AIK.Service/IsExternalInit.cs @@ -0,0 +1,6 @@ +// +// Polyfill for record types with init-only setters (required by .NET Framework 4.8) +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/AIK.Service/Service/AikDataService.cs b/AIK.Service/Service/AikDataService.cs new file mode 100644 index 0000000..6f2c866 --- /dev/null +++ b/AIK.Service/Service/AikDataService.cs @@ -0,0 +1,621 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Common.Interface; +using AIK.Common.SysCommon; +using AIK.Common.SysCommon.UserCommon; +using AIK.Models; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Banner; +using AIK.Models.ApiModels.Burn; +using AIK.Models.ApiModels.CloudData; +using AIK.Models.ApiModels.Integral; +using AIK.Models.ApiModels.Record; +using AIK.Models.ApiModels.TpmsModels; +using AIK.Models.ApiModels.Version; +using AIK.Models.BannerModels; +using AIK.Models.HttpPolicy; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.AccessControl; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class AikDataService : IAikDataService + { + private readonly IDataCacheService _cacheService; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private readonly IRetryPolicyService _retryPolicyService; + private readonly RetryPolicyConfig _retryConfig; + private readonly IHttpClientService _httpClientService; + private readonly ISettingsService _settingsService; + + private const string CacheKey_Banner = "banner_data"; + private readonly TimeSpan _cacheExpiry = TimeSpan.FromHours(3);//默认3个小时有效期 + + // 通过构造函数注入 + public AikDataService(IDataCacheService cacheService, IHttpClientFactory httpClientFactory, ILogger logger, IConfigurationService configurationService, IRetryPolicyService retryPolicyService, IHttpClientService httpClientService, ISettingsService settingsService) + { + _cacheService = cacheService; + _httpClientFactory = httpClientFactory; + _configurationService = configurationService; + _logger = logger; + _retryPolicyService = retryPolicyService; + _httpClientService = httpClientService; + _retryConfig = new RetryPolicyConfig + { + MaxRetryCount = 3, + RetryDelay = TimeSpan.FromSeconds(1), + DefaultTimeout = TimeSpan.FromSeconds(15) + }; + _cacheExpiry = TimeSpan.FromHours(_configurationService.CacheSettings.DataCacheExpiryHours); + _settingsService = settingsService; + } + #region App版本相关 + public async Task GetAppVersionInfo(string type) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.AppNew}?type={type}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAsync( + url, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj["data"]?.ToString(); + return JsonConvert.DeserializeObject(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取用户信息数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取Banner失败"); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetUserInfo)); + } + #endregion + /// + /// 账密登录:POST {BaseUrl}/prod-api/work_main/login。 + /// 参数 account/code/password/type 均非必填(string)。 + /// 成功(code=1):msg 为 JWT Token;失败(code!=1):msg 为错误信息。 + /// 登录失败不做重试(密码错误重试无意义)。 + /// + public async Task<(bool Success, string Message)> LoginByAccountAsync(string account, string code, string password, string type) + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.AccountLogin}"; + var requestData = new + { + account = account, + code = code, + password = password, + type = type + }; + string sendData = JsonConvert.SerializeObject(requestData); + + var response = await _httpClientService.PostJsonWithTimeoutAsync( + url, + sendData, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + string status = jobj["code"]?.ToString() ?? string.Empty; + string msg = jobj["msg"]?.ToString() ?? string.Empty; + + if (status == "1") + { + return (true, msg); // msg 为 JWT Token + } + + _logger.LogWarning("账密登录失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return (false, string.IsNullOrEmpty(msg) ? "登录失败" : msg); + } + catch (Exception ex) + { + _logger.LogError(ex, "账密登录请求异常"); + return (false, $"网络请求失败: {ex.Message}"); + } + } + /// + /// 发送注册验证码:GET {BaseUrl}/prod-api/work_sms/sms/send_code?phone=xxx。 + /// 成功(code=1)返回 (true, 空);失败(code!=1)返回 (false, 错误信息)。 + /// + public Task<(bool Success, string Message)> SendRegisterCodeAsync(string phone) + { + return SmsRequestAsync(WebApiAddress.SendCode, new Dictionary { ["phone"] = phone }); + } + + /// + /// 发送登录验证码:GET {BaseUrl}/prod-api/work_sms/sms/send_login?phone=xxx。 + /// 成功(code=1)返回 (true, 空);失败(code!=1)返回 (false, 错误信息)。 + /// + public Task<(bool Success, string Message)> SendLoginCodeAsync(string phone) + { + return SmsRequestAsync(WebApiAddress.SendLoginCode, new Dictionary { ["phone"] = phone }); + } + + /// + /// 验证验证码:GET {BaseUrl}/prod-api/work_sms/sms/check_code?code=xxx&phone=xxx[&type=]。 + /// 成功(code=1)返回 (true, 空);失败(code!=1)返回 (false, 错误信息)。 + /// + public Task<(bool Success, string Message)> CheckVerifyCodeAsync(string phone, string code, string type = "") + { + var query = new Dictionary + { + ["phone"] = phone, + ["code"] = code + }; + if (!string.IsNullOrEmpty(type)) + { + query["type"] = type; + } + return SmsRequestAsync(WebApiAddress.CheckCode, query); + } + + /// + /// 发送邮箱验证码:GET {BaseUrl}/prod-api/work_sms/email/send_code?address=xxx[&type=]。 + /// type 为语言(默认中文,en:英文,可选),成功(code=1)返回 (true, 空)。 + /// + public Task<(bool Success, string Message)> SendEmailCodeAsync(string address, string type = "") + { + var query = new Dictionary { ["address"] = address }; + if (!string.IsNullOrEmpty(type)) + { + query["type"] = type; + } + return SmsRequestAsync(WebApiAddress.EmailSendCode, query); + } + + /// + /// 验证邮箱验证码:GET {BaseUrl}/prod-api/work_sms/email/check_code?address=xxx&code=xxx。 + /// 成功(code=1)返回 (true, 空);失败(code!=1)返回 (false, 错误信息)。 + /// + public Task<(bool Success, string Message)> CheckEmailCodeAsync(string address, string code) + { + return SmsRequestAsync(WebApiAddress.EmailCheckCode, new Dictionary + { + ["address"] = address, + ["code"] = code + }); + } + + /// 短信服务接口公共实现(GET + query 参数,成功状态码为 1)。 + private async Task<(bool Success, string Message)> SmsRequestAsync(string endpoint, Dictionary query) + { + try + { + var sb = new StringBuilder(); + foreach (var kv in query) + { + if (sb.Length > 0) + { + sb.Append('&'); + } + sb.Append(Uri.EscapeDataString(kv.Key)).Append('=').Append(Uri.EscapeDataString(kv.Value)); + } + + string url = $"{_configurationService.ApiSettings.BaseUrl}{endpoint}"; + if (sb.Length > 0) + { + url += "?" + sb; + } + + var response = await _httpClientService.GetStringWithTimeoutAsync(url, TimeSpan.FromSeconds(15)); + var jobj = JObject.Parse(response); + string status = jobj["code"]?.ToString() ?? string.Empty; + string msg = jobj["msg"]?.ToString() ?? string.Empty; + + if (status == "1") + { + return (true, string.Empty); + } + + _logger.LogWarning("短信接口失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return (false, string.IsNullOrEmpty(msg) ? "请求失败" : msg); + } + catch (Exception ex) + { + _logger.LogError(ex, "短信接口请求异常"); + return (false, $"网络请求失败: {ex.Message}"); + } + } + /// + /// 手机号注册:POST {BaseUrl}/prod-api/work_main/user/phone_register。 + /// code(验证码)、phone、psw(密码)必填;其余可选。 + /// 注意:注册接口成功状态码为 1,响应 data 为 boolean。 + /// + public Task<(bool Success, string Message)> RegisterByPhoneAsync(string code, string phone, string psw, string nickName = "", string sex = "", string local = "", string image = "") + { + return RegisterCoreAsync(WebApiAddress.PhoneRegister, code, phone, string.Empty, psw, nickName, sex, local, image); + } + + /// + /// 邮箱注册:POST {BaseUrl}/prod-api/work_main/user/user/email_register。 + /// code(验证码)、email、psw(密码)必填;其余可选。 + /// 注意:注册接口成功状态码为 1,响应 data 为 boolean。 + /// + public Task<(bool Success, string Message)> RegisterByEmailAsync(string code, string email, string psw, string nickName = "", string sex = "", string local = "", string image = "") + { + return RegisterCoreAsync(WebApiAddress.EmailRegister, code, string.Empty, email, psw, nickName, sex, local, image); + } + + /// 手机号/邮箱注册公共实现(两接口参数结构一致,仅路径不同)。 + private async Task<(bool Success, string Message)> RegisterCoreAsync( + string endpoint, string code, string phone, string email, string psw, + string nickName, string sex, string local, string image) + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{endpoint}"; + var requestData = new + { + code = code, + phone = phone, + email = email, + psw = psw, + nickName = nickName, + sex = sex, + local = local, + image = image + }; + string sendData = JsonConvert.SerializeObject(requestData); + + var response = await _httpClientService.PostJsonWithTimeoutAsync( + url, + sendData, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + string status = jobj["code"]?.ToString() ?? string.Empty; + string msg = jobj["msg"]?.ToString() ?? string.Empty; + + if (status == "1") + { + return (true, string.Empty); + } + + _logger.LogWarning("注册失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return (false, string.IsNullOrEmpty(msg) ? "注册失败" : msg); + } + catch (Exception ex) + { + _logger.LogError(ex, "注册请求异常"); + return (false, $"网络请求失败: {ex.Message}"); + } + } + #region Banner相关 + public async Task?> GetBanner(string languagecode = "") + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.BannerAll}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAsync( + url, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj["data"]?.ToString(); + return JsonConvert.DeserializeObject>(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取用户信息数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取Banner失败"); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetUserInfo)); + } + + public async Task> GetBannerCache() + { + var result = await _cacheService.GetOrCreateAsync>( + key: CacheKey_Banner, + dataFetchFunc: () => GetBanner(""), + expiry: _cacheExpiry + ); + return result ?? new List(); + } + #endregion + #region 用户信息和积分相关 + public async Task GetUserInfo(string token) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.CommonProfile}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync( + url, + token, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj["data"]["user"]?.ToString(); + return JsonConvert.DeserializeObject(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取用户信息数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户信息数据失败: {Token}", token); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetUserInfo)); + } + + public async Task GetUserIntegral(string token) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.Integral}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync( + url, + token, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj["data"]?.ToString(); + return JsonConvert.DeserializeObject(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取用户信息数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户信息数据失败: {Token}", token); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetUserIntegral)); + } + + public async Task GetKeyLogList(string token, int currentindex = 1, int pagesize = 20) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.LogList}"; + var requestData = new + { + pageNum = currentindex, + pageSize = pagesize, + language = _settingsService.Load().Language + }; + + string sendData = JsonConvert.SerializeObject(requestData); + // 使用带超时的HTTP请求 + var response = await _httpClientService.PostJsonWithTimeoutAndTokenAsync( + url, + sendData, + token, + TimeSpan.FromSeconds(15)); + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + if (status == "1") + { + var jsonstr = jobj?.ToString(); + return JsonConvert.DeserializeObject(jsonstr) ?? new ApiResponseRecord(); + } + else + { + _logger.LogWarning("获取用户信息数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return new ApiResponseRecord(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户信息数据失败: {Token}", token); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetKeyLogList)); + } + + public async Task GetUserIntegralHistroy(string token) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.IntegralHistroy}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync( + url, + token, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj?.ToString(); + return JsonConvert.DeserializeObject(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取用户积分数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户积分数据失败: {Token}", token); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetUserIntegralHistroy)); + } + + /// + /// 用户烧录信息添加 + /// + /// + /// + /// + /// + public Task BurnAddInfo(string token, BurnItem burnItem) + { + throw new NotImplementedException(); + } + /// + /// 添加积分 + /// + /// + /// + /// + /// + public Task AddIntegral(string token, BurnKeyItem burnKeyItem) + { + throw new NotImplementedException(); + } + #endregion + #region K3tool 云数据包相关 + /// + /// 获取K3tool 云数据包信息 + /// + /// + /// + /// + /// + public async Task GetOssDataCache(string token, string ossType = "K3tool_bin") + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + //测试 等待调整 + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.OssDataCache}/{WebApiAddress.K3ToolBin}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync( + url, + token, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj?.ToString(); + return JsonConvert.DeserializeObject(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取云数据数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取云数据数据失败: {Token}", token); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetOssDataCache)); + } + #endregion + #region 胎压 + /// + /// 获取所有胎压类型 + /// + /// + /// + /// + /// + public async Task?> GetTpmsType(string token, int? id) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.TpmsType}?parentId={id}&language={_settingsService.Load().Language}"; + + // 使用带超时的HTTP请求 + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync( + url, + token, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj["data"]?.ToString(); + return JsonConvert.DeserializeObject>(jsonstr) ?? null; + } + else + { + _logger.LogWarning("获取胎压类型失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取胎压类型失败: {Token}", token); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetTpmsType)); + } + + #endregion + } +} diff --git a/AIK.Service/Service/BouncyCastleTlsHandler.cs b/AIK.Service/Service/BouncyCastleTlsHandler.cs new file mode 100644 index 0000000..5df80dd --- /dev/null +++ b/AIK.Service/Service/BouncyCastleTlsHandler.cs @@ -0,0 +1,777 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + /// + /// 基于 BouncyCastle 纯托管 TLS 实现的 HttpMessageHandler,专门用于 Win7。 + /// 背景:Win7 系统 SChannel 默认未启用 TLS 1.2(且补丁/注册表方案常因精简系统、权限、未重启而不生效), + /// 而 .NET Framework 的 HttpClient/HttpWebRequest 全部走 SChannel,代码层无法绕过。 + /// BouncyCastle 的 TLS 客户端是纯托管实现(Org.BouncyCastle.Crypto.Tls),不依赖系统 SChannel, + /// 因此在 Win7 上可直接协商 TLS 1.2 与 key.aikkey.cn 握手。 + /// 本 Handler 内部流程:TCP 连接 → BouncyCastle TLS 1.2 握手(含 SNI)→ HTTP/1.1 请求/响应。 + /// + public class BouncyCastleTlsHandler : HttpMessageHandler + { + private const int DefaultTimeoutMilliseconds = 30000; + private const int IdleTimeoutMilliseconds = 60000; // 流式下载:响应体每段读取的空闲超时 + private const int MaxRedirects = 5; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var currentRequest = request; + int redirectCount = 0; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + var response = await ExecuteSingleAsync(currentRequest, cancellationToken).ConfigureAwait(false); + + int status = (int)response.StatusCode; + bool isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308; + if (!isRedirect || redirectCount >= MaxRedirects) + { + return response; + } + + var location = response.Headers.Location; + response.Dispose(); + if (location == null) + { + return new HttpResponseMessage(response.StatusCode); // 无 Location,直接返回原状态 + } + + redirectCount++; + var nextUri = location.IsAbsoluteUri ? location : new Uri(currentRequest.RequestUri, location); + var method = currentRequest.Method; + + // 301/302/303 对非 GET/HEAD 请求按 HTTP 规范转成 GET;307/308 保持原方法 + if ((status == 301 || status == 302 || status == 303) && method != HttpMethod.Get && method != HttpMethod.Head) + { + method = HttpMethod.Get; + } + + var next = new HttpRequestMessage(method, nextUri); + // 携带 token 等业务头 + foreach (var header in currentRequest.Headers) + { + next.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + if (method == HttpMethod.Get || method == HttpMethod.Head) + { + currentRequest.Content?.Dispose(); + } + else if (currentRequest.Content != null) + { + next.Content = new ByteArrayContent(await currentRequest.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); + foreach (var header in currentRequest.Content.Headers) + { + next.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + currentRequest = next; + } + } + + /// + /// 流式发送:仅读取响应头即返回,响应体通过 StreamContent 逐段读取(每段读取带 60s 空闲超时,无总时长限制), + /// 专用于大文件下载,解决原实现"30s 总超时 + 全量读内存"导致的大文件必失败问题。 + /// 调用方负责 Dispose 响应(会关闭底层 TLS 连接并发出 close_notify)。 + /// + public async Task SendStreamingAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var currentRequest = request; + int redirectCount = 0; + + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + var response = await ExecuteStreamingAsync(currentRequest, cancellationToken).ConfigureAwait(false); + + int status = (int)response.StatusCode; + bool isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308; + if (!isRedirect || redirectCount >= MaxRedirects) + { + return response; + } + + var location = response.Headers.Location; + response.Dispose(); // 重定向不读取 body,直接关闭连接 + if (location == null) + { + return new HttpResponseMessage(response.StatusCode); // 无 Location,直接返回原状态 + } + + redirectCount++; + var nextUri = location.IsAbsoluteUri ? location : new Uri(currentRequest.RequestUri, location); + var method = currentRequest.Method; + + // 301/302/303 对非 GET/HEAD 请求按 HTTP 规范转成 GET;307/308 保持原方法 + if ((status == 301 || status == 302 || status == 303) && method != HttpMethod.Get && method != HttpMethod.Head) + { + method = HttpMethod.Get; + } + + var next = new HttpRequestMessage(method, nextUri); + // 携带 token 等业务头 + foreach (var header in currentRequest.Headers) + { + next.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + if (method == HttpMethod.Get || method == HttpMethod.Head) + { + currentRequest.Content?.Dispose(); + } + else if (currentRequest.Content != null) + { + next.Content = new ByteArrayContent(await currentRequest.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); + foreach (var header in currentRequest.Content.Headers) + { + next.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + currentRequest = next; + } + } + + /// 执行单次 HTTP 请求(不处理重定向)。 + private async Task ExecuteSingleAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var uri = request.RequestUri; + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(DefaultTimeoutMilliseconds); + var ct = timeoutCts.Token; + + // ---- 1. TCP 连接 ---- + using var tcp = new TcpClient(); + try + { + var connectTask = tcp.ConnectAsync(uri.Host, uri.Port); + await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct)).ConfigureAwait(false); + ct.ThrowIfCancellationRequested(); + await connectTask.ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + throw new HttpRequestException($"TCP 连接失败: {uri.Host}:{uri.Port} — {ex.Message}", ex); + } + + // ---- 2. BouncyCastle TLS 1.2 握手(复用 BouncyCastleTlsHelper,含 SNI 与证书链校验) ---- + Stream tlsStream; + try + { + tlsStream = await Task.Run(() => BouncyCastleTlsHelper.ConnectTlsStream(tcp.GetStream(), uri.Host), ct).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + throw new HttpRequestException($"TLS 1.2 握手失败(BouncyCastle): {uri.Host}:{uri.Port} — {ex.Message}", ex); + } + + // BufferedStream.Dispose 会连带释放内层 TLS 流(close_notify),因此无需单独 using tlsStream + using var buffered = new BufferedStream(tlsStream); + try + { + // ---- 3. 发送 HTTP 请求 ---- + byte[] bodyBytes = null; + if (request.Content != null) + { + bodyBytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + + var sb = new StringBuilder(); + var pathQuery = uri.PathAndQuery; + if (string.IsNullOrEmpty(pathQuery)) pathQuery = "/"; + sb.Append(request.Method.Method).Append(' ').Append(pathQuery).Append(" HTTP/1.1\r\n"); + sb.Append("Host: ").Append(uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}").Append("\r\n"); + sb.Append("Connection: close\r\n"); + + // 业务请求头(User-Agent 多值用空格连接,其余用逗号连接) + foreach (var header in request.Headers) + { + bool isUserAgent = string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase); + sb.Append(header.Key).Append(": ") + .Append(string.Join(isUserAgent ? " " : ",", header.Value)).Append("\r\n"); + } + // Content 头(Content-Type / Content-Length 等) + if (request.Content != null) + { + foreach (var header in request.Content.Headers) + { + sb.Append(header.Key).Append(": ") + .Append(string.Join(",", header.Value)).Append("\r\n"); + } + } + sb.Append("\r\n"); + + var headBytes = Encoding.ASCII.GetBytes(sb.ToString()); + await buffered.WriteAsync(headBytes, 0, headBytes.Length, ct).ConfigureAwait(false); + if (bodyBytes != null && bodyBytes.Length > 0) + { + await buffered.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct).ConfigureAwait(false); + } + await buffered.FlushAsync(ct).ConfigureAwait(false); + + // ---- 4. 读取响应头 ---- + string statusLine = await ReadLineAsync(buffered, ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(statusLine)) + { + throw new HttpRequestException("服务器返回空响应(连接被关闭)"); + } + + var statusParts = statusLine.Split(' '); + if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal)) + { + throw new HttpRequestException($"无法解析 HTTP 状态行: {statusLine}"); + } + int statusCode = int.Parse(statusParts[1], CultureInfo.InvariantCulture); + string reasonPhrase = statusParts.Length > 2 ? string.Join(" ", statusParts, 2, statusParts.Length - 2) : string.Empty; + + var responseHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); + string line; + while (!string.IsNullOrEmpty(line = await ReadLineAsync(buffered, ct).ConfigureAwait(false))) + { + int colon = line.IndexOf(':'); + if (colon <= 0) continue; + var name = line.Substring(0, colon).Trim(); + var value = line.Substring(colon + 1).Trim(); + if (!responseHeaders.TryGetValue(name, out var list)) + { + list = new List(); + responseHeaders[name] = list; + } + list.Add(value); + } + + // ---- 5. 读取响应体 ---- + byte[] body = await ReadBodyAsync(buffered, responseHeaders, ct).ConfigureAwait(false); + + // ---- 6. 解压(兼容原 HttpClientHandler.AutomaticDecompression 行为) ---- + if (responseHeaders.TryGetValue("Content-Encoding", out var encodings) && encodings.Count > 0) + { + string encoding = encodings[0].ToLowerInvariant(); + if (encoding == "gzip" && body.Length > 0) + { + using var gz = new GZipStream(new MemoryStream(body), CompressionMode.Decompress); + body = ReadAllBytes(gz); + responseHeaders.Remove("Content-Encoding"); + } + else if (encoding == "deflate" && body.Length > 0) + { + using var def = new DeflateStream(new MemoryStream(body), CompressionMode.Decompress); + body = ReadAllBytes(def); + responseHeaders.Remove("Content-Encoding"); + } + } + + // ---- 7. 包装为 HttpResponseMessage ---- + var msg = new HttpResponseMessage((HttpStatusCode)statusCode) + { + ReasonPhrase = string.IsNullOrEmpty(reasonPhrase) ? null : reasonPhrase, + Content = new ByteArrayContent(body) + }; + foreach (var kv in responseHeaders) + { + if (IsContentHeader(kv.Key)) + { + msg.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + else + { + msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } + return msg; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (OperationCanceledException ex) + { + throw new HttpRequestException($"请求超时({DefaultTimeoutMilliseconds}ms): {uri.Host}", ex); + } + catch (Exception ex) when (!(ex is HttpRequestException)) + { + throw new HttpRequestException($"HTTP 请求失败: {uri.Host} — {ex.Message}", ex); + } + // 注:tcp/tlsStream/buffered 均为 using 声明,按反向顺序释放(buffered → tlsStream → tcp), + // 确保 BouncyCastle 在 socket 关闭前发出 close_notify,避免 internal_error。 + } + + /// 执行单次流式 HTTP 请求(不处理重定向)。响应体不预读,交给调用方通过 StreamContent 逐段读取。 + private async Task ExecuteStreamingAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var uri = request.RequestUri; + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(DefaultTimeoutMilliseconds); // 仅覆盖:连接/握手/发送/读响应头 + var ct = timeoutCts.Token; + + // ---- 1. TCP 连接 ---- + var tcp = new TcpClient(); + try + { + var connectTask = tcp.ConnectAsync(uri.Host, uri.Port); + await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct)).ConfigureAwait(false); + ct.ThrowIfCancellationRequested(); + await connectTask.ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + tcp.Dispose(); + throw new HttpRequestException($"TCP 连接失败: {uri.Host}:{uri.Port} — {ex.Message}", ex); + } + + // ---- 2. BouncyCastle TLS 1.2 握手(复用 BouncyCastleTlsHelper,含 SNI 与证书链校验) ---- + Stream tlsStream; + try + { + tlsStream = await Task.Run(() => BouncyCastleTlsHelper.ConnectTlsStream(tcp.GetStream(), uri.Host), ct).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + tcp.Dispose(); + throw new HttpRequestException($"TLS 1.2 握手失败(BouncyCastle): {uri.Host}:{uri.Port} — {ex.Message}", ex); + } + + var buffered = new BufferedStream(tlsStream); + try + { + // ---- 3. 发送 HTTP 请求 ---- + byte[] bodyBytes = null; + if (request.Content != null) + { + bodyBytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + + var sb = new StringBuilder(); + var pathQuery = uri.PathAndQuery; + if (string.IsNullOrEmpty(pathQuery)) pathQuery = "/"; + sb.Append(request.Method.Method).Append(' ').Append(pathQuery).Append(" HTTP/1.1\r\n"); + sb.Append("Host: ").Append(uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}").Append("\r\n"); + sb.Append("Connection: close\r\n"); + + // 业务请求头(User-Agent 多值用空格连接,其余用逗号连接) + foreach (var header in request.Headers) + { + bool isUserAgent = string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase); + sb.Append(header.Key).Append(": ") + .Append(string.Join(isUserAgent ? " " : ",", header.Value)).Append("\r\n"); + } + // Content 头(Content-Type / Content-Length 等) + if (request.Content != null) + { + foreach (var header in request.Content.Headers) + { + sb.Append(header.Key).Append(": ") + .Append(string.Join(",", header.Value)).Append("\r\n"); + } + } + sb.Append("\r\n"); + + var headBytes = Encoding.ASCII.GetBytes(sb.ToString()); + await buffered.WriteAsync(headBytes, 0, headBytes.Length, ct).ConfigureAwait(false); + if (bodyBytes != null && bodyBytes.Length > 0) + { + await buffered.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct).ConfigureAwait(false); + } + await buffered.FlushAsync(ct).ConfigureAwait(false); + + // ---- 4. 读取响应头 ---- + string statusLine = await ReadLineAsync(buffered, ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(statusLine)) + { + throw new HttpRequestException("服务器返回空响应(连接被关闭)"); + } + + var statusParts = statusLine.Split(' '); + if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal)) + { + throw new HttpRequestException($"无法解析 HTTP 状态行: {statusLine}"); + } + int statusCode = int.Parse(statusParts[1], CultureInfo.InvariantCulture); + string reasonPhrase = statusParts.Length > 2 ? string.Join(" ", statusParts, 2, statusParts.Length - 2) : string.Empty; + + var responseHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); + string line; + while (!string.IsNullOrEmpty(line = await ReadLineAsync(buffered, ct).ConfigureAwait(false))) + { + int colon = line.IndexOf(':'); + if (colon <= 0) continue; + var name = line.Substring(0, colon).Trim(); + var value = line.Substring(colon + 1).Trim(); + if (!responseHeaders.TryGetValue(name, out var list)) + { + list = new List(); + responseHeaders[name] = list; + } + list.Add(value); + } + + // ---- 5. 构造流式响应体(chunked 解码 + gzip/deflate 解压 + 每段空闲超时) ---- + var bodyStream = CreateBodyStream(buffered, responseHeaders); + + // ---- 6. 包装为 HttpResponseMessage ---- + var msg = new HttpResponseMessage((HttpStatusCode)statusCode) + { + ReasonPhrase = string.IsNullOrEmpty(reasonPhrase) ? null : reasonPhrase, + Content = new StreamContent(bodyStream) + }; + foreach (var kv in responseHeaders) + { + if (IsContentHeader(kv.Key)) + { + msg.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + else + { + msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } + // 连接所有权移交给 bodyStream,Dispose 响应时按序关闭(buffered → tlsStream → tcp) + ((TlsBodyStream)bodyStream).AttachConnection(tcp, buffered); + return msg; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + buffered.Dispose(); + tcp.Dispose(); + throw; + } + catch (OperationCanceledException ex) + { + buffered.Dispose(); + tcp.Dispose(); + throw new HttpRequestException($"请求超时({DefaultTimeoutMilliseconds}ms): {uri.Host}", ex); + } + catch (Exception ex) when (!(ex is HttpRequestException)) + { + buffered.Dispose(); + tcp.Dispose(); + throw new HttpRequestException($"HTTP 请求失败: {uri.Host} — {ex.Message}", ex); + } + } + + /// + /// 按响应头构造流式读取链:Transfer-Encoding: chunked → ChunkedDecodingStream;Content-Encoding → gzip/deflate 解压流。 + /// 连接(tcp/buffered)随后通过 TlsBodyStream.AttachConnection 移交。 + /// + private static TlsBodyStream CreateBodyStream(Stream buffered, Dictionary> headers) + { + Stream stream = buffered; + + if (headers.TryGetValue("Transfer-Encoding", out var tes)) + { + string te = string.Join(",", tes).ToLowerInvariant(); + if (te.Contains("chunked")) + { + stream = new ChunkedDecodingStream(stream); + } + } + + if (headers.TryGetValue("Content-Encoding", out var encodings) && encodings.Count > 0) + { + string encoding = encodings[0].ToLowerInvariant(); + if (encoding == "gzip") + { + stream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); + } + else if (encoding == "deflate") + { + stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true); + } + // 解压后字节数不再等于 Content-Length,移除该头避免下游按 Content-Length 校验/判进度时误判 + headers.Remove("Content-Encoding"); + headers.Remove("Content-Length"); + } + + return new TlsBodyStream(stream, headers); + } + + /// 按 Transfer-Encoding / Content-Length / EOF 读取响应体。 + private static async Task ReadBodyAsync(Stream stream, Dictionary> headers, CancellationToken ct) + { + if (headers.TryGetValue("Transfer-Encoding", out var tes)) + { + string te = string.Join(",", tes).ToLowerInvariant(); + if (te.Contains("chunked")) + { + return await ReadChunkedAsync(stream, ct).ConfigureAwait(false); + } + } + + if (headers.TryGetValue("Content-Length", out var cl) && cl.Count > 0 + && long.TryParse(cl[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long length)) + { + return await ReadExactlyAsync(stream, length, ct).ConfigureAwait(false); + } + + // 无长度信息(Connection: close 场景)→ 读到 EOF + return await ReadAllAsync(stream, ct).ConfigureAwait(false); + } + + private static async Task ReadChunkedAsync(Stream stream, CancellationToken ct) + { + using var ms = new MemoryStream(); + while (true) + { + string sizeLine = await ReadLineAsync(stream, ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(sizeLine)) break; + int semi = sizeLine.IndexOf(';'); + if (semi >= 0) sizeLine = sizeLine.Substring(0, semi); + int size = int.Parse(sizeLine.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + if (size == 0) + { + // 读取 trailer 直到空行 + while (!string.IsNullOrEmpty(await ReadLineAsync(stream, ct).ConfigureAwait(false))) { } + break; + } + var chunk = await ReadExactlyAsync(stream, size, ct).ConfigureAwait(false); + ms.Write(chunk, 0, chunk.Length); + await ReadExactlyAsync(stream, 2, ct).ConfigureAwait(false); // \r\n + } + return ms.ToArray(); + } + + private static async Task ReadExactlyAsync(Stream stream, long count, CancellationToken ct) + { + if (count > int.MaxValue) throw new HttpRequestException("响应体过大"); + var buffer = new byte[(int)count]; + int offset = 0; + while (offset < buffer.Length) + { + int read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, ct).ConfigureAwait(false); + if (read == 0) throw new HttpRequestException($"响应体提前结束(期望 {count} 字节,实际 {offset})"); + offset += read; + } + return buffer; + } + + private static async Task ReadAllAsync(Stream stream, CancellationToken ct) + { + using var ms = new MemoryStream(); + var buffer = new byte[81920]; + while (true) + { + int read = await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false); + if (read == 0) break; + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } + + /// 逐行读取(\r\n 结尾,不含结尾符)。 + private static async Task ReadLineAsync(Stream stream, CancellationToken ct) + { + var sb = new StringBuilder(); + var buffer = new byte[1]; + int prev = -1; + while (true) + { + int read = await stream.ReadAsync(buffer, 0, 1, ct).ConfigureAwait(false); + if (read == 0) + { + if (sb.Length == 0) return null; + break; // EOF,返回已收集内容 + } + int b = buffer[0]; + if (prev == '\r' && b == '\n') + { + sb.Remove(sb.Length - 1, 1); // 去掉 \r + break; + } + prev = b; + sb.Append((char)b); + } + return sb.ToString(); + } + + private static byte[] ReadAllBytes(Stream stream) + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + + private static readonly HashSet ContentHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Content-Type", "Content-Length", "Content-Encoding", "Content-Language", + "Content-Location", "Content-Range", "Content-Disposition", "Content-MD5", + "Expires", "Last-Modified", "Allow", "Content-Transfer-Encoding", "Content-Security-Policy" + }; + + private static bool IsContentHeader(string name) => ContentHeaders.Contains(name); + + /// + /// 流式响应体读取流:每段 ReadAsync 带空闲超时(无总时长限制),Dispose 时按序关闭 + /// buffered → tlsStream(close_notify)→ tcp,连接由 AttachConnection 在响应成功后移交。 + /// + private sealed class TlsBodyStream : Stream + { + private readonly Stream _inner; // 解码链后的流(可能为 ChunkedDecodingStream / GZipStream 包装) + private readonly Dictionary> _headers; + private TcpClient _tcp; + private Stream _buffered; + private bool _disposed; + + public TlsBodyStream(Stream inner, Dictionary> headers) + { + _inner = inner; + _headers = headers; + } + + public void AttachConnection(TcpClient tcp, Stream buffered) + { + _tcp = tcp; + _buffered = buffered; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override void Flush() { } + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + using var idleCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + idleCts.CancelAfter(IdleTimeoutMilliseconds); + try + { + return await _inner.ReadAsync(buffer, offset, count, idleCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new HttpRequestException($"读取响应数据超时(空闲 {IdleTimeoutMilliseconds}ms 无数据)"); + } + } + + protected override void Dispose(bool disposing) + { + if (_disposed) return; + _disposed = true; + if (disposing) + { + try { _inner.Dispose(); } catch { } // 先关闭解码链(GZipStream 等 leaveOpen 不关底层) + try { _buffered?.Dispose(); } catch { } // buffered → tlsStream close_notify + try { _tcp?.Dispose(); } catch { } + } + base.Dispose(disposing); + } + } + + /// HTTP chunked 分块解码流:隐藏分块帧,向调用方暴露连续的解码后数据。 + private sealed class ChunkedDecodingStream : Stream + { + private readonly Stream _inner; + private long _chunkRemaining; + private bool _finished; + + public ChunkedDecodingStream(Stream inner) => _inner = inner; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override void Flush() { } + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_finished) return 0; + + while (_chunkRemaining == 0) + { + // 读取下一个 chunk 大小行(hex) + string sizeLine = await ReadChunkLineAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrEmpty(sizeLine)) + { + // EOF 提前结束:正常终止必须读到 0 长度的终止块,这里说明连接被中途掐断 + throw new HttpRequestException("chunked 响应体提前结束(未读到终止块)"); + } + int semi = sizeLine.IndexOf(';'); + if (semi >= 0) sizeLine = sizeLine.Substring(0, semi); + _chunkRemaining = long.Parse(sizeLine.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + if (_chunkRemaining == 0) + { + // 终止块:读取 trailer 直到空行 + while (!string.IsNullOrEmpty(await ReadChunkLineAsync(cancellationToken).ConfigureAwait(false))) { } + _finished = true; + return 0; + } + } + + int toRead = (int)Math.Min(count, _chunkRemaining); + int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken).ConfigureAwait(false); + if (read == 0) throw new HttpRequestException("chunked 响应体提前结束"); + _chunkRemaining -= read; + if (_chunkRemaining == 0) + { + // 吃掉 chunk 后的 \r\n + var crlf = new byte[2]; + int got = 0; + while (got < 2) + { + int r = await _inner.ReadAsync(crlf, got, 2 - got, cancellationToken).ConfigureAwait(false); + if (r == 0) break; + got += r; + } + } + return read; + } + + /// 逐字节读一行(\r\n 结尾,不含结尾符),复用于 chunk 大小行与 trailer。 + private async Task ReadChunkLineAsync(CancellationToken cancellationToken) + { + var sb = new StringBuilder(); + var buffer = new byte[1]; + int prev = -1; + while (true) + { + int read = await _inner.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); + if (read == 0) + { + if (sb.Length == 0) return null; + break; + } + int b = buffer[0]; + if (prev == '\r' && b == '\n') + { + sb.Remove(sb.Length - 1, 1); // 去掉 \r + break; + } + prev = b; + sb.Append((char)b); + } + return sb.ToString(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) _inner.Dispose(); + base.Dispose(disposing); + } + } + } +} diff --git a/AIK.Service/Service/BouncyCastleTlsHelper.cs b/AIK.Service/Service/BouncyCastleTlsHelper.cs new file mode 100644 index 0000000..cf85f14 --- /dev/null +++ b/AIK.Service/Service/BouncyCastleTlsHelper.cs @@ -0,0 +1,93 @@ +using Org.BouncyCastle.Crypto.Tls; +using Org.BouncyCastle.Security; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace AIK.Service.Service +{ + /// + /// BouncyCastle 纯托管 TLS 1.2 握手封装,供 HTTP(BouncyCastleTlsHandler)与 + /// WebSocket(BouncyCastleWebSocketClient)共用。完全绕过 Win7 SChannel 对 TLS 1.2 的限制。 + /// + internal static class BouncyCastleTlsHelper + { + /// + /// 在已建立的 TCP 流上执行 BouncyCastle TLS 1.2 握手(含 SNI 与证书链校验), + /// 返回可读写的 TLS 流。握手为阻塞操作,调用方可在后台线程执行。 + /// + public static Stream ConnectTlsStream(Stream innerStream, string host) + { + var protocol = new TlsClientProtocol(innerStream, new SecureRandom()); + protocol.Connect(new BouncyTlsClient(host)); + return protocol.Stream; + } + + /// + /// BouncyCastle TLS 1.2 客户端,支持 SNI(server_name 扩展)。 + /// 证书验证:使用 .NET X509Chain 校验,RevocationMode=NoCheck 以兼容 Win7 离线环境。 + /// + private sealed class BouncyTlsClient : DefaultTlsClient + { + private readonly string _host; + + public BouncyTlsClient(string host) + : base() + { + _host = host; + } + + public override IDictionary GetClientExtensions() + { + var extensions = base.GetClientExtensions() ?? new Hashtable(); + // SNI:多域名虚拟主机(CDN/nginx)必须带 server_name,否则握手被拒 + var serverNames = new ServerNameList(new List { new ServerName(0, _host) }); + TlsExtensionsUtilities.AddServerNameExtension(extensions, serverNames); + return extensions; + } + + public override TlsAuthentication GetAuthentication() => new BouncyTlsAuthentication(); + } + + private sealed class BouncyTlsAuthentication : TlsAuthentication + { + public void NotifyServerCertificate(Certificate serverCertificate) + { + if (serverCertificate == null || serverCertificate.Length == 0) + { + throw new TlsFatalAlert(AlertDescription.bad_certificate); + } + + // 将 BouncyCastle 证书转为 .NET X509Certificate2 并做链校验 + var chain = new X509Chain { ChainPolicy = { RevocationMode = X509RevocationMode.NoCheck } }; + var certs = new List(); + try + { + for (int i = 0; i < serverCertificate.Length; i++) + { + certs.Add(new X509Certificate2(serverCertificate.GetCertificateAt(i).GetEncoded())); + } + foreach (var extra in certs.Skip(1)) + { + chain.ChainPolicy.ExtraStore.Add(extra); + } + bool valid = chain.Build(certs[0]); + if (!valid) + { + var errors = string.Join("; ", chain.ChainStatus.Select(s => s.StatusInformation.Trim())); + throw new TlsFatalAlert(AlertDescription.bad_certificate, new Exception($"证书链校验失败: {errors}")); + } + } + finally + { + foreach (var c in certs) c.Dispose(); + } + } + + public TlsCredentials GetClientCredentials(Org.BouncyCastle.Crypto.Tls.CertificateRequest certificateRequest) => null; + } + } +} diff --git a/AIK.Service/Service/BouncyCastleWebSocketClient.cs b/AIK.Service/Service/BouncyCastleWebSocketClient.cs new file mode 100644 index 0000000..35728fb --- /dev/null +++ b/AIK.Service/Service/BouncyCastleWebSocketClient.cs @@ -0,0 +1,622 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace AIK.Service.Service +{ + /// WebSocket 连接状态(与 websocket-sharp 兼容的取值)。 + public enum WebSocketState + { + New, + Connecting, + Open, + Closing, + Closed + } + + /// WebSocket 关闭状态码(RFC 6455 常用值,与 websocket-sharp 兼容)。 + public enum CloseStatusCode : ushort + { + Normal = 1000, + GoingAway = 1001, + ProtocolError = 1002, + UnsupportedData = 1003, + NoStatus = 1005, + Abnormal = 1006, + InvalidData = 1007, + PolicyViolation = 1008, + TooBig = 1009, + MandatoryExtension = 1010, + ServerError = 1011, + TlsHandshakeFailure = 1015 + } + + /// 收到 WebSocket 消息的事件参数(与 websocket-sharp 兼容)。 + public class MessageEventArgs : EventArgs + { + public bool IsText { get; } + public bool IsBinary { get; } + public string Data { get; } + public byte[] RawData { get; } + + internal MessageEventArgs(bool isText, byte[] rawData) + { + IsText = isText; + IsBinary = !isText; + RawData = rawData ?? new byte[0]; + Data = isText ? Encoding.UTF8.GetString(RawData) : string.Empty; + } + } + + /// WebSocket 错误事件参数。 + public class ErrorEventArgs : EventArgs + { + public string Message { get; } + public Exception Exception { get; } + + internal ErrorEventArgs(string message, Exception exception = null) + { + Message = message; + Exception = exception; + } + } + + /// WebSocket 关闭事件参数。 + public class CloseEventArgs : EventArgs + { + public CloseStatusCode Code { get; } + public string Reason { get; } + public bool WasClean { get; } + + internal CloseEventArgs(CloseStatusCode code, string reason, bool wasClean = false) + { + Code = code; + Reason = reason ?? string.Empty; + WasClean = wasClean; + } + } + + /// + /// 基于 BouncyCastle 纯托管 TLS 的最小 RFC 6455 WebSocket 客户端,用于 Win7 扫码登录。 + /// 背景:websocket-sharp 内部硬编码 new SslStream(...)(走系统 SChannel),Win7 SChannel 未启用 TLS 1.2 + /// 时握手必然失败(关闭码 1015)。本客户端用 BouncyCastleTlsHelper 建立 TLS 1.2 连接后自行实现 + /// WebSocket 握手与帧编解码,完全绕过 SChannel。 + /// 事件模型与 websocket-sharp 兼容:OnMessage / OnError / OnClose,Send(string|byte[]),Close(code, reason)。 + /// + public class BouncyCastleWebSocketClient : IDisposable + { + private const string WsGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + private readonly Uri _uri; + private TcpClient _tcp; + private Stream _stream; + private WebSocketState _state = WebSocketState.New; + private Thread _receiveThread; + private readonly object _sendLock = new object(); + private volatile bool _disposed; + + public event EventHandler OnMessage; + public event EventHandler OnError; + public event EventHandler OnClose; + + /// 连接/关闭超时(默认 15 秒,与原先 websocket-sharp WaitTime 一致)。 + public TimeSpan WaitTime { get; set; } = TimeSpan.FromSeconds(15); + + public WebSocketState ReadyState => _state; + + public BouncyCastleWebSocketClient(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + throw new ArgumentException("WebSocket URL 不能为空", nameof(url)); + } + _uri = new Uri(url); + if (!string.Equals(_uri.Scheme, "wss", StringComparison.OrdinalIgnoreCase) + && !string.Equals(_uri.Scheme, "ws", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"不支持的 WebSocket 协议: {_uri.Scheme}", nameof(url)); + } + } + + /// + /// 建立连接:TCP → BouncyCastle TLS 1.2 握手 → WebSocket 升级握手,成功后启动接收线程。 + /// 同步阻塞(与 websocket-sharp Connect() 一致,调用方可 Task.Run 包装)。 + /// + public void Connect() + { + EnsureNotDisposed(); + if (_state != WebSocketState.New) + { + throw new InvalidOperationException($"当前状态不允许连接: {_state}"); + } + + _state = WebSocketState.Connecting; + var port = _uri.IsDefaultPort ? (_uri.Scheme == "wss" ? 443 : 80) : _uri.Port; + var host = _uri.Host; + + try + { + // ---- 1. TCP 连接(带超时) ---- + _tcp = new TcpClient(); + var connectTask = _tcp.ConnectAsync(host, port); + if (!connectTask.Wait(WaitTime)) + { + throw new TimeoutException($"TCP 连接超时: {host}:{port}"); + } + + // ---- 2. BouncyCastle TLS 1.2 握手(含 SNI + 证书链校验,绕过 SChannel) ---- + _stream = BouncyCastleTlsHelper.ConnectTlsStream(_tcp.GetStream(), host); + + // ---- 3. WebSocket 升级握手 ---- + PerformHandshake(host, port); + + // ---- 4. 启动接收线程 ---- + _state = WebSocketState.Open; + _receiveThread = new Thread(ReceiveLoop) + { + IsBackground = true, + Name = "BouncyCastleWebSocket-Receive" + }; + _receiveThread.Start(); + } + catch + { + _state = WebSocketState.Closed; + CloseResources(); + throw; + } + } + + /// 发送文本消息(客户端帧必须加掩码,RFC 6455)。 + public void Send(string data) + { + EnsureOpen(); + var bytes = Encoding.UTF8.GetBytes(data ?? string.Empty); + SendFrame(0x1, bytes); + } + + /// 发送二进制消息(STOMP 心跳 0x0A 等)。 + public void Send(byte[] data) + { + EnsureOpen(); + SendFrame(0x2, data ?? new byte[0]); + } + + /// 主动关闭连接(发送 Close 帧并等待接收线程退出)。 + public void Close(CloseStatusCode code, string reason) + { + if (_state == WebSocketState.Closed || _disposed) + { + return; + } + + _state = WebSocketState.Closing; + try + { + var reasonBytes = Encoding.UTF8.GetBytes(reason ?? string.Empty); + var payload = new byte[2 + reasonBytes.Length]; + payload[0] = (byte)((ushort)code >> 8); + payload[1] = (byte)((ushort)code & 0xFF); + Buffer.BlockCopy(reasonBytes, 0, payload, 2, reasonBytes.Length); + SendFrame(0x8, payload); + } + catch + { + // 发送 Close 帧失败也继续关闭资源 + } + + // 等待接收线程处理完对端 Close 回帧后自行退出 + var thread = _receiveThread; + if (thread != null && thread.IsAlive) + { + thread.Join(WaitTime); + } + CloseResources(); + _state = WebSocketState.Closed; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + try + { + if (_state == WebSocketState.Open || _state == WebSocketState.Closing) + { + Close(CloseStatusCode.GoingAway, "client disposed"); + } + } + catch + { + // 忽略清理异常 + } + finally + { + CloseResources(); + _state = WebSocketState.Closed; + } + } + + #region 内部实现 + + private void PerformHandshake(string host, int port) + { + var keyBytes = new byte[16]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(keyBytes); + } + var key = Convert.ToBase64String(keyBytes); + + var path = _uri.PathAndQuery; + if (string.IsNullOrEmpty(path)) + { + path = "/"; + } + + var sb = new StringBuilder(); + sb.Append("GET ").Append(path).Append(" HTTP/1.1\r\n"); + sb.Append("Host: ").Append(port == 443 ? host : $"{host}:{port}").Append("\r\n"); + sb.Append("Upgrade: websocket\r\n"); + sb.Append("Connection: Upgrade\r\n"); + sb.Append("Sec-WebSocket-Key: ").Append(key).Append("\r\n"); + sb.Append("Sec-WebSocket-Version: 13\r\n"); + sb.Append("\r\n"); + + var head = Encoding.ASCII.GetBytes(sb.ToString()); + _stream.Write(head, 0, head.Length); + _stream.Flush(); + + // ---- 读取响应状态行与响应头 ---- + var statusLine = ReadHandshakeLine(); + if (string.IsNullOrEmpty(statusLine)) + { + throw new IOException("WebSocket 升级握手无响应(连接被关闭)"); + } + if (!statusLine.Contains("101")) + { + throw new IOException($"WebSocket 升级握手失败: {statusLine}"); + } + + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + string line; + while (!string.IsNullOrEmpty(line = ReadHandshakeLine())) + { + int colon = line.IndexOf(':'); + if (colon <= 0) + { + continue; + } + var name = line.Substring(0, colon).Trim(); + var value = line.Substring(colon + 1).Trim(); + headers[name] = value; + } + + // 校验 Sec-WebSocket-Accept + var expectedAccept = Convert.ToBase64String( + SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + WsGuid))); + if (!headers.TryGetValue("Sec-WebSocket-Accept", out var accept) || accept != expectedAccept) + { + throw new IOException("WebSocket 升级握手校验失败(Sec-WebSocket-Accept 不匹配)"); + } + } + + private string ReadHandshakeLine() + { + var sb = new StringBuilder(); + var buf = new byte[1]; + int prev = -1; + while (true) + { + int read = _stream.Read(buf, 0, 1); + if (read == 0) + { + if (sb.Length == 0) + { + return null; + } + break; + } + int b = buf[0]; + if (prev == '\r' && b == '\n') + { + sb.Remove(sb.Length - 1, 1); // 去掉 \r + break; + } + prev = b; + sb.Append((char)b); + } + return sb.ToString(); + } + + /// 接收循环:解析帧,分派事件;处理 ping/pong/close/分片。 + private void ReceiveLoop() + { + var fragmentBuffer = new List(); + byte fragmentOpcode = 0; + try + { + while (_state == WebSocketState.Open || _state == WebSocketState.Closing) + { + int b0 = _stream.ReadByte(); + if (b0 < 0) + { + break; // 对端关闭连接 + } + int b1 = _stream.ReadByte(); + if (b1 < 0) + { + break; + } + + bool fin = (b0 & 0x80) != 0; + int opcode = b0 & 0x0F; + bool masked = (b1 & 0x80) != 0; + long payloadLen = b1 & 0x7F; + if (payloadLen == 126) + { + payloadLen = ReadUInt16BigEndian(); + } + else if (payloadLen == 127) + { + payloadLen = (long)ReadUInt64BigEndian(); + if (payloadLen > int.MaxValue) + { + throw new IOException("WebSocket 帧过大"); + } + } + + byte[] maskKey = null; + if (masked) + { + maskKey = ReadExactly(4); + } + + var payload = payloadLen > 0 ? ReadExactly((int)payloadLen) : new byte[0]; + if (masked && payloadLen > 0) + { + for (int i = 0; i < payload.Length; i++) + { + payload[i] ^= maskKey[i % 4]; + } + } + + switch (opcode) + { + case 0x0: // continuation + fragmentBuffer.AddRange(payload); + if (fin) + { + RaiseMessage(fragmentOpcode == 0x1, fragmentBuffer.ToArray()); + fragmentBuffer.Clear(); + } + break; + + case 0x1: // text + case 0x2: // binary + if (!fin) + { + fragmentOpcode = (byte)opcode; + fragmentBuffer.Clear(); + fragmentBuffer.AddRange(payload); + } + else + { + RaiseMessage(opcode == 0x1, payload); + } + break; + + case 0x8: // close + HandleCloseFrame(payload); + return; + + case 0x9: // ping → 回 pong + SendFrame(0xA, payload); + break; + + case 0xA: // pong + break; + } + } + } + catch (Exception ex) + { + if (_state != WebSocketState.Closed) + { + try + { + OnError?.Invoke(this, new ErrorEventArgs(ex.Message, ex)); + } + catch + { + // 事件处理器异常不影响连接清理 + } + } + } + finally + { + if (_state != WebSocketState.Closed) + { + _state = WebSocketState.Closed; + CloseResources(); + try + { + OnClose?.Invoke(this, new CloseEventArgs(CloseStatusCode.Abnormal, "connection closed")); + } + catch + { + // 忽略 + } + } + } + } + + private void HandleCloseFrame(byte[] payload) + { + CloseStatusCode code = CloseStatusCode.NoStatus; + string reason = string.Empty; + if (payload.Length >= 2) + { + code = (CloseStatusCode)(ushort)((payload[0] << 8) | payload[1]); + if (payload.Length > 2) + { + reason = Encoding.UTF8.GetString(payload, 2, payload.Length - 2); + } + } + + // 对端发起关闭,回一个 Close 帧 + try + { + SendFrame(0x8, payload.Length >= 2 ? payload : new byte[] { 0x03, 0xE8 }); + } + catch + { + // 忽略 + } + + _state = WebSocketState.Closed; + CloseResources(); + OnClose?.Invoke(this, new CloseEventArgs(code, reason, wasClean: true)); + } + + private void RaiseMessage(bool isText, byte[] rawData) + { + if (rawData == null || rawData.Length == 0) + { + return; + } + OnMessage?.Invoke(this, new MessageEventArgs(isText, rawData)); + } + + /// 发送一个 WebSocket 帧(客户端帧强制加掩码)。 + private void SendFrame(byte opcode, byte[] payload) + { + lock (_sendLock) + { + EnsureNotDisposed(); + var len = payload.Length; + var header = new List(14); + header.Add((byte)(0x80 | opcode)); // FIN=1 + + var mask = new byte[4]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(mask); + } + + if (len < 126) + { + header.Add((byte)(0x80 | len)); + } + else if (len <= 0xFFFF) + { + header.Add((byte)(0x80 | 126)); + header.Add((byte)(len >> 8)); + header.Add((byte)(len & 0xFF)); + } + else + { + header.Add((byte)(0x80 | 127)); + ulong l = (ulong)len; + for (int i = 7; i >= 0; i--) + { + header.Add((byte)(l >> (i * 8))); + } + } + header.AddRange(mask); + + var maskedPayload = new byte[len]; + for (int i = 0; i < len; i++) + { + maskedPayload[i] = (byte)(payload[i] ^ mask[i % 4]); + } + + _stream.Write(header.ToArray(), 0, header.Count); + if (len > 0) + { + _stream.Write(maskedPayload, 0, len); + } + _stream.Flush(); + } + } + + private ushort ReadUInt16BigEndian() + { + var b = ReadExactly(2); + return (ushort)((b[0] << 8) | b[1]); + } + + private ulong ReadUInt64BigEndian() + { + var b = ReadExactly(8); + ulong v = 0; + for (int i = 0; i < 8; i++) + { + v = (v << 8) | b[i]; + } + return v; + } + + private byte[] ReadExactly(int count) + { + var buffer = new byte[count]; + int offset = 0; + while (offset < count) + { + int read = _stream.Read(buffer, offset, count - offset); + if (read == 0) + { + throw new IOException("连接被对端关闭(读取中断)"); + } + offset += read; + } + return buffer; + } + + private void EnsureOpen() + { + EnsureNotDisposed(); + if (_state != WebSocketState.Open) + { + throw new InvalidOperationException($"WebSocket 未处于 Open 状态(当前 {_state})"); + } + } + + private void EnsureNotDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BouncyCastleWebSocketClient)); + } + } + + private void CloseResources() + { + try + { + _stream?.Dispose(); + } + catch + { + // 忽略 + } + try + { + _tcp?.Close(); + } + catch + { + // 忽略 + } + } + + #endregion + } +} diff --git a/AIK.Service/Service/CarfactoryService.cs b/AIK.Service/Service/CarfactoryService.cs new file mode 100644 index 0000000..0353d84 --- /dev/null +++ b/AIK.Service/Service/CarfactoryService.cs @@ -0,0 +1,327 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Common; +using AIK.Common.Interface; +using AIK.Common.SysCommon; +using AIK.Common.WebHelper; +using AIK.Models; +using AIK.Models.HttpPolicy; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Security.AccessControl; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.Service +{ + public class CarfactoryService : ICarfactoryService + { + //private static string weburlTest = "https://keytest.aik518.com"; + //public static string fileurl = "https://keytest.aik518.com/prod-api/work_file/file/file_download?objectName="; + //当前平台语言 + + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private readonly IRetryPolicyService _retryPolicyService; + private readonly RetryPolicyConfig _retryConfig; + private readonly IHttpClientService _httpClientService; + private readonly ISettingsService _settingsService; + + // 通过构造函数注入 + public CarfactoryService(IHttpClientFactory httpClientFactory, ILogger logger, IConfigurationService configurationService, IRetryPolicyService retryPolicyService, IHttpClientService httpClientService, ISettingsService settingsService) + { + _httpClientFactory = httpClientFactory; + _configurationService = configurationService; + _logger = logger; + _retryPolicyService = retryPolicyService; + _httpClientService = httpClientService; + _retryConfig = new RetryPolicyConfig + { + MaxRetryCount = 3, + RetryDelay = TimeSpan.FromSeconds(1), + DefaultTimeout = TimeSpan.FromSeconds(15) + }; + _settingsService = settingsService; + + } + #region 接口实现 + + + private static string GetVhetype(string vhetype) + { + //后台没有对应接口暂时赋值car + if (vhetype == "yssc" || vhetype == "sjxf" || vhetype == "bcq") + { + return "ysjs"; + } + if (vhetype == "car") + { + return string.Empty; + } + return string.Empty; + } + + #region 异步方法(带重试) + /// + /// 获取车辆品牌数据 + /// (车car,车库:barn 电动车:dd 摩托车:mt 后装:hz i2车库门:i2_barn i2电动车:i2_dd obd数据:obd ,原车钥匙解锁:ysjs) + /// + /// + /// + public async Task> GetCarfactoryClassListAsync(string vhetype) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + vhetype = GetVhetype(vhetype); + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.Vehicle_keyBrand_List}"; + + var requestData = new + { + brandType = vhetype, + language = _settingsService.Load().Language + }; + + string sendData = JsonConvert.SerializeObject(requestData); + + // 使用带超时的HTTP请求 + var response = await _httpClientService.PostJsonWithTimeoutAsync( + url, sendData, + TimeSpan.FromSeconds(15)); + + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + + if (status == "1") + { + var jsonstr = jobj["data"]?.ToString(); + return JsonConvert.DeserializeObject>(jsonstr) ?? new List(); + } + else + { + _logger.LogWarning("获取车辆品牌数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return new List(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取车辆品牌数据失败: {Vhetype}", vhetype); + throw; // 重新抛出,让重试机制处理 + } + }, nameof(GetCarfactoryClassListAsync)); + } + + public async Task> GetVehKeyClassListAsync(int id) + { + return await _retryPolicyService.ExecuteWithRetryAsync(async () => + { + try + { + List cflist = new List(); + string url = $"{_configurationService.ApiSettings.BaseUrl}{WebApiAddress.Vehicle_keyBrand_Detail}/{id}?language={_settingsService.Load().Language}"; + string response = await _httpClientService.GetStringWithTimeoutAsync(url, TimeSpan.FromSeconds(15)); + var jobj = JObject.Parse(response); + var status = jobj["code"]?.ToString(); + if (status == "1") + { + var dataObject = JObject.Parse(jobj["data"]?.ToString() ?? "{}"); + var typeList = dataObject["typeList"]?.ToString(); + return JsonConvert.DeserializeObject>(typeList) ?? new List(); + } + else + { + _logger.LogWarning("获取车辆钥匙数据失败,状态码: {Status}, 响应: {Response}", status, jobj.ToString()); + return new List(); + } + } + catch (HttpRequestException ex) + { + throw new HttpRequestException("无网络连接,请检测网络连接情况"); + } + catch (Exception ex) + { + _logger.LogError("执行方法: {MethodName},{Message}", nameof(GetVehKeyClassListAsync), ex.Message); + //未知异常默认返回空 + return new List(); + } + }, nameof(GetVehKeyClassListAsync)); + } + #endregion + + #region 同步方法 + /// + /// 获取车辆品牌数据 + /// (车car,车库:barn 电动车:dd 摩托车:mt 后装:hz i2车库门:i2_barn i2电动车:i2_dd obd数据:obd ,原车钥匙解锁:ysjs) + /// + /// + /// + public List GetCarfactoryClassList(string vhetype) + { + try + { + vhetype = GetVhetype(vhetype); + List cflist = new List(); + string url = string.Format("{0}/prod-api/work_key/vehicle_key/brand_list", _configurationService.ApiSettings.BaseUrl); + JObject patientinfo = new JObject(); + patientinfo["brandType"] = vhetype; + patientinfo["language"] = _settingsService.Load().Language; + + string sendData = JsonConvert.SerializeObject(patientinfo); + Newtonsoft.Json.Linq.JObject jobj = jobj = Newtonsoft.Json.Linq.JObject.Parse(WebHelper.PostJson(url, sendData)); + var status = jobj["code"].ToString(); + if (status == "1") + { + var jsonstr = jobj["data"].ToString(); + cflist = JsonConvert.DeserializeObject>(jsonstr); + } + else + { + _logger.LogInformation($"第三方状态码:{status},数据: {jobj.ToString()}"); + } + return cflist; + } + catch (Exception ex) + { + _logger.LogError("执行方法: {MethodName},{Message}", nameof(GetVehKeyImg), ex.Message); + return new List(); + } + } + /// + /// 获取车辆钥匙数据 + /// + /// + /// + public List GetVehKeyClassList(int id) + { + try + { + List cflist = new List(); + string url = string.Format("{0}/prod-api/work_key/vehicle_key/brand_detail/{1}?language={2}", _configurationService.ApiSettings.BaseUrl, id, _settingsService.Load().Language); + Newtonsoft.Json.Linq.JObject jobj = Newtonsoft.Json.Linq.JObject.Parse(WebHelper.HttpWebRequest(url, null, 10 * 1000, false)); + var status = jobj["code"].ToString(); + if (status == "1") + { + var jsonstr = Newtonsoft.Json.Linq.JObject.Parse(jobj["data"].ToString()); + cflist = JsonConvert.DeserializeObject>(jsonstr["typeList"].ToString()); + } + else + { + _logger.LogInformation($"第三方状态码:{status},数据: {jobj.ToString()}"); + } + return cflist; + } + catch (Exception ex) + { + _logger.LogError("执行方法: {MethodName},{Message}", nameof(GetVehKeyClassList), ex.Message); + return new List(); + } + } + #endregion + + + + /// + /// 获取K3TOOL和K3Genie固件版本数据 copying(k3mini),i2(k3tool),k3_jingling + /// + /// + [Obsolete] + public List GetVersionClass(string type) + { + try + { + List cflist = new List(); + string url = string.Format("{0}/prod-api/work_key/app/more_new?type={1}", _configurationService.ApiSettings.BaseUrl, type); + Newtonsoft.Json.Linq.JObject jobj = Newtonsoft.Json.Linq.JObject.Parse(WebHelper.HttpWebRequest(url, null, 10 * 1000, false)); + var status = jobj["code"].ToString(); + if (status == "1") + { + cflist = JsonConvert.DeserializeObject>(jobj["data"].ToString()); + } + return cflist; + } + catch (Exception ex) + { + _logger.LogError("执行方法: {MethodName},{Message}", nameof(GetVersionClass), ex.Message); + return new List(); + } + } + #endregion + + #region 获取车钥匙图片和Bin文件 + + /// + /// 获取车钥匙图片(WPF) + /// + /// + [Obsolete] + public async Task GetVehKeyImg(string url) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + if (GlobalImageCache.TryGetValue(url, out var cachedImage)) + return cachedImage; + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + using var response = await httpClient.GetAsync($"{_configurationService.ApiSettings.FileUrl}{url}"); + + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + var image = ImageUtils.StreamToBitmapImage(stream); + + if (image is not null) + GlobalImageCache.GetOrAdd(url, _ => image); + + return image; + } + catch (Exception ex) + { + _logger.LogError("执行方法: {MethodName},{Message}", nameof(GetVehKeyImg), ex.Message); + return null; + } + } + /// + /// 获取车钥匙升级文件 + /// + /// + [Obsolete] + public async Task GetVehKeyBin(string url) + { + try + { + using (var client = new HttpClient()) + { + using (var content = new MultipartFormDataContent()) + { + HttpResponseMessage response = await client.GetAsync(_configurationService.ApiSettings.FileUrl + url); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsByteArrayAsync(); + } + } + } + catch (Exception ex) + { + _logger.LogError("执行方法: {MethodName},{Message}", nameof(GetVehKeyBin), ex.Message); + return null; + } + } + + + + #endregion + } +} diff --git a/AIK.Service/Service/ConfigurationService.cs b/AIK.Service/Service/ConfigurationService.cs new file mode 100644 index 0000000..0be474e --- /dev/null +++ b/AIK.Service/Service/ConfigurationService.cs @@ -0,0 +1,236 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Models.SystemSettings; +using AIK.Service.IService; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class ConfigurationService : IConfigurationService + { + private readonly string _configFilePath; + private AppConfig _config; + private readonly object _lockObject = new object(); + public ConfigurationService() + { + _configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"); + _config = LoadConfiguration() ?? new AppConfig(); + } + + public string Environment => _config.Environment; + + public ApiSettings ApiSettings => _config.ApiSettings ?? new ApiSettings(); + + public CacheSettings CacheSettings => _config.CacheSettings ?? new CacheSettings(); + + public T GetSection(string sectionName) where T : new() + { + return sectionName.ToLower() switch + { + "apisettings" when typeof(T) == typeof(ApiSettings) => (T)(object)(ApiSettings), + "cachesettings" when typeof(T) == typeof(CacheSettings) => (T)(object)(CacheSettings), + _ => new T() + }; + } + + public string GetConnectionString(string name) => string.Empty; + + + #region 新增方法 - 修改配置 + + /// + /// 更新语言设置 + /// + public async Task UpdateLanguageAsync(string languageCode) + { + if (string.IsNullOrEmpty(languageCode)) + throw new ArgumentException("语言代码不能为空", nameof(languageCode)); + + lock (_lockObject) + { + if (_config.ApiSettings == null) + _config.ApiSettings = new ApiSettings(); + + _config.ApiSettings.Language = languageCode; + } + + await SaveConfigurationAsync(); + } + + /// + /// 更新 API 设置 + /// + public async Task UpdateApiSettingsAsync(Action updateAction) + { + if (updateAction == null) + throw new ArgumentNullException(nameof(updateAction)); + + lock (_lockObject) + { + if (_config.ApiSettings == null) + _config.ApiSettings = new ApiSettings(); + + updateAction(_config.ApiSettings); + } + + await SaveConfigurationAsync(); + } + + /// + /// 更新缓存设置 + /// + public async Task UpdateCacheSettingsAsync(Action updateAction) + { + if (updateAction == null) + throw new ArgumentNullException(nameof(updateAction)); + + lock (_lockObject) + { + if (_config.CacheSettings == null) + _config.CacheSettings = new CacheSettings(); + + updateAction(_config.CacheSettings); + } + + await SaveConfigurationAsync(); + } + + /// + /// 保存配置到文件 + /// + public async Task SaveConfigurationAsync() + { + try + { + var json = JsonConvert.SerializeObject(_config, Formatting.Indented); + File.WriteAllText(_configFilePath, json, Encoding.UTF8); + await ReloadConfigurationAsync(); + Debug.WriteLine($"配置已保存到: {_configFilePath}"); + } + catch (Exception ex) + { + Debug.WriteLine($"保存配置失败: {ex.Message}"); + throw new InvalidOperationException("保存配置文件失败", ex); + } + } + + /// + /// 重新加载配置 + /// + public async Task ReloadConfigurationAsync() + { + var newConfig = LoadConfiguration(); + if (newConfig != null) + { + lock (_lockObject) + { + _config = newConfig; + } + } + await Task.CompletedTask; + } + + #endregion + + #region 私有方法 + + private AppConfig LoadConfiguration() + { + try + { + if (File.Exists(_configFilePath)) + { + var json = File.ReadAllText(_configFilePath); + return JsonConvert.DeserializeObject(json); + } + + // 如果配置文件不存在,创建默认配置并保存 + var defaultConfig = CreateDefaultConfig(); + SaveConfigurationAsync().Wait(); // 同步保存默认配置 + return defaultConfig; + } + catch (Exception ex) + { + Debug.WriteLine($"配置文件加载失败: {ex.Message}"); + return CreateDefaultConfig(); + } + } + + private AppConfig CreateDefaultConfig() + { + return new AppConfig + { + Environment = Debugger.IsAttached ? "Development" : "Production", + ApiSettings = new ApiSettings + { + BaseUrl = Debugger.IsAttached ? "https://keytest.aik518.com" : "https://key.aik518.com", + Language = "zh", + Timeout = 30, + RetryCount = 3, + Endpoints = new ApiEndpoints + { + GetCarData = "/api/v1/cars", + GetCarModels = "/api/v1/models/{0}", + GetCarImages = "/api/v1/images/{0}", + Authentication = "/api/v1/auth/login" + } + }, + CacheSettings = new CacheSettings + { + DataCacheExpiryHours = 3, + ImageCacheExpiryDays = 7, + MaxCacheSizeMB = 100 + } + }; + } + + #endregion + + #region 便捷方法 + + /// + /// 获取当前语言 + /// + public string GetCurrentLanguage() + { + return _config?.ApiSettings?.Language ?? "zh"; + } + + /// + /// 检查语言是否支持 + /// + public bool IsLanguageSupported(string languageCode) + { + var supportedLanguages = new[] { "zh", "en", "pt", "es" }; + return supportedLanguages.Contains(languageCode); + } + + /// + /// 批量更新设置 + /// + public async Task UpdateSettingsAsync(Action updateAction) + { + if (updateAction == null) + throw new ArgumentNullException(nameof(updateAction)); + + lock (_lockObject) + { + updateAction(_config); + } + + await SaveConfigurationAsync(); + } + + #endregion +} +} diff --git a/AIK.Service/Service/FileBinService.cs b/AIK.Service/Service/FileBinService.cs new file mode 100644 index 0000000..5bb17a2 --- /dev/null +++ b/AIK.Service/Service/FileBinService.cs @@ -0,0 +1,245 @@ +using AIK.Common; +using AIK.Common.WebHelper; +using AIK.Models; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.Service +{ + public class FileBinService : IFileCacheService + { + private readonly string _cacheBaseDirectory; + private readonly IHttpClientService _httpClient; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private string GetCacheFilePath(string cacheKey) => Path.Combine(_cacheBaseDirectory, $"{cacheKey}.bin"); + public FileBinService(IConfigurationService configurationService, IHttpClientService httpClient, ILogger logger) + { + _configurationService = configurationService; + _httpClient = httpClient; + _logger = logger; + // 设置缓存目录,例如在用户本地应用数据文件夹下 + _cacheBaseDirectory = CommonString.BinCacheDirectory; + Directory.CreateDirectory(_cacheBaseDirectory); // 确保目录存在 + } + public async Task ClearAllCacheAsync() + { + await Task.Run(() => + { + try + { + var directory = new DirectoryInfo(_cacheBaseDirectory); + if (directory.Exists) + { + directory.Delete(true); + Directory.CreateDirectory(_cacheBaseDirectory); + _logger.LogInformation("清空所有缓存完成"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "清空所有缓存失败"); + } + }); + } + + public async Task ClearExpiredCacheAsync() + { + await Task.Run(() => + { + try + { + var directory = new DirectoryInfo(_cacheBaseDirectory); + if (!directory.Exists) return; + + int deletedCount = 0; + foreach (var file in directory.GetFiles("*.bin")) + { + try + { + file.Delete(); + deletedCount++; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "删除缓存文件失败: {FilePath}", file.FullName); + } + } + + _logger.LogInformation("清理缓存完成,删除文件数: {DeletedCount}", deletedCount); + } + catch (Exception ex) + { + _logger.LogError(ex, "清理缓存失败"); + } + }); + } + public async Task GetFileByteAsync(string fileUrl, string cacheKey = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(fileUrl)) + return GetEmptyByte(); + + + // 生成缓存键,如果未提供则使用URL的MD5值 + var key = cacheKey ?? GenerateCacheKey(fileUrl); + if (fileUrl.Contains("http")) + { + var urifileName = UrlHelper.GetEncodedFileNameFromUrl(fileUrl); + var realfileName = Uri.UnescapeDataString(urifileName); + key = GenerateCacheKey(realfileName); + } + + // 2. 检查磁盘缓存 + string filePath = GetCacheFilePath(key); + if (File.Exists(filePath)) + { + //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); + // 从磁盘加载并放入内存 + var filebyte = await LoadFileFromDiskAsync(filePath); + return filebyte; + } + + // 3. 从网络下载 + try + { + var fullImgUrl = _configurationService.ApiSettings.FileUrl + fileUrl; + if (fileUrl.Contains("http")) + { + fullImgUrl = fileUrl; + } + _logger.LogInformation("开始下载bin文件: {Url}", fullImgUrl); + + // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) + byte[] fileBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); + + if (fileBytes == null || fileBytes.Length == 0) + { + _logger.LogWarning("文件数据为空: {Url}", fullImgUrl); + return GetEmptyByte(); + } + + // 保存到磁盘缓存 + await SavefileToDiskAsync(fileBytes, filePath); + return fileBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "下载文件失败: {fileUrl}", filePath); + } + + return GetEmptyByte(); + } + + #region 辅助方法 + private byte[] GetEmptyByte() + { + return Array.Empty(); + } + /// + /// 保存字节数组到磁盘 + /// + private async Task SavefileToDiskAsync(byte[] fileBytes, string filePath) + { + try + { + // 使用临时文件避免写入过程中出错 + var tempFilePath = filePath + ".tmp"; + + File.WriteAllBytes(tempFilePath, fileBytes); + + // 原子性操作:重命名临时文件 + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.Move(tempFilePath, filePath); + + _logger.LogDebug("文件保存成功: {FilePath}, 大小: {Size}字节", filePath, fileBytes.Length); + } + catch (Exception ex) + { + _logger.LogError(ex, "保存文件到磁盘失败: {FilePath}", filePath); + + // 清理临时文件 + var tempFilePath = filePath + ".tmp"; + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + + /// + /// 优化的磁盘加载方法 + /// + private async Task LoadFileFromDiskAsync(string filePath) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + { + return GetEmptyByte(); + } + + try + { + // 直接读取文件字节数组 + byte[] fileBytes = File.ReadAllBytes(filePath); + + if (fileBytes == null || fileBytes.Length == 0) + { + _logger.LogWarning("磁盘缓存文件为空: {FilePath}", filePath); + // 删除损坏的文件 + File.Delete(filePath); + return GetEmptyByte(); + } + + return fileBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "从磁盘加载文件失败: {FilePath}", filePath); + return GetEmptyByte(); + } + } + private string GenerateCacheKey(string url) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(url); + byte[] hashBytes = md5.ComputeHash(inputBytes); + return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").Substring(0, 10); + } + } + #endregion + + #region + private async Task UpdateOrAddBinFile(List vehKeys) + { + var binFilePathList = new ConcurrentDictionary(); + foreach (var item in vehKeys) + { + var tempArray = item.sourceUrl.Split(','); + foreach (var tempitem in tempArray) + { + var filename = UrlHelper.GetEncodedFileNameFromUrl(tempitem); + binFilePathList.GetOrAdd(filename, tempitem); + } + } + } + + public Task<(bool success, string localFilePath)> DownloadFileWithProgressAsync(string fileUrl, string cacheKey, IProgress progress = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + #endregion + } +} diff --git a/AIK.Service/Service/FileCloudDataService.cs b/AIK.Service/Service/FileCloudDataService.cs new file mode 100644 index 0000000..985828f --- /dev/null +++ b/AIK.Service/Service/FileCloudDataService.cs @@ -0,0 +1,353 @@ +using AIK.Common; +using AIK.Common.WebHelper; +using AIK.Service.Extensions; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using SharpCompress.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class FileCloudDataService : IFileCacheService + { + private readonly string _cacheBaseDirectory; + private readonly IHttpClientService _httpClient; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private string GetCacheFilePath(string cacheKey) => Path.Combine(_cacheBaseDirectory, $"{cacheKey}"); + public FileCloudDataService(IConfigurationService configurationService, IHttpClientService httpClient, ILogger logger) + { + _configurationService = configurationService; + _httpClient = httpClient; + _logger = logger; + // 设置缓存目录,例如在用户本地应用数据文件夹下 + _cacheBaseDirectory = CommonString.CloudCacheDirectory; + Directory.CreateDirectory(_cacheBaseDirectory); // 确保目录存在 + } + public async Task ClearAllCacheAsync() + { + await Task.Run(() => + { + try + { + var directory = new DirectoryInfo(_cacheBaseDirectory); + if (directory.Exists) + { + directory.Delete(true); + Directory.CreateDirectory(_cacheBaseDirectory); + _logger.LogInformation("清空所有缓存完成"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "清空所有缓存失败"); + } + }); + } + + public async Task ClearExpiredCacheAsync() + { + await Task.Run(() => + { + try + { + var directory = new DirectoryInfo(_cacheBaseDirectory); + if (!directory.Exists) return; + + int deletedCount = 0; + foreach (var file in directory.GetFiles("*.bin")) + { + try + { + file.Delete(); + deletedCount++; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "删除缓存文件失败: {FilePath}", file.FullName); + } + } + + _logger.LogInformation("清理缓存完成,删除文件数: {DeletedCount}", deletedCount); + } + catch (Exception ex) + { + _logger.LogError(ex, "清理缓存失败"); + } + }); + } + public async Task GetFileByteAsync(string fileUrl, string cacheKey = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(fileUrl)) + return GetEmptyByte(); + // 生成缓存键,如果未提供则使用URL的MD5值 + var key = cacheKey ?? GenerateCacheKey(fileUrl); + if (fileUrl.Contains("http")) + { + var urifileName = UrlHelper.GetEncodedFileNameFromUrl(fileUrl); + var realfileName = Uri.UnescapeDataString(urifileName); + key = realfileName; + } + + // 2. 检查磁盘缓存 + string filePath = GetCacheFilePath(key); + if (File.Exists(filePath)) + { + //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); + // 从磁盘加载并放入内存 + var filebyte = await LoadFileFromDiskAsync(filePath); + return filebyte; + } + + // 3. 从网络下载 + try + { + var fullImgUrl = fileUrl; + if (fileUrl.Contains("http")) + { + fullImgUrl = fileUrl; + } + _logger.LogInformation("开始下载bin文件: {Url}", fullImgUrl); + + // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) + byte[] fileBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); + + if (fileBytes == null || fileBytes.Length == 0) + { + _logger.LogWarning("文件数据为空: {Url}", fullImgUrl); + return GetEmptyByte(); + } + + // 保存到磁盘缓存 + await SavefileToDiskAsync(fileBytes, filePath); + return fileBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "下载文件失败: {Url}", fileUrl); + } + + return GetEmptyByte(); + } + + public async Task<(bool success, string localFilePath)> DownloadFileWithProgressAsync(string fileUrl, string cacheKey, IProgress progress = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(fileUrl)) + return (false, string.Empty); + + // 生成缓存键,如果未提供则使用URL的MD5值 + var key = cacheKey ?? GenerateCacheKey(fileUrl); + if (fileUrl.Contains("http")) + { + var urifileName = UrlHelper.GetEncodedFileNameFromUrl(fileUrl); + var realfileName = Uri.UnescapeDataString(urifileName); + key = realfileName; + } + // 2. 检查磁盘缓存 + string filePath = GetCacheFilePath(key); + if (File.Exists(filePath)) + { + // 校验缓存 zip 有效性:历史版本截断下载可能留下损坏文件,命中坏缓存会导致解压永远失败 + if (IsValidZipFile(filePath)) + { + progress?.Report(100.0); + return (true, filePath); + } + + _logger.LogWarning("缓存 zip 文件损坏,删除并重新下载: {FilePath}", filePath); + try + { + File.Delete(filePath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "删除损坏缓存失败: {FilePath}", filePath); + } + } + // 3. 从网络流式下载到临时文件(每段读取空闲超时、无总时长限制;支持断点续传),成功后原子替换 + var fullImgUrl = fileUrl; + var tempFilePath = filePath + ".tmp"; + const int maxAttempts = 5; + + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + // 断点续传:检查临时文件已下载的字节数,作为 Range 偏移从断点继续 + long resumeOffset = 0; + if (File.Exists(tempFilePath)) + { + resumeOffset = new FileInfo(tempFilePath).Length; + } + + if (resumeOffset > 0) + { + _logger.LogInformation("断点续传下载: {Url},已有 {Offset} 字节(第 {Attempt}/{Max} 次)", + fullImgUrl, resumeOffset, attempt, maxAttempts); + } + else + { + _logger.LogInformation("开始下载bin文件: {Url}", fullImgUrl); + } + + // 流式写盘:BouncyCastle TLS + 逐段读取(空闲 60s 超时)+ Range 断点续传 + await _httpClient.DownloadToFileWithProgressAsync(fullImgUrl, tempFilePath, progress, cancellationToken, resumeOffset); + + // 下载产物完整性校验(拦截截断/错误页),不通过则抛异常走重试 + if (!IsValidZipFile(tempFilePath)) + { + throw new InvalidDataException($"下载的 zip 文件无效(可能被截断): {fullImgUrl}"); + } + + // 原子性操作:临时文件替换正式文件 + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.Move(tempFilePath, filePath); + + progress?.Report(100.0); + return (true, filePath); + } + catch (Exception ex) + { + // 保留本次已下载的临时文件(供下次断点续传),仅在最终失败时清理 + if (attempt < maxAttempts && !cancellationToken.IsCancellationRequested) + { + _logger.LogWarning(ex, "下载文件失败(第 {Attempt}/{Max} 次,将断点续传): {Url}", + attempt, maxAttempts, fullImgUrl); + await Task.Delay(TimeSpan.FromSeconds(attempt), cancellationToken).ConfigureAwait(false); + } + else + { + _logger.LogError(ex, "下载文件失败(最终): {Url}", fullImgUrl); + try + { + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + catch { } + return (false, string.Empty); + } + } + } + + return (false, string.Empty); + } + + #region 辅助方法 + private byte[] GetEmptyByte() + { + return Array.Empty(); + } + /// + /// 保存字节数组到磁盘 + /// + private async Task SavefileToDiskAsync(byte[] fileBytes, string filePath) + { + try + { + // 使用临时文件避免写入过程中出错 + var tempFilePath = filePath + ".tmp"; + + File.WriteAllBytes(tempFilePath, fileBytes); + + // 原子性操作:重命名临时文件 + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.Move(tempFilePath, filePath); + + _logger.LogDebug("文件保存成功: {FilePath}, 大小: {Size}字节", filePath, fileBytes.Length); + } + catch (Exception ex) + { + _logger.LogError(ex, "保存文件到磁盘失败: {FilePath}", filePath); + + // 清理临时文件 + var tempFilePath = filePath + ".tmp"; + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + + /// + /// 优化的磁盘加载方法 + /// + private async Task LoadFileFromDiskAsync(string filePath) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + { + return GetEmptyByte(); + } + + try + { + // 直接读取文件字节数组 + byte[] fileBytes = File.ReadAllBytes(filePath); + + if (fileBytes == null || fileBytes.Length == 0) + { + _logger.LogWarning("磁盘缓存文件为空: {FilePath}", filePath); + // 删除损坏的文件 + File.Delete(filePath); + return GetEmptyByte(); + } + + return fileBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "从磁盘加载文件失败: {FilePath}", filePath); + return GetEmptyByte(); + } + } + private string GenerateCacheKey(string url) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(url); + byte[] hashBytes = md5.ComputeHash(inputBytes); + return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").Substring(0, 10); + } + } + + /// + /// 校验 zip 文件是否有效(能否打开中央目录记录)。 + /// 用于拦截:历史版本截断下载留下的损坏缓存、本次下载被中途掐断或返回错误页的产物。 + /// + private static bool IsValidZipFile(string filePath) + { + try + { + using (var archive = ZipFile.OpenRead(filePath)) + { + return archive.Entries.Count > 0 || new FileInfo(filePath).Length > 0; + } + } + catch (Exception ex) when (ex is InvalidDataException || ex is IOException || ex is UnauthorizedAccessException) + { + return false; + } + catch + { + return false; + } + } + #endregion + } +} diff --git a/AIK.Service/Service/FileOBDService.cs b/AIK.Service/Service/FileOBDService.cs new file mode 100644 index 0000000..37f24af --- /dev/null +++ b/AIK.Service/Service/FileOBDService.cs @@ -0,0 +1,33 @@ +using AIK.Service.IService; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class FileOBDService : IFileCacheService + { + public Task ClearAllCacheAsync() + { + throw new NotImplementedException(); + } + + public Task ClearExpiredCacheAsync() + { + throw new NotImplementedException(); + } + + public Task<(bool success, string localFilePath)> DownloadFileWithProgressAsync(string fileUrl, string cacheKey, IProgress progress = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task GetFileByteAsync(string fileUrl, string cacheKey = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Service/Service/FileOperationService.cs b/AIK.Service/Service/FileOperationService.cs new file mode 100644 index 0000000..9995d8f --- /dev/null +++ b/AIK.Service/Service/FileOperationService.cs @@ -0,0 +1,176 @@ +using AIK.Common.SysCommon; +using AIK.Service.IService; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class FileOperationService : IFileOperationService + { + public async Task ExtractSingleArchiveAsync( + string archivePath, + string destinationDir, + bool overwrite = true, + CancellationToken cancellationToken = default) + { + if (!Directory.Exists(destinationDir)) + Directory.CreateDirectory(destinationDir); + + try + { + using var archive = ArchiveFactory.Open(archivePath); + var allEntries = archive.Entries + .Where(e => !e.IsDirectory) + .ToList(); + + bool shouldStripReceivedData = HasRootReceivedDataFolder(allEntries); + + foreach (var entry in allEntries) + { + cancellationToken.ThrowIfCancellationRequested(); + + string normalizedEntryPath = NormalizeArchivePath(entry.Key); + + string targetRelativePath; + if (shouldStripReceivedData) + { + if (normalizedEntryPath.StartsWith($"{ComStringHelper.K3ToolDirectory}/", StringComparison.OrdinalIgnoreCase)) + { + targetRelativePath = normalizedEntryPath.Substring($"{ComStringHelper.K3ToolDirectory}/".Length); + } + else + { + continue; // 跳过非 received_data 内容 + } + } + else + { + targetRelativePath = normalizedEntryPath; + } + + // 安全校验(GetSafeExtractionPath 内部也应基于规范化路径) + string destPath = GetSafeExtractionPath(destinationDir, targetRelativePath); + Directory.CreateDirectory(Path.GetDirectoryName(destPath)!); + + await Task.Run(() => + { + entry.WriteToFile(destPath, new ExtractionOptions + { + Overwrite = overwrite, + PreserveFileTime = false + }); + }, cancellationToken); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"解压失败: {archivePath}", ex); + } + } + + public async Task ExtractMultipleArchivesAsyncNotPoress( + IEnumerable archivePaths, + string destinationDir, + bool overwrite = true, + CancellationToken cancellationToken = default) + { + if (!Directory.Exists(destinationDir)) + Directory.CreateDirectory(destinationDir); + + foreach (string path in archivePaths) + { + await ExtractSingleArchiveAsync(path, destinationDir, overwrite, cancellationToken); + } + } + + public async Task ExtractMultipleArchivesAsync( + IReadOnlyList archivePaths, + string destDir, + IProgress progress, + bool overwrite, + CancellationToken cancellationToken) + { + int total = archivePaths.Count; + if (total == 0) + { + progress.Report(100); + return; + } + + for (int i = 0; i < total; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + string path = archivePaths[i]; + + if (File.Exists(path)) + { + await Task.Run(() => + { + System.IO.Compression.ZipFile.ExtractToDirectory(path, destDir); + }, cancellationToken); + } + + double percent = (i + 1.0) / total * 100.0; + progress.Report(percent); + } + + progress.Report(100); + } + // 私有工具方法 + private static string GetSafeExtractionPath(string baseDir, string normalizedRelativePath) + { + // 此时 normalizedRelativePath 已是 clean 格式,如 "bin/app.exe" + if (string.IsNullOrEmpty(normalizedRelativePath)) + throw new ArgumentException("路径为空"); + + // 拼接并获取完整路径 + string fullPath = Path.GetFullPath(Path.Combine(baseDir, normalizedRelativePath)); + + // 安全校验:必须在 baseDir 内 + string baseDirNormalized = Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar; + if (!fullPath.StartsWith(baseDirNormalized, StringComparison.OrdinalIgnoreCase)) + { + throw new SecurityException($"非法路径遍历: {normalizedRelativePath}"); + } + + // 仅对文件名部分处理非法字符 + string dirPart = Path.GetDirectoryName(fullPath)!; + string filePart = Path.GetFileName(fullPath); + string safeFilePart = Path.GetInvalidFileNameChars() + .Aggregate(filePart, (current, c) => current.Replace(c.ToString(), "_")); + + return Path.Combine(dirPart, safeFilePart); + } + + private static bool HasRootReceivedDataFolder(List entries) + { + return entries.Any(e => + e.Key.Replace('\\', '/').StartsWith($"{ComStringHelper.K3ToolDirectory}/", StringComparison.OrdinalIgnoreCase)); + } + private static string NormalizeArchivePath(string archivePath) + { + if (string.IsNullOrEmpty(archivePath)) + return string.Empty; + + // 1. 统一斜杠 + string path = archivePath.Replace('\\', '/'); + + // 2. 分割并过滤危险/冗余段 + var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + .Where(part => part != "." && !part.Equals("..", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + // 3. 重建路径(无开头/结尾斜杠) + return parts.Count == 0 ? string.Empty : string.Join("/", parts); + } + } +} diff --git a/AIK.Service/Service/HidCommunicationService.cs b/AIK.Service/Service/HidCommunicationService.cs new file mode 100644 index 0000000..3c727a4 --- /dev/null +++ b/AIK.Service/Service/HidCommunicationService.cs @@ -0,0 +1,2984 @@ +using AIK.Common.AIKProtocol; +using AIK.Common.Enum; +using AIK.Common.Event; +using AIK.Common.Language; +using AIK.Common.SysCommon; +using AIK.Models.HidModels; +using AIK.Service.Interop; +using AIK.Service.IService; +using HidSharp; +using HidSharp.Reports; +using HidSharp.Reports.Input; +using Microsoft.Extensions.Logging; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Management; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Service.Service +{ + public class HidCommunicationService : IHidCommunicationService, IDisposable + { + + #region 常量定义 + private const int REPORT_LENGTH = 65; + private int _reconnectAttempts = 0; + private const int MAX_RECONNECT_ATTEMPTS = 5; + private const int RECONNECT_INTERVAL_MS = 100; + private const int DEVICE_MONITOR_INTERVAL_MS = 300; + private const int DEVICE_MONITOR_ERROR_DELAY_MS = 500; + private const int KEEP_ALIVE_INTERVAL_MS = 50000; + private const int HID_RESPONSE_WARNING_MS = 90000; + private const int COPY_BUFFER_SIZE = 32 * 1024; + private const int COPY_CHUNK_DELAY_MS = 5; + private const int COPY_FILE_DELAY_MS = 50; + private const int FILE_FLUSH_INTERVAL_BYTES = 4 * 1024 * 1024; + #endregion + + #region 公共属性 + public BigTransferPhase CurrentBigPhase => _currentBigPhase; + public int CompletedBigPhases => _bigPhaseTasks.Count(t => t.IsCompleted); + public int TotalBigPhases => _bigPhaseTasks.Count; + public bool IsConnected => _stream != null && _isRunning; + public TransferState State { get; } = new TransferState(); + #endregion + + #region 数据传输字段 + //写入文件(本地文件) + private byte[]? _filelocalData; + private string? _fileName; + //key钥匙bin文件 + private byte[]? _fileBinData; + private string? _fileBinName; + #endregion + + #region 设备配置 + //private readonly int _vendorId, _productId; //设备的硬件Id + private readonly int _vendorId = 0x28E9; + private readonly int _productId = 0xFFF0; + private readonly string usbdiskDir = ComStringHelper.K3ToolDirectory; + #endregion + + #region 核心状态字段 + private HidDevice? _currentDevice; + private HidStream? _stream; + + private volatile bool _isRunning; + private IProgress? _currentProgress; + private double _currentBigPhaseProgress; + private int _currentDataPacketIndex = 0; + private List resNotity = new List(); + // 自动重连相关字段 + private bool _isMonitoring = false; + private bool _autoReconnectEnabled = true; + private volatile bool _isProcessingResponse = false; + private volatile bool _isReconnecting = false; + private bool _hasSentConnectRequest = false; // 👈 关键标志 + + //单开线程接收设备响应(CPU占用高/备用方法适应老版本库) + //private Thread? _receiveThread; + + private HidDeviceInputReceiver? _inputReceiver; + private byte[] _inputBuffer = Array.Empty(); + private DateTime _lastHidResponseUtc = DateTime.MinValue; + private int _consecutiveKeepAliveFailures = 0; + + private HidOperationTypeEnum _hidOperationType; + #endregion + + #region 异步控制 + private CancellationTokenSource? _cts; + private Thread? _monitorThread; + private CancellationToken _currentCancellationToken; + //用于针对大阶段任务进度 + private readonly Dictionary> _phaseTcsDict = new Dictionary>(); + private BigTransferPhase _currentBigPhase = BigTransferPhase.None; // 当前大阶段 + private List _bigPhaseTasks = new(); // 大阶段任务队列 + + //第一次连接 + private TaskCompletionSource? _connectTcs; + // (用于传输进度) + private TaskCompletionSource? _transferTcs; + // 全局大阶段任务完成源 + private TaskCompletionSource? _allBigPhasesTcs; + // 标记是否所有大阶段已完成 + private volatile bool _allBigPhasesCompleted = false; + + #region 锁 + private readonly object _streamLock = new(); + private readonly object _responseLock = new object(); + private readonly object _reconnectLock = new object(); + // 加锁保护TCS字典 + private readonly object _phaseTcsLock = new object(); + private readonly object _bigPhaseLock = new(); // 大阶段锁 + #endregion + + #endregion + + #region 事件定义 + // 新增:大阶段变更事件 + public event EventHandler? BigPhaseChanged; + // 新增:所有大阶段完成事件 + public event EventHandler? AllBigPhasesCompleted; + public event EventHandler? ResponseReceived; + public event EventHandler? LogMessage; + // 自动重连相关事件 + public event EventHandler? DeviceConnected; + public event EventHandler? DeviceDisconnected; + public event EventHandler? ReconnectAttempt; + + public event EventHandler? ConnectionStateChanged; + #endregion + + private readonly ILogger _logger; + public HidCommunicationService(ILogger logger) + { + _logger = logger; + HidStart(); + } + + private void HidStart() + { + try + { + if (_isMonitoring || _isReconnecting) + { + _logger.LogInformation("设备监视器已在运行或正在重连中,跳过启动。"); + } + else + { + _logger.LogInformation("启动设备监视器..."); + StartDeviceMonitoring(); + // 启动时立即尝试连接,不等轮询 + _ = TryConnectIfDevicePresentAsync(); + // 启用事件驱动监听(设备插入/拔出即时响应) + StartHidDeviceMonitoring(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "启动 HID 设备监控失败"); + } + + } + #region 连接状态事件 + private void OnConnectionStateChanged(bool isConnected, string message = "") + { + ConnectionStateChanged?.Invoke(this, + new ConnectionStateChangedEventArgs(isConnected, message)); + } + #endregion + #region 自动重连实现 + + #region 新单开线程监听设备状态 (轮询方式CPU占用稍高) + /// + /// 轮询模式 + /// + private void StartDeviceMonitoring() + { + if (_isMonitoring) return; + + _isMonitoring = true; + _monitorThread = new Thread(DeviceMonitorLoop) + { + IsBackground = true, + Name = "HID-DeviceMonitor" + }; + _monitorThread.Start(); + + OnLog("🔍 开始设备监视..."); + } + + private void StopDeviceMonitoring() + { + _isMonitoring = false; + if (_monitorThread != null && _monitorThread.IsAlive) + { + _monitorThread.Join(DEVICE_MONITOR_INTERVAL_MS); + } + } + + private void DeviceMonitorLoop() + { + while (_isMonitoring) + { + try + { + // 检查设备连接状态 + CheckDeviceConnection(); + + // 报告实际连接状态(IsConnected = 物理连接,IsDataConnected = 数据交互已建立) + OnConnectionStateChanged(State.IsDataConnected, + State.IsDataConnected ? "设备已就绪" : + State.IsConnected ? "设备已连接,等待应答..." : + "设备未连接"); + + // 每秒检查一次 + Thread.Sleep(DEVICE_MONITOR_INTERVAL_MS); + } + catch (Exception ex) + { + _logger.LogWarning($"设备监视异常: {ex.Message}"); + + Thread.Sleep(DEVICE_MONITOR_ERROR_DELAY_MS); // 异常时等待更久 + } + } + } + private void CheckDeviceConnection() + { + lock (_reconnectLock) + { + var isDevicePresent = CheckIfDevicePresent(); + // 去掉慢速WMI查询,HidSharp枚举已足够可靠 + + if (isDevicePresent) + { + _autoReconnectEnabled = true; + } + if ((State.IsConnected || State.IsDataConnected) && !isDevicePresent) + { + // 设备意外断开(IsConnected 物理连接 或 IsDataConnected 数据连接,任一成立都应触发) + OnLog("⚠️ 检测到设备断开", true); + HandleUnexpectedDisconnection(); + } + else if (!State.IsConnected && !State.IsDataConnected && isDevicePresent && _autoReconnectEnabled) + { + // 设备重新插入,尝试重连 + _ = AttemptReconnect(isDevicePresent); + } + else if (!State.IsConnected && !State.IsDataConnected && !isDevicePresent && _autoReconnectEnabled) + { + // 设备不存在,但允许重连尝试 + if (_reconnectAttempts < MAX_RECONNECT_ATTEMPTS) + { + _ = AttemptReconnect(isDevicePresent); + } + } + } + } + + // 新增:通过WMI检测USB设备(绕过HidSharp缓存,更精准) + private bool CheckDeviceByManagementScope() + { + try + { + using var searcher = new ManagementObjectSearcher( + "root\\CIMV2", + $"SELECT * FROM Win32_USBHub WHERE PNPDeviceID LIKE '%VID_{_vendorId:X4}&PID_{_productId:X4}%'"); + + foreach (var queryObj in searcher.Get()) + { + return true; + } + return false; + } + catch (Exception ex) + { + _logger.LogWarning($"WMI设备检测失败: {ex.Message}"); + return CheckIfDevicePresent(); // 降级使用HidSharp检测 + } + } + private bool CheckIfDevicePresent() + { + try + { + var devices = DeviceList.Local.GetHidDevices(_vendorId, _productId); + return devices.Any(); + } + catch (Exception ex) + { + OnLog($"检查设备存在性失败: {ex.Message}", true); + return false; + } + } + + private void HandleUnexpectedDisconnection() + { + _isRunning = false; + State.IsDataConnected = false; + State.IsConnected = false; + State.IsTransferring = false; + _hasSentConnectRequest = false; + _lastHidResponseUtc = DateTime.MinValue; + _consecutiveKeepAliveFailures = 0; + lock (_streamLock) + { + _stream?.Dispose(); + _stream = null; + } + + DeviceDisconnected?.Invoke(this, EventArgs.Empty); + _reconnectAttempts = 0; // 重置重连计数 + _inputReceiver = null; + OnLog("设备连接已断开", true); + CleanupTransfer(_currentBigPhase); + } + + private async Task AttemptReconnect(bool isDevicePresent) + { + if (_reconnectAttempts >= MAX_RECONNECT_ATTEMPTS && !isDevicePresent) + { + OnLog($"❌ 已达到最大重连次数({MAX_RECONNECT_ATTEMPTS}),停止重连", true); + _autoReconnectEnabled = false; // 禁用自动重连 + return; + } + if (State.IsDataConnected && State.IsConnected) return; + _reconnectAttempts++; + var currentAttempt = _reconnectAttempts; + + ReconnectAttempt?.Invoke(this, new HidReconnectEventArgs(currentAttempt, MAX_RECONNECT_ATTEMPTS)); + + //OnLog($"🔄 尝试重连... ({currentAttempt}/{MAX_RECONNECT_ATTEMPTS})"); + + try + { + // 等待一段时间再重连 + await Task.Delay(RECONNECT_INTERVAL_MS); + // 指数退避策略 + //int delayMs = Math.Min(500 * _reconnectAttempts, RECONNECT_INTERVAL_MS); + //await Task.Delay(delayMs); + + var success = await ConnectAsync(_vendorId, _productId); + if (success) + { + OnLog("✅ 重连成功"); + _reconnectAttempts = 0; // 重置计数 + DeviceConnected?.Invoke(this, EventArgs.Empty); + } + else + { + OnLog($"❌ 重连失败 ({currentAttempt}/{MAX_RECONNECT_ATTEMPTS})", true); + if (State.IsDataConnected == false) + { + _ = DisconnectAsync(); + } + } + } + catch (Exception ex) + { + OnLog($"重连异常: {ex.Message}", true); + _ = DisconnectAsync(); + } + } + #endregion + + #region HidSharp 热拔插监听机制(设备只响应变更,每次打开页面需要重新执行连接方法,单例模式不适合) + /// + /// 事件驱动模式 + /// + private void StartHidDeviceMonitoring() + { + DeviceList.Local.Changed += OnDeviceListChanged; + } + public void StopHidDeviceMonitoring() + { + DeviceList.Local.Changed -= OnDeviceListChanged; + } + + private async void OnDeviceListChanged(object? sender, EventArgs e) + { + try + { + // 检查目标设备是否存在 + var deviceExists = DeviceList.Local.GetHidDevices(_vendorId, _productId).Any(); + bool currentlyConnected = IsConnected; + // 如果当前未连接,且设备出现了 → 尝试重连 + if (!currentlyConnected && deviceExists) + { + OnLog("🔌 检测到设备插入,尝试自动重连..."); + await TryReconnectAsync(); + } + // 如果已连接,但设备消失了 → 触发断开 + else if (currentlyConnected && !deviceExists) + { + OnLog("⚠️ 检测到设备拔出", true); + // 立即断开并清理状态 + HandleUnexpectedDisconnection(); + } + + OnConnectionStateChanged(IsConnected, $"设备连接状态:{IsConnected}"); + } + catch (Exception ex) + { + OnLog($"设备监听回调异常: {ex.Message}", true); + } + } + private async Task TryReconnectAsync() + { + if (_isReconnecting) return; // 防止并发重连 + _isReconnecting = true; + + try + { + await ConnectAsync(_vendorId, _productId); + } + finally + { + _isReconnecting = false; + } + } + private async Task TryConnectIfDevicePresentAsync() + { + // 防止重复连接 + if (IsConnected) return; + + var devices = DeviceList.Local.GetHidDevices(_vendorId, _productId); + if (devices.Any()) + { + OnLog("🔌 检测到启动时设备已插入,尝试连接..."); + await ConnectAsync(_vendorId, _productId); + } + else + { + OnLog("📭 启动时未检测到设备"); + } + } + #endregion + + + #endregion + #region 新增:大阶段状态通知 + private void OnBigPhaseChanged(BigTransferPhase newPhase) + { + var previous = _currentBigPhase; + _currentBigPhase = newPhase; + BigPhaseChanged?.Invoke(this, new BigPhaseChangedEventArgs( + newPhase, previous, CompletedBigPhases, TotalBigPhases)); + OnLog($"📌 大阶段切换:{previous} → {newPhase}"); + } + + private void OnAllBigPhasesCompleted() + { + OnLog($"🎉 所有大阶段执行完成(共{TotalBigPhases}个)"); + AllBigPhasesCompleted?.Invoke(this, EventArgs.Empty); + // 标记全局完成 + _allBigPhasesCompleted = true; + // 双重保障:触发全局TCS + _allBigPhasesTcs?.TrySetResult(true); + // 重置大阶段状态 + lock (_bigPhaseLock) + { + _bigPhaseTasks.Clear(); + _currentBigPhase = BigTransferPhase.None; + } + } + #endregion + #region 新增的自动重连控制方法 + + /// + /// 启用自动重连 + /// + public void EnableAutoReconnect() + { + _autoReconnectEnabled = true; + _reconnectAttempts = 0; + OnLog("✅ 自动重连已启用"); + } + + /// + /// 禁用自动重连 + /// + public void DisableAutoReconnect() + { + _autoReconnectEnabled = false; + OnLog("❌ 自动重连已禁用"); + } + + /// + /// 立即尝试重连 + /// + public async Task ReconnectNowAsync() + { + _reconnectAttempts = 0; // 重置计数 + _hasSentConnectRequest = false; + return await ConnectAsync(_vendorId, _productId); + } + + /// + /// 获取重连状态 + /// + public ReconnectStatus GetReconnectStatus() + { + return new ReconnectStatus + { + IsAutoReconnectEnabled = _autoReconnectEnabled, + ReconnectAttempts = _reconnectAttempts, + MaxReconnectAttempts = MAX_RECONNECT_ATTEMPTS, + IsMonitoring = _isMonitoring + }; + } + + #endregion + #region 基础发送及响应 + public async Task ConnectAsync(int vendorId = 0x28E9, int productId = 0xFFF0) + { + var devices = DeviceList.Local.GetHidDevices(vendorId, productId); + if (!devices.Any()) + { + OnLog("未找到设备", true); + return false; + } + + try + { + var device = devices.First(); + if (!device.TryOpen(out _stream)) + { + OnLog("无法打开设备流", true); + return false; + } + Debug.Write($"设备已打开,输出报告长度: {device.GetMaxOutputReportLength()},输入报告长度: {device.GetMaxInputReportLength()}"); + _isRunning = true; + _cts = new CancellationTokenSource(); + _lastHidResponseUtc = DateTime.UtcNow; + _consecutiveKeepAliveFailures = 0; + + #region 事件异步接收 + // 初始化接收器 + var descriptor = device.GetReportDescriptor(); + _inputReceiver = descriptor.CreateHidDeviceInputReceiver(); + _inputBuffer = new byte[device.GetMaxInputReportLength()]; + + // 订阅异步接收事件(自动在线程池触发) + _inputReceiver.Received += OnInputReceived; + + // 启动接收(非阻塞) + _inputReceiver.Start(_stream); + #endregion + + State.IsConnected = true; + _reconnectAttempts = 0; + + if (_hasSentConnectRequest == false) + { + _hasSentConnectRequest = true; + await SendAsync(K3ToolUSBProtocol.ConnectData); + } + + // ✅ 适配 .NET Framework 4.6.1 的超时等待机制 + var localConnectTcs = new TaskCompletionSource(); + using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token)) + { + timeoutCts.CancelAfter(2500); // 2.5秒超时 + + // 临时保存到字段,供 ProcessIncomingResponse 使用 + _connectTcs = localConnectTcs; + + try + { + // 经典超时等待模式:将 TCS 任务与超时取消令牌结合 + var completedTask = await Task.WhenAny(localConnectTcs.Task, Task.Delay(Timeout.Infinite, timeoutCts.Token)); + await completedTask; // 如果超时,此行会抛出 OperationCanceledException + return true; + } + catch (OperationCanceledException) + { + if (timeoutCts.IsCancellationRequested) + OnLog("连接超时:未收到设备应答(设备可能未就绪)", false); + return false; + } + //finally + //{ + // _connectTcs = null; // 清理 + //} + } // using 块结束,自动释放 timeoutCts + } + catch (Exception ex) + { + OnLog($"连接失败: {ex.Message}", true); + await DisconnectAsync(); + Dispose(); + return false; + } + } + + /// + /// 轮询等待复合设备(U盘+HID)完全加载完成 + /// + /// 厂商ID + /// 产品ID + /// 最大等待时间(毫秒) + /// 轮询间隔(毫秒) + /// true 表示设备已就绪;false 表示超时 + public async Task WaitForDeviceReadyAsync( + int vendorId = 0x28E9, + int productId = 0xFFF0, + int maxWaitMs = 6000, + int pollIntervalMs = 300) + { + var startTime = DateTime.UtcNow; + var timeout = TimeSpan.FromMilliseconds(maxWaitMs); + + while ((DateTime.UtcNow - startTime) < timeout) + { + try + { + // Step 1: 枚举设备 + var devices = DeviceList.Local.GetHidDevices(vendorId, productId); + if (!devices.Any()) + { + await Task.Delay(pollIntervalMs); + continue; + } + + var device = devices.First(); + + // Step 2: 尝试打开设备(可选,但建议) + if (!device.TryOpen(out var stream)) + { + await Task.Delay(pollIntervalMs); + continue; + } + + // Step 3: 【核心】读取报告描述符 + var descriptor = null as ReportDescriptor; + try + { + descriptor = device.GetReportDescriptor(); // HidSharp API + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"GetReportDescriptor failed: {ex.Message}"); + } + finally + { + stream?.Dispose(); // 立即释放流 + } + + // ✅ 描述符有效 = 设备 ready! + if (descriptor != null && descriptor.DeviceItems.Count > 0) + { + OnLog("✅ HID 报告描述符读取成功,设备已就绪", false); + return true; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"轮询异常: {ex.Message}"); + } + + await Task.Delay(pollIntervalMs); + } + + OnLog($"⚠️ 设备未在 {maxWaitMs}ms 内就绪", true); + return false; + } + #region 接收线程(兼容 .NET Framework 4.6.2,同步 Read 方式) + private void ReceiveLoop() + { + var buffer = new byte[REPORT_LENGTH]; + while (_isRunning && _stream != null) + { + try + { + _stream.ReadTimeout = 0; + int n = _stream.Read(buffer, 0, REPORT_LENGTH); + + if (n > 0) + { + var copy = new byte[n]; + Array.Copy(buffer, copy, n); + + // 触发响应事件 + ResponseReceived?.Invoke(this, new HidResponseReceivedEventArgs(copy)); + + // 处理协议响应 + ProcessIncomingResponse(copy); + } + } + catch (TimeoutException) + { + // 正常:无数据,继续循环 + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + OnLog($"接收异常: {ex.Message}", true); + break; + } + } + + // 自动断开 + if (_isRunning) + { + OnLog("设备断开", true); + _isRunning = false; + State.IsDataConnected = false; + State.IsConnected = false; + _transferTcs?.TrySetResult(false); + } + } + #endregion + + #region HidSharp 接收设备响应事件驱动 + private void OnInputReceived(object? sender, EventArgs e) + { + if (_inputReceiver == null || _inputBuffer.Length == 0) return; + + Report report; + while (_inputReceiver.TryRead(_inputBuffer, 0, out report)) + { + if (report.Length > 0) + { + var copy = new byte[report.Length]; + Array.Copy(_inputBuffer, copy, report.Length); + _lastHidResponseUtc = DateTime.UtcNow; + _consecutiveKeepAliveFailures = 0; + + // 触发响应事件(注意:此方法在后台线程调用!) + ResponseReceived?.Invoke(this, new HidResponseReceivedEventArgs(copy)); + //var s = BitConverter.ToString(copy); + //if (copy[0] == 0xA5 && copy[0] == 0x5A) + //{ + // ProcessSpecialFrame(copy); + //} + // 处理协议(如果 ProcessIncomingResponse 涉及 UI,需调度) + ProcessIncomingResponse(copy); + } + } + } + #endregion + public async Task SendAsync(byte[] data) + { + if (_stream == null || !_isRunning) + throw new InvalidOperationException(GetLanguageValueOrDefault("DeviceDisconnected") ?? string.Empty); + + try + { + if (data.Length == 0) return; + + // 分包发送 + for (int i = 0; i < data.Length; i += REPORT_LENGTH - 1) + { + int chunkSize = Math.Min(REPORT_LENGTH - 1, data.Length - i); + var packet = new byte[REPORT_LENGTH]; + packet[0] = 0x00; // Report ID + Array.Copy(data, i, packet, 1, chunkSize); + + lock (_streamLock) + { + _stream?.Write(packet); + } + + OnLog($"📤 发送包: {BitConverter.ToString(packet)}"); + //await Task.Delay(1); // 避免总线过载 + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"{GetLanguageValueOrDefault("HidSendPacketError") ?? string.Empty}:{ex.Message}"); + } + + } + + public async Task DisconnectAsync() + { + _isRunning = false; + _cts?.Cancel(); + + #region (CPU占用高/备用方法适应老版本库) + //if (_receiveThread != null && _receiveThread.IsAlive) + //{ + // _receiveThread.Join(DEVICE_MONITOR_INTERVAL_MS); + //} + #endregion + _hasSentConnectRequest = false; + _inputReceiver = null; + lock (_streamLock) + { + _stream?.Dispose(); + _stream = null; + } + _lastHidResponseUtc = DateTime.MinValue; + _consecutiveKeepAliveFailures = 0; + State.IsDataConnected = false; + State.IsConnected = false; + OnLog("🔌 设备已断开"); + } + + // 响应处理方法 + public void ProcessIncomingResponse(byte[] data) + { + lock (_responseLock) + { + if (_isProcessingResponse) return; + _isProcessingResponse = true; + try + { + if (resNotity.Count < 195) + { + resNotity.AddRange(data); + } + if (resNotity.Count != 195) return; + + var responseType = ParseResponseType(resNotity); + if (responseType == ResponseType.ReadyForData) + { + if (State.IsDataConnected == false) + { + State.IsDataConnected = true; + _connectTcs.TrySetResult(true); + } + else + { + //只有写入钥匙Bin文件时才走下面逻辑 + if (_currentBigPhase == BigTransferPhase.Phase3_Write) + { + switch (State.CurrentPhase) + { + case TransferPhase.Phase2_WaitingForHeaderAck: + HandleHeaderAck(); + break; + case TransferPhase.Phase4_WaitingForDataFrameAck: // 中间确认 + HandleDataFrameAck(); + break; + case TransferPhase.Phase5_WaitingForFinalDataAck: // 最终确认 + HandleFinalDataAck(); + break; + case TransferPhase.Phase11_WaitingForComplete: + HandleFinalComplete(); + break; + default: + State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase} 收到意外的ACK"; + CleanupTransfer(_currentBigPhase); + break; + } + } + else if (_currentBigPhase == BigTransferPhase.Phase1_Connect || _currentBigPhase == BigTransferPhase.Phase2_Generate) + { + HandleFinalComplete(); + } + else + { + //未知情况的ACK回复 + State.StatusMessage = $"⚠️ 大阶段{_currentBigPhase},阶段 {State.CurrentPhase} 收到意外的ACK"; + CleanupTransfer(_currentBigPhase); + } + } + } + else if (responseType == ResponseType.Nak) + { + if (State.CurrentPhase == TransferPhase.Phase7_WaitingForNak) + { + HandleNakResponse(); + } + else + { + State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase} 收到意外的 NAK"; + CleanupTransfer(_currentBigPhase); + } + } + else if (responseType == ResponseType.Ackc) + { + if (State.CurrentPhase == TransferPhase.Phase9_WaitingForAckc) + { + HandleAckcResponse(); + } + else + { + State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase} 收到意外的 ACKC"; + CleanupTransfer(_currentBigPhase); + } + } + else + { + State.StatusMessage = $"⚠️ 阶段 {State.CurrentPhase}未知的错误,{BitConverter.ToString(resNotity.ToArray())}"; + CleanupTransfer(_currentBigPhase); + } + resNotity.Clear(); + } + finally + { + _isProcessingResponse = false; + } + } + } + + private ResponseType ParseResponseType(List buffer) + { + if (buffer.Count == 195) + { + // 检查 ReadyForData(ACK) + if (K3ToolUSBProtocol.IsReadyForDataFrameResponse(buffer)) + { + return ResponseType.ReadyForData; + } + // 检查 NAK + else if (K3ToolUSBProtocol.IsNakResponse(buffer)) + { + return ResponseType.Nak; + } + // 检查 ACKC + else if (K3ToolUSBProtocol.IsAckcResponse(buffer)) + { + return ResponseType.Ackc; + } + } + return ResponseType.Unknown; + } + /// + /// 处理特殊帧(协议待定) + /// + private void ProcessSpecialFrame(byte[] frame) + { + if (frame.Length < 2) return; + + byte header = frame[0]; + byte type = frame[1]; + + if (header != 0xA5) + { + return; + } + + switch (type) + { + case 0x5A: // 固件版本 + if (frame.Length >= 4) + { + string version = $"{frame[2]}.{frame[3]}.0"; + } + break; + + case 0x5B: // 电池电量 + break; + + case 0x5C: // 序列号 + if (frame.Length >= 10) + { + string serialNumber = BitConverter.ToString(frame, 2, 8).Replace("-", ""); + } + break; + default: + break; + } + } + private async Task HandleHeaderAck() + { + OnLog("收到头帧确认,开始发送数据帧"); + State.StatusMessage = "开始发送数据帧"; + State.CurrentPhase = TransferPhase.Phase3_SendingDataFrames; + _currentDataPacketIndex = 0; + + if (_fileBinData == null || State.TotalPackets <= 0) return; + + await SendNextDataFrameAsync(); + } + + private async Task SendNextDataFrameAsync() + { + if (_currentDataPacketIndex >= State.TotalPackets || !IsConnected) return; + var frame = K3ToolUSBProtocol.GetSendDataFrame(_currentDataPacketIndex, _fileBinData!).ToArray(); + await SendAsync(frame); + _currentBigPhaseProgress += (60.0 / State.TotalPackets); + _currentProgress?.Report(_currentBigPhaseProgress); + OnLog($"数据帧 {_currentDataPacketIndex + 1}/{State.TotalPackets} 已发送"); + + if (_currentDataPacketIndex == State.TotalPackets - 1) + { + State.CurrentPhase = TransferPhase.Phase5_WaitingForFinalDataAck; + } + else + { + State.CurrentPhase = TransferPhase.Phase4_WaitingForDataFrameAck; + } + + _currentDataPacketIndex++; + } + + private async Task HandleDataFrameAck() + { + OnLog($"收到数据帧确认 {_currentDataPacketIndex - 1}"); + + if (_currentDataPacketIndex < State.TotalPackets) + { + State.CurrentPhase = TransferPhase.Phase3_SendingDataFrames; + await SendNextDataFrameAsync(); + } + else + { + OnLog("所有数据帧发送完成,等待最终确认"); + State.CurrentPhase = TransferPhase.Phase5_WaitingForFinalDataAck; + } + } + + private async Task HandleFinalDataAck() + { + OnLog("收到最终数据帧确认,准备发送EOT"); + State.CurrentPhase = TransferPhase.Phase6_SendingEOT; + _currentBigPhaseProgress += (5.0) / 3; + _currentProgress?.Report(_currentBigPhaseProgress); + await SendAsync(K3ToolUSBProtocol.EndEOTData); + State.CurrentPhase = TransferPhase.Phase7_WaitingForNak; + } + + private async Task HandleNakResponse() + { + OnLog("收到NAK确认,发送最终EOT"); + State.CurrentPhase = TransferPhase.Phase8_SendingFinalEOT; + _currentBigPhaseProgress += (5.0) / 3; + _currentProgress?.Report(_currentBigPhaseProgress); + await SendAsync(K3ToolUSBProtocol.EndEOTData); + State.CurrentPhase = TransferPhase.Phase9_WaitingForAckc; + } + + private async Task HandleAckcResponse() + { + OnLog("收到ACKC确认,发送结束帧"); + State.CurrentPhase = TransferPhase.Phase10_SendEndFrame; + _currentBigPhaseProgress += (5.0) / 3; + _currentProgress?.Report(_currentBigPhaseProgress); + await SendAsync(K3ToolUSBProtocol.GetEndDataFrame()); + State.CurrentPhase = TransferPhase.Phase11_WaitingForComplete; + if (_hidOperationType == HidOperationTypeEnum.DeviceUpdate) + { + await HandleFinalComplete(); + } + } + + private async Task HandleFinalComplete() + { + OnLog("文件传输完成!"); + State.CurrentPhase = TransferPhase.Completed; + State.IsTransferring = false; + State.StatusMessage = "传输完成"; + // 标记当前大阶段任务完成 + lock (_bigPhaseLock) + { + var currentTask = _bigPhaseTasks.FirstOrDefault(t => t.Phase == _currentBigPhase); + if (currentTask != null) + { + currentTask.IsCompleted = true; + //if (_currentBigPhase == BigTransferPhase.Phase3_Write) + //{ + // // ✅ 报告 100% + // _currentProgress?.Report(100.0); + //} + } + // ========== 触发当前阶段的独立TCS完成 ========== + lock (_phaseTcsLock) + { + if (_phaseTcsDict.TryGetValue(_currentBigPhase, out var currentPhaseTcs) && !currentPhaseTcs.Task.IsCompleted) + { + currentPhaseTcs.TrySetResult(true); + } + } + // ========== 关键:检查是否所有大阶段都已完成 ========== + _allBigPhasesCompleted = _bigPhaseTasks.All(t => t.IsCompleted); + if (_allBigPhasesCompleted) + { + OnAllBigPhasesCompleted(); + // 触发全局TCS完成(只有所有阶段都完成才会执行这行) + _allBigPhasesTcs?.TrySetResult(true); + return; + } + } + } + + + #endregion + private void OnLog(string message, bool isError = false) + { + LogMessage?.Invoke(this, new HidLogMessageEventArgs(message, isError)); + } + + + #region 进度条拓展(烧录文件) + /// + /// 用于测试(本地路径文件),线上采用读取文件方式 + /// + /// + /// + /// + /// + [Obsolete] + public async Task<(bool, string)> SendFileAsync(string filePath, IProgress progress, CancellationToken ct) + { + _currentProgress?.Report(0.0); + if (!IsConnected) + { + return (false, "设备未找到"); + } + + try + { + if (!File.Exists(filePath)) + { + OnLog("文件不存在", true); + return (false, $"烧录的文件不存在"); + } + + _filelocalData = File.ReadAllBytes(filePath); + K3ToolUSBProtocol.SetFileData(_filelocalData); + var totalPackets = (int)Math.Ceiling(_filelocalData.Length / 1024.0); + State.TotalPackets = totalPackets; + State.IsTransferring = true; + // 初始化传输上下文 + _currentProgress = progress; + _currentCancellationToken = ct; + _transferTcs = new TaskCompletionSource(); + + // 注册取消监听 + ct.Register(() => + { + if (!_transferTcs.Task.IsCompleted) + { + OnLog("文件传输已取消", true); + _transferTcs.TrySetCanceled(); + CleanupTransfer(_currentBigPhase); + } + }); + + // 报告初始进度(比如 5%) + progress?.Report(5.0); + // 发送头帧 + var fileName = Path.GetFileName(filePath); + var header = K3ToolUSBProtocol.GetSendHeader(fileName); + + await SendAsync(header); + + State.CurrentPhase = TransferPhase.Phase2_WaitingForHeaderAck; + State.StatusMessage = "等待头帧确认"; + // 等待传输完成(通过 TCS) + bool success = await _transferTcs.Task; + + if (success) + { + return (true, "✅ 文件传输完成!"); + } + else + { + // 可以从 State.StatusMessage 获取更具体的错误信息 + var msg = State.StatusMessage ?? "传输失败"; + if (IsConnected == false) + { + return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】"); + } + else + { + return (false, $"❌【烧录失败】 {msg}"); + } + + } + } + catch (Exception ex) + { + OnLog($"发送文件失败: {ex.Message}", true); + CleanupTransfer(_currentBigPhase); + return (false, $"发送文件失败:{ex.Message}"); + } + } + + public async Task<(bool, string)> SendFileAsync(string fileName, byte[] filebin, IProgress progress, CancellationToken ct) + { + if (!IsConnected) + { + return (false, "设备未找到"); + } + + try + { + if (!IsValidHasFile(filebin)) + { + OnLog("文件不存在", true); + return (false, $"烧录的文件不存在"); + } + K3ToolUSBProtocol.SetFileData(filebin); + var totalPackets = (int)Math.Ceiling(filebin.Length / 1024.0); + State.TotalPackets = totalPackets; + State.IsTransferring = true; + // 初始化传输上下文 + _currentProgress = progress; + _currentCancellationToken = ct; + _transferTcs = new TaskCompletionSource(); + + // 注册取消监听 + ct.Register(() => + { + if (!_transferTcs.Task.IsCompleted) + { + OnLog("文件传输已取消", true); + CleanupTransfer(_currentBigPhase); + } + }); + + // 报告初始进度(比如 5%) + progress?.Report(5.0); + // 发送头帧 + var header = K3ToolUSBProtocol.GetSendHeader(fileName); + + await SendAsync(header); + + State.CurrentPhase = TransferPhase.Phase2_WaitingForHeaderAck; + State.StatusMessage = "等待头帧确认"; + // 等待传输完成(通过 TCS) + bool success = await _transferTcs.Task; + + if (success) + { + return (true, "✅ 烧录成功!"); + } + else + { + // 可以从 State.StatusMessage 获取更具体的错误信息 + var msg = State.StatusMessage ?? "传输失败"; + if (IsConnected == false) + { + return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】"); + } + else + { + return (false, $"❌【烧录失败】 {msg}"); + } + } + } + catch (Exception ex) + { + OnLog($"发送文件失败: {ex.Message}", true); + CleanupTransfer(_currentBigPhase); + return (false, $"发送文件失败:{ex.Message}"); + } + } + + #region 阶段性烧录(连接/生成/写入) + /// + /// 固定单命令(如连接/生成 等命令) + /// + /// + /// + /// + /// + /// + private async Task<(bool, string)> SendSingleCommandAsync(string fileName, byte[] filebin, IProgress progress, CancellationToken ct) + { + if (!IsConnected) + { + return (false, "设备未找到"); + } + try + { + // ========== 关键:为当前大阶段创建独立TCS ========== + TaskCompletionSource currentPhaseTcs; + lock (_phaseTcsLock) + { + // 确保每个阶段只有一个TCS + if (!_phaseTcsDict.TryGetValue(_currentBigPhase, out currentPhaseTcs) || currentPhaseTcs.Task.IsCompleted) + { + currentPhaseTcs = new TaskCompletionSource(); + _phaseTcsDict[_currentBigPhase] = currentPhaseTcs; + } + } + if (!IsValidHasFile(filebin)) + { + OnLog("文件不存在", true); + return (false, $"烧录的文件不存在"); + } + State.IsTransferring = true; + State.CurrentPhase = TransferPhase.Phase11_WaitingForComplete; + await SendAsync(filebin); + // 等待传输完成(通过 TCS) + bool success = await currentPhaseTcs.Task; + + if (success) + { + GetCurrentPhaseStartProgress(); + _currentProgress?.Report(_currentBigPhaseProgress); + return (true, $"基础命令:{_currentBigPhase}完成"); + } + else + { + // 可以从 State.StatusMessage 获取更具体的错误信息 + var msg = State.StatusMessage ?? "传输失败"; + if (IsConnected == false) + { + return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】"); + } + else + { + return (false, $"❌【烧录失败】 {msg}"); + } + } + } + catch (Exception ex) + { + OnLog($"发送文件失败: {ex.Message}", true); + lock (_phaseTcsLock) + { + if (_phaseTcsDict.TryGetValue(_currentBigPhase, out var tcs)) + { + tcs.TrySetCanceled(); + } + } + CleanupTransfer(_currentBigPhase); + return (false, $"发送文件失败:{ex.Message}"); + } + } + private async Task<(bool, string)> SendSingleFileAsync(string fileName, byte[] filebin, IProgress progress, CancellationToken ct) + { + if (!IsConnected) + { + return (false, "设备未找到"); + } + // ========== 关键:为当前大阶段创建独立TCS ========== + TaskCompletionSource currentPhaseTcs; + lock (_phaseTcsLock) + { + if (!_phaseTcsDict.TryGetValue(_currentBigPhase, out currentPhaseTcs) || currentPhaseTcs.Task.IsCompleted) + { + currentPhaseTcs = new TaskCompletionSource(); + _phaseTcsDict[_currentBigPhase] = currentPhaseTcs; + } + } + try + { + if (!IsValidHasFile(filebin)) + { + OnLog("文件不存在", true); + return (false, $"烧录的文件不存在"); + } + K3ToolUSBProtocol.SetFileData(filebin); + var totalPackets = (int)Math.Ceiling(filebin.Length / 1024.0); + State.TotalPackets = totalPackets; + State.IsTransferring = true; + + // 发送头帧 + var header = K3ToolUSBProtocol.GetSendHeader(fileName); + + await SendAsync(header); + GetCurrentPhaseStartProgress(); + State.CurrentPhase = TransferPhase.Phase2_WaitingForHeaderAck; + State.StatusMessage = "等待头帧确认"; + // 等待传输完成(通过 TCS) + bool success = await currentPhaseTcs.Task; + + if (success) + { + double scopedProgressVal = _currentBigPhaseProgress + 5.0; + _currentProgress?.Report(scopedProgressVal); + return (true, "✅ 烧录成功!"); + } + else + { + // 可以从 State.StatusMessage 获取更具体的错误信息 + var msg = State.StatusMessage ?? "传输失败"; + if (IsConnected == false) + { + return (false, $"❌【设备断开连接,请勿在烧录中拔插设备】"); + } + else + { + return (false, $"❌【烧录失败】 {msg}"); + } + } + } + catch (Exception ex) + { + OnLog($"发送文件失败: {ex.Message}", true); + CleanupTransfer(_currentBigPhase); + return (false, $"发送文件失败:{ex.Message}"); + } + } + public async Task<(bool, string)> SendFileProcessAsync(string fileName, byte[] filebin, IProgress progress, CancellationToken ct) + { + _currentProgress?.Report(0.0); + if (!IsConnected) + { + return (false, "设备未找到"); + } + + try + { + if (!IsValidHasFile(filebin)) + { + OnLog("文件不存在", true); + return (false, $"烧录的文件不存在"); + } + //========== 关键:初始化全局大阶段任务完成源 ========== + _allBigPhasesTcs = new TaskCompletionSource(); + _allBigPhasesCompleted = false; + // 初始化大阶段任务队列 + lock (_bigPhaseLock) + { + _bigPhaseTasks.Clear(); + _bigPhaseTasks.Add(new BigPhaseTask + { + Phase = BigTransferPhase.Phase1_Connect, + IsCompleted = false + }); + _bigPhaseTasks.Add(new BigPhaseTask + { + Phase = BigTransferPhase.Phase2_Generate, + IsCompleted = false + }); + _bigPhaseTasks.Add(new BigPhaseTask + { + Phase = BigTransferPhase.Phase3_Write, + IsCompleted = false + }); + } + _fileBinData = filebin; + _fileBinName = fileName; + // 初始化传输上下文 + _currentProgress = progress; + _currentCancellationToken = ct; + + // ========== 关键:取消监听改为触发全局TCS ========== + ct.Register(() => + { + if (_allBigPhasesTcs != null && !_allBigPhasesTcs.Task.IsCompleted) + { + OnLog("多阶段传输已取消", true); + _allBigPhasesTcs.TrySetCanceled(); + CleanupTransfer(); + } + }); + + // 启动第一个大阶段 + await SwitchToNextBigPhaseAsync(); + + // 等待所有大阶段完成 + // ========== 关键:等待所有大阶段完成(直到HandleFinalComplete触发所有阶段完成) ========== + bool allSuccess = false; + try + { + // 等待全局TCS完成(只有所有大阶段执行HandleFinalComplete并标记完成后才会触发) + allSuccess = await _allBigPhasesTcs.Task; + } + catch (OperationCanceledException) + { + OnLog("多阶段传输被取消", true); + return (false, "传输已取消"); + } + + // 收集结果 + string errorMsg = string.Empty; + lock (_bigPhaseLock) + { + errorMsg = string.Join("; ", _bigPhaseTasks.Where(t => !string.IsNullOrEmpty(t.ErrorMessage)).Select(t => $"{t.Phase}: {t.ErrorMessage}")); + } + + if (allSuccess) + { + _currentProgress.Report(100.00); + return (true, GetLanguageValueOrDefault("BurnSuccess")); + } + else + { + return (false, string.IsNullOrEmpty(errorMsg) ? GetLanguageValueOrDefault("BurnError") : errorMsg); + } + + } + catch (Exception ex) + { + OnLog($"发送文件失败: {ex.Message}", true); + CleanupTransfer(); + return (false, $"发送文件失败:{ex.Message}"); + } + } + private async Task SwitchToNextBigPhaseAsync() + { + BigPhaseTask? nextTask = null; + // 先在锁内获取下一个任务(避免锁内执行异步逻辑) + lock (_bigPhaseLock) + { + nextTask = _bigPhaseTasks.FirstOrDefault(t => !t.IsCompleted); + if (nextTask == null) + { + OnAllBigPhasesCompleted(); + return; + } + // 切换大阶段状态(提前切换,避免重复执行) + OnBigPhaseChanged(nextTask.Phase); + } + + // 锁外执行异步逻辑(避免阻塞其他线程) + try + { + bool phaseSuccess = false; + string phaseError = string.Empty; + + switch (nextTask.Phase) + { + case BigTransferPhase.Phase1_Connect: + (phaseSuccess, phaseError) = await SendSingleCommandAsync( + K3ToolUSBProtocol.ConnectFileName, + K3ToolUSBProtocol.ConnectData, + _currentProgress!, + _currentCancellationToken); + if (phaseSuccess) + { + // 延迟,避免设备未就绪(可选,根据设备特性调整) + await Task.Delay(1000); + } + else + { + + } + break; + case BigTransferPhase.Phase2_Generate: + (phaseSuccess, phaseError) = await SendSingleCommandAsync( + K3ToolUSBProtocol.CreateRemoteFileName, + K3ToolUSBProtocol.CreateRemoteData, + _currentProgress!, + _currentCancellationToken); + if (phaseSuccess) + { + // 延迟,避免设备未就绪(可选,根据设备特性调整) + await Task.Delay(500); + } + break; + case BigTransferPhase.Phase3_Write: + (phaseSuccess, phaseError) = await SendSingleFileAsync( + _fileBinName, + _fileBinData!, + _currentProgress!, + _currentCancellationToken); + if (phaseSuccess) + { + // 延迟,避免设备未就绪(可选,根据设备特性调整) + //await Task.Delay(500); + } + break; + } + + // 再次加锁更新任务状态(关键:确保状态更新原子性) + lock (_bigPhaseLock) + { + nextTask.IsCompleted = true; + nextTask.ErrorMessage = phaseError; + } + + if (!phaseSuccess) + { + OnLog($"❌ {nextTask.Phase} 执行失败: {phaseError}", true); + // ========== 触发当前阶段TCS失败 + 全局TCS失败 ========== + lock (_phaseTcsLock) + { + if (_phaseTcsDict.TryGetValue(nextTask.Phase, out var tcs)) + { + tcs.TrySetResult(false); + } + } + _allBigPhasesTcs?.TrySetResult(false); + return; + } + + OnLog($"✅ {nextTask.Phase} 执行完成,准备进入下一阶段"); + + // 递归调用,进入下一个大阶段 + await SwitchToNextBigPhaseAsync(); + } + catch (Exception ex) + { + lock (_bigPhaseLock) + { + nextTask.IsCompleted = true; + nextTask.ErrorMessage = ex.Message; + } + OnLog($"❌ {nextTask.Phase} 执行异常: {ex.Message}", true); + _allBigPhasesTcs?.TrySetResult(false); + } + } + #endregion + + #region 辅助方法 + public void Dispose() + { + _isRunning = false; + _cts?.Cancel(); + #region (CPU占用高/备用方法适应老版本库) + //if (_receiveThread != null && _receiveThread.IsAlive) + //{ + // _receiveThread.Join(DEVICE_MONITOR_INTERVAL_MS); + //} + #endregion + _inputReceiver = null; + lock (_streamLock) + { + _stream?.Dispose(); + _stream = null; + } + CleanupTransfer(); + _cts?.Dispose(); + } + private void CleanupTransfer(BigTransferPhase? phase = null) + { + State.IsTransferring = false; + + // 清理指定阶段的TCS + if (phase.HasValue) + { + lock (_phaseTcsLock) + { + if (_phaseTcsDict.ContainsKey(phase.Value)) + { + _phaseTcsDict[phase.Value].TrySetCanceled(); + _phaseTcsDict.Remove(phase.Value); + } + } + } + else + { + // 清理所有阶段的TCS + lock (_phaseTcsLock) + { + foreach (var tcs in _phaseTcsDict.Values) + { + tcs.TrySetCanceled(); + } + _phaseTcsDict.Clear(); + } + // 清理全局TCS + _allBigPhasesTcs?.TrySetCanceled(); + _allBigPhasesTcs = null; + _allBigPhasesCompleted = false; + } + + _currentProgress = null; + _fileBinData = null; + _fileBinName = null; + } + private bool IsValidHasFile(byte[] filebin) + { + if (filebin == null || filebin.Length == 0) + { + return false; + } + return true; + } + + #region 阶段进度条数据 + private double GetCurrentPhaseStartProgress() + { + switch (_currentBigPhase) + { + case BigTransferPhase.Phase1_Connect: + //连接指令较快 + _currentBigPhaseProgress = 10; + break; + case BigTransferPhase.Phase2_Generate: + //指令较快 + _currentBigPhaseProgress += 10; + break; + case BigTransferPhase.Phase3_Write: + _currentBigPhaseProgress += 10; + break; + default: + return 0.0; + } + return _currentBigPhaseProgress; + } + #endregion + #endregion + + #endregion + + #region 设备固件升级 + public async Task<(bool, string)> SendFileProcessAsync(string fileName, byte[] filebin, HidOperationTypeEnum hidOperationType, IProgress progress, CancellationToken ct) + { + _hidOperationType = hidOperationType; + _currentProgress?.Report(0.0); + if (!IsConnected) + { + return (false, "设备未找到"); + } + + try + { + if (!IsValidHasFile(filebin)) + { + OnLog("文件不存在", true); + return (false, $"烧录的文件不存在"); + } + //========== 关键:初始化全局大阶段任务完成源 ========== + _allBigPhasesTcs = new TaskCompletionSource(); + _allBigPhasesCompleted = false; + // 初始化大阶段任务队列 + lock (_bigPhaseLock) + { + _bigPhaseTasks.Clear(); + _bigPhaseTasks.Add(new BigPhaseTask + { + Phase = BigTransferPhase.Phase1_Connect, + IsCompleted = false + }); + _bigPhaseTasks.Add(new BigPhaseTask + { + Phase = BigTransferPhase.Phase2_Generate, + IsCompleted = false + }); + _bigPhaseTasks.Add(new BigPhaseTask + { + Phase = BigTransferPhase.Phase3_Write, + IsCompleted = false + }); + } + _fileBinData = filebin; + _fileBinName = fileName; + // 初始化传输上下文 + _currentProgress = progress; + _currentCancellationToken = ct; + + // ========== 关键:取消监听改为触发全局TCS ========== + ct.Register(() => + { + if (_allBigPhasesTcs != null && !_allBigPhasesTcs.Task.IsCompleted) + { + OnLog("多阶段传输已取消", true); + _allBigPhasesTcs.TrySetCanceled(); + CleanupTransfer(); + } + }); + + // 启动第一个大阶段 + await SwitchToNextBigPhaseAsync(); + + // 等待所有大阶段完成 + // ========== 关键:等待所有大阶段完成(直到HandleFinalComplete触发所有阶段完成) ========== + bool allSuccess = false; + try + { + // 等待全局TCS完成(只有所有大阶段执行HandleFinalComplete并标记完成后才会触发) + allSuccess = await _allBigPhasesTcs.Task; + } + catch (OperationCanceledException) + { + OnLog("多阶段传输被取消", true); + return (false, "传输已取消"); + } + + // 收集结果 + string errorMsg = string.Empty; + lock (_bigPhaseLock) + { + errorMsg = string.Join("; ", _bigPhaseTasks.Where(t => !string.IsNullOrEmpty(t.ErrorMessage)).Select(t => $"{t.Phase}: {t.ErrorMessage}")); + } + + if (allSuccess) + { + _currentProgress.Report(100.00); + return (true, GetLanguageValueOrDefault("BurnSuccess")); + } + else + { + return (false, string.IsNullOrEmpty(errorMsg) ? GetLanguageValueOrDefault("BurnError") : errorMsg); + } + + } + catch (Exception ex) + { + OnLog($"发送文件失败: {ex.Message}", true); + CleanupTransfer(); + return (false, $"发送文件失败:{ex.Message}"); + } + } + #endregion + + #region 获取设备版本 + public async Task<(bool, string)> GetDeviceVersion(HidOperationTypeEnum hidOperationType) + { + if (!IsConnected) + { + return (false, "设备未找到"); + } + await Task.Delay(5000); + await SendAsync(K3ToolUSBProtocol.VersionQuery()); + return (true, "获取设备版本指令已发送"); + } + #endregion + + #region 获取K3ToolUSB设备对应的U盘盘符 + public async Task FindAssociatedUsbDisk(int vid, int pid) + { + string model = "K3 TOOL"; + try + { + // 步骤1:获取所有可移动磁盘盘符(确保设备已挂载) + var removableDrives = DriveInfo.GetDrives() + .Where(d => d.DriveType == DriveType.Removable && d.IsReady) + .ToList(); + + if (!removableDrives.Any()) + return null; + + // 步骤2:查询所有 USB 磁盘的 Model 和 Index + using var searcher = new ManagementObjectSearcher( + "SELECT Index, Model FROM Win32_DiskDrive WHERE InterfaceType='USB'"); + + var usbDisks = new List<(uint Index, string Model)>(); + foreach (ManagementObject disk in searcher.Get()) + { + if (disk["Model"] is string diskModel && + diskModel.IndexOf(model, StringComparison.OrdinalIgnoreCase) >= 0) + { + uint index = (uint)(disk["Index"] ?? 0); + usbDisks.Add((index, diskModel)); + } + } + + // 步骤3:根据匹配数量决定策略 + if (usbDisks.Count == 0) + return null; // 无匹配设备 + + if (usbDisks.Count == 1 && removableDrives.Count == 1) + { + // 唯一匹配 → 返回唯一盘符 + return removableDrives[0].RootDirectory.FullName; + } + + // 多个匹配?返回第一个(或扩展为用户选择) + return removableDrives.FirstOrDefault()?.RootDirectory.FullName; + } + catch (ManagementException ex) when (ex.Message.Contains("Invalid class")) + { + // WMI 损坏,降级处理 + // 如果只有一个可移动磁盘,且您确定它是目标设备,可直接返回 + var drives = DriveInfo.GetDrives() + .Where(d => d.DriveType == DriveType.Removable && d.IsReady) + .ToList(); + + if (drives.Count == 1) + return drives[0].RootDirectory.FullName; + + return null; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"FindUsbDiskByModel error: {ex}"); + return null; + } + } + #endregion + + #region 复制文件到HID U盘 + public async Task<(bool, string)> CopyToUdiskAsync(string sourcePath, bool isDirectory, IProgress progress, + CancellationToken cancellationToken = default) + { + await OpenUDiskAndDetectDriveAsync(); + var targetDrive = await FindAssociatedUsbDisk(_vendorId, _productId); + if (string.IsNullOrEmpty(targetDrive)) + throw new InvalidOperationException("未检测到 HID U盘设备"); + // 目标路径改为 {targetDrive}\received_data + string receivedDataDir = Path.Combine(targetDrive, usbdiskDir); + + // 自动创建 received_data 目录(如果不存在) + Directory.CreateDirectory(receivedDataDir); + // 启动保活任务(每60秒发送一次 ConnectData) + using var keepAliveCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var keepAliveTask = KeepDeviceAliveAsync(keepAliveCts.Token); + try + { + if (isDirectory) + { + //文件夹复制 + string sourceFolderName = Path.GetFileName(sourcePath); + + string destDir; + if (string.Equals(sourceFolderName, usbdiskDir, StringComparison.OrdinalIgnoreCase)) + { + // 源文件夹名 == usbdiskDir(如 "received_data") + // → 直接复制其内部内容到 receivedDataDir,不嵌套 + destDir = receivedDataDir; + } + else + { + // 正常:在 received_data 下创建子文件夹 + destDir = Path.Combine(receivedDataDir, sourceFolderName); + } + int skipped = await CopyDirectoryWithProgressAsync(sourcePath, destDir, progress, cancellationToken); + progress?.Report(100.0); + return skipped > 0 + ? (true, $"数据同步完成({skipped} 个文件因损坏被跳过,建议对U盘执行 chkdsk 修复)") + : (true, "数据同步完成"); + } + else + { + //单文件复制 + string destFile = Path.Combine(receivedDataDir, Path.GetFileName(sourcePath)); + long totalSize = new FileInfo(sourcePath).Length; + var context = new CopyProgressContext(totalSize, progress); + await CopyFileWithProgressAsync(sourcePath, destFile, context, cancellationToken); + + progress?.Report(100.0); + return (true, "数据同步完成"); + } + } + catch (Exception ex) + { + var failureCategory = UsbCopyGuard.ClassifyCopyException(ex); + var failureLabel = UsbCopyGuard.GetFailureLabel(failureCategory); + _logger.LogWarning(ex, + "复制到K3Tool设备失败。类别:{FailureCategory},目标盘符:{TargetDrive},源路径:{SourcePath},连接状态: IsConnected={IsConnected}, IsDataConnected={IsDataConnected}", + failureLabel, + targetDrive, + sourcePath, + State.IsConnected, + State.IsDataConnected); + return (false, $"错误信息:[{failureLabel}] {ex.Message}"); + } + finally + { + // 复制完成后,停止保活 + keepAliveCts.Cancel(); + //try + //{ + // // 无论成功与否,都先尝试弹出卷;即使失败也继续发送关闭U盘指令。 + // await WindowsVolumeEjector.EjectVolumeAsync(targetDrive); + //} + //catch (OperationCanceledException) { /* 忽略 */ } + //catch (Exception ex) + //{ + // _logger.LogWarning(ex, "弹出K3Tool卷失败,继续执行关闭U盘指令。目标盘符:{TargetDrive}", targetDrive); + //} + + try + { + await CloseUDisk(); + } + catch (OperationCanceledException) { /* 忽略 */ } + finally + { + try + { + await keepAliveTask; // 等待保活任务结束 + } + catch (OperationCanceledException) { /* 忽略 */ } + } + + // 同步完成后执行 devcon remove+rescan(应急方案):把设备从 U 盘模式"软件拔插"恢复为 HID 模式, + // 清理 Win7 残留的设备状态,避免下次切换再次 Code 10。失败静默降级,不影响返回结果。 + try + { + bool restoreOk = await UsbDevconHelper.RemoveAndRescanAsync(_vendorId, _productId); + OnLog(restoreOk + ? "✅ 同步完成,devcon remove+rescan 已执行,设备已恢复为 HID 模式" + : "⚠️ 同步完成但 devcon remove+rescan 未执行(需要管理员权限,且 devcon.exe 需随程序分发到运行目录)"); + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "同步完成后 devcon remove+rescan 执行失败"); + } + + } + } + #region 普通复制方法 + //private async Task CopyFileWithCancellation(string src, string dest, CancellationToken ct) + //{ + // const int bufferSize = 64 * 1024; // 64KB + // var buffer = new byte[bufferSize]; + + // Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + + // using var sourceStream = File.OpenRead(src); + // using var destStream = File.Create(dest); + + // int bytesRead; + // while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) + // { + // await destStream.WriteAsync(buffer, 0, bytesRead, ct); + // // 可选:定期检查取消(ReadAsync 已支持) + // } + //} + //private async Task CopyDirectoryRecursive(string src, string dest, CancellationToken ct) + //{ + // Directory.CreateDirectory(dest); + + // foreach (var file in Directory.GetFiles(src)) + // { + // ct.ThrowIfCancellationRequested(); // 关键:检查取消 + // var destFile = Path.Combine(dest, Path.GetFileName(file)); + // await CopyFileWithCancellation(file, destFile, ct); + // } + + // foreach (var dir in Directory.GetDirectories(src)) + // { + // ct.ThrowIfCancellationRequested(); + // await CopyDirectoryRecursive(dir, Path.Combine(dest, Path.GetFileName(dir)), ct); + // } + //} + #endregion + #region 带进度的复制方法 + private async Task CopyFileWithProgressAsync( + string src, + string dest, + CopyProgressContext context, + CancellationToken ct) + { + var throttle = UsbCopyGuard.GetCopyThrottleSettings(); + int bufferSize = throttle.BufferSize; + try + { + UsbCopyGuard.ProbeCopyTargetHealth(dest); + } + catch (IOException ex) + { + _logger?.LogError("目录损坏或无法访问:{Dest}, {Message}", dest, ex.Message); + throw; + } + if (File.Exists(dest)) + { + FileInfo fi = new FileInfo(dest); + + // 长度读取失败(损坏条目)按损坏文件处理,走下面的清理逻辑 + long existingLength; + try + { + existingLength = fi.Length; + } + catch + { + existingLength = 0; + } + + // 正常文件 → 直接跳过 + if (existingLength > 0) + { + context.ReportCopied(existingLength); + //_logger?.LogInformation("文件已存在,跳过:{Dest}", dest); + return; + } + + // =============== 0KB/损坏文件:删除 → 重命名 → 强制覆盖 =============== + if (UsbCopyGuard.TryClearCorruptedTarget(_logger, dest, out var clearDetail)) + { + // 给设备一点点刷新时间(非常短,不影响速度) + await Task.Delay(20, ct); + } + else + { + _logger?.LogError("损坏文件清理失败,跳过:{Dest}, 错误:{Detail}", dest, clearDetail); + return; + } + } + + byte[] buffer = new byte[bufferSize]; + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + + using var sourceStream = new FileStream(src, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, true); + using var destStream = new FileStream(dest, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, true); + + int bytesRead; + int chunkIndex = 0; + + try + { + while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) + { + chunkIndex++; + ct.ThrowIfCancellationRequested(); + + UsbCopyGuard.ThrowIfCopyTargetUnavailable(_logger, State, Path.GetPathRoot(dest), dest, chunkIndex, context.CopiedBytes); + await UsbCopyGuard.WriteChunkWithTimeoutAsync(destStream, buffer, bytesRead, ct, dest, chunkIndex, context.CopiedBytes); + + context.ReportCopied(bytesRead); + + // ====================== 加速关键:5ms 超短延迟 ====================== + await Task.Delay(throttle.ChunkDelayMs, ct); + } + + // 整个文件写完只刷 1 次 + await destStream.FlushAsync(CancellationToken.None); + } + finally + { + // 文件间隔 50ms 防掉盘 + //await Task.Delay(throttle.FileDelayMs, ct); + } + } + /// + /// 检测目录健康状态,确保目标路径可访问且没有损坏。 + /// + /// + /// + public static void ProbeCopyTargetHealth(string destinationPath) + { + string? dir = Path.GetDirectoryName(destinationPath); + if (string.IsNullOrEmpty(dir)) return; + + try + { + // 仅检查目录是否存在,最轻量操作 + bool exists = Directory.Exists(dir); + if (!exists) Directory.CreateDirectory(dir); + } + catch + { + // 出错代表目录损坏 + throw new IOException("目标目录已损坏,请修复U盘后重试"); + } + } + private async Task CopyDirectoryWithProgressAsync( + string src, + string dest, + IProgress progress, + CancellationToken ct) + { + long totalBytes = CalculateTotalSize(src); + var context = new CopyProgressContext(totalBytes, progress); + + int skipped = await CopyDirectoryRecursiveInternal(src, dest, context, ct); + + // 确保最终进度为 100% + progress?.Report(100.0); + return skipped; + } + + private async Task CopyDirectoryRecursiveInternal( + string src, + string dest, + CopyProgressContext context, + CancellationToken ct) + { + int skipped = 0; + + // 目标目录本身损坏/不可创建 → 跳过整个子目录,不影响其他文件 + try + { + Directory.CreateDirectory(dest); + } + catch (Exception ex) + { + _logger?.LogError("目标目录损坏或无法创建,跳过该目录:{Dest}, 错误:{Msg}", dest, ex.Message); + return 1; + } + + string[] files; + try + { + files = Directory.GetFiles(src); + } + catch (Exception ex) + { + _logger?.LogError("源目录枚举失败,跳过该目录:{Src}, 错误:{Msg}", src, ex.Message); + return 1; + } + + foreach (var file in files) + { + ct.ThrowIfCancellationRequested(); + var destFile = Path.Combine(dest, Path.GetFileName(file)); + try + { + await CopyFileWithProgressAsync(file, destFile, context, ct); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + var category = UsbCopyGuard.ClassifyCopyException(ex); + // 设备级问题(断联/写入超时):重试无意义,中断整个同步 + if (category == UsbCopyGuard.CopyFailureCategory.DeviceDisconnected || + category == UsbCopyGuard.CopyFailureCategory.WriteTimeout) + { + throw; + } + _logger?.LogWarning("文件复制失败,跳过继续:{Src} → {Dest}, 类别:{Label}, 错误:{Msg}", + file, destFile, UsbCopyGuard.GetFailureLabel(category), ex.Message); + skipped++; + } + } + + string[] dirs; + try + { + dirs = Directory.GetDirectories(src); + } + catch (Exception ex) + { + _logger?.LogError("源子目录枚举失败,跳过该目录:{Src}, 错误:{Msg}", src, ex.Message); + return skipped + 1; + } + + foreach (var dir in dirs) + { + ct.ThrowIfCancellationRequested(); + var destSubDir = Path.Combine(dest, Path.GetFileName(dir)); + skipped += await CopyDirectoryRecursiveInternal(dir, destSubDir, context, ct); + } + + return skipped; + } + + #endregion + /// + /// 递归计算目录中所有文件的总字节数。 + /// + private long CalculateTotalSize(string path) + { + long total = 0; + try + { + // 文件 + if (File.Exists(path)) + return new FileInfo(path).Length; + + // 目录 + foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) + { + total += new FileInfo(file).Length; + } + } + catch (Exception ex) + { + // 可选:记录日志 + System.Diagnostics.Debug.WriteLine($"计算大小失败: {ex.Message}"); + } + return total; + } + + #endregion + + #region 打开/关闭Hid MSC U盘 + private async Task OpenUDisk() + { + await SendAsync(K3ToolUSBProtocol.OpentheUdiskData); + } + + private async Task CloseUDisk() + { + await SendAsync(K3ToolUSBProtocol.ClosetheUdiskData); + _logger?.LogWarning($"{DateTime.Now}关闭U盘指令"); + + } + public async Task OpenUDiskAndDetectDriveAsync(int maxWaitSeconds = 50, int pollIntervalMs = 500, CancellationToken cancellationToken = default) + { + // 1. 发送打开U盘指令 + await OpenUDisk(); // 这个方法负责发 HID 指令 + + // 2. 设备固件收到指令后会重新枚举 USB(HID 模式 → U 盘模式)。 + // Win7 的 USB 栈对热切换支持差(典型表现为 Code 10),模式切换与重新枚举需要时间, + // 先固定等待 2 秒让 USB 栈完成切换,再开始轮询,避免过早轮询全部落空。 + try + { + await Task.Delay(2000, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + + // 3. 轮询检测盘符,直到出现或超时 + var stopwatch = Stopwatch.StartNew(); + var lastProgressLogUtc = DateTime.UtcNow; + bool removeRescanAttempted = false; + while (stopwatch.Elapsed.TotalSeconds < maxWaitSeconds) + { + cancellationToken.ThrowIfCancellationRequested(); + + string drive = await FindAssociatedUsbDisk(_vendorId, _productId); + if (!string.IsNullOrEmpty(drive)) + { + // 可选:再验证该盘是否可写(防“假挂载”) + try + { + var testFile = Path.Combine(drive, ".k3tool_test"); + File.WriteAllText(testFile, "ok"); + File.Delete(testFile); + return drive; // 成功 + } + catch + { + // 盘符存在但不可写(可能还在初始化),继续等待 + } + } + + // 每 5 秒输出一次等待进度,便于用户判断设备是否仍在切换中 + if (DateTime.UtcNow - lastProgressLogUtc >= TimeSpan.FromSeconds(5)) + { + lastProgressLogUtc = DateTime.UtcNow; + OnLog($"⏳ 正在等待 U 盘出现(已等待 {stopwatch.Elapsed.TotalSeconds:F0}s / {maxWaitSeconds}s)..." + + "若长时间无盘符,请尝试拔插设备或重启设备"); + } + + // devcon 兜底(Win7 Code 10 场景):10 秒仍无盘符时直接执行 + // remove + rescan(删除设备实例后重新扫描,等价于软件拔插,最彻底)。 + // devcon.exe 未分发或非管理员时 UsbDevconHelper 返回 false,静默降级不影响原流程。 + if (!removeRescanAttempted && stopwatch.Elapsed.TotalSeconds >= 10) + { + removeRescanAttempted = true; + OnLog("🔄 尚未检测到 U 盘,直接执行 devcon remove + rescan(软件拔插)..."); + bool removeOk = await UsbDevconHelper.RemoveAndRescanAsync(_vendorId, _productId); + OnLog(removeOk + ? "✅ devcon remove+rescan 已完成,等待设备重新枚举..." + : "⚠️ devcon remove+rescan 未执行(需要管理员权限,且 devcon.exe 需随程序分发到运行目录)"); + } + + await Task.Delay(pollIntervalMs, cancellationToken); + } + + throw new TimeoutException( + $"在 {maxWaitSeconds} 秒内未检测到 U 盘设备(VID={_vendorId}, PID={_productId})。" + + "若设备管理器中该设备显示黄色感叹号(Code 10),请拔插设备或重启设备后重试。"); + } + + public async Task CloseUDiskSafeDriveAsync(string targetDrive="") + { + //无论成功与否,都尝试关闭U盘并发送关闭指令 + //await WindowsVolumeEjector.EjectVolumeAsync(targetDrive); + + // 按固定时序执行:发指令 → 关闭 HID 句柄 → 等待切换 → 重试连接。 + // 注:发送"关闭U盘"指令依赖 HID 流,故先发指令、再断开句柄(与物理拔插同理)。 + + // Step 1: 发送"关闭U盘"指令给 HID + await CloseUDisk(); + + // Step 2: 关闭所有 HID 句柄(释放设备流,让 Windows 能干净地重新枚举) + try + { + await DisconnectAsync(); + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "关闭U盘后断开 HID 失败"); + } + + // Step 3: 等待固件完成切换 + Win7 反应 + await Task.Delay(600); + + // Step 4: devcon remove+rescan 应急清理(Win7 残留幽灵节点,等价于软件拔插) + try + { + bool ok = await UsbDevconHelper.RemoveAndRescanAsync(_vendorId, _productId); + OnLog(ok + ? "✅ devcon remove+rescan 已执行,等待设备重新枚举..." + : "⚠️ devcon remove+rescan 未执行(需要管理员权限,且 devcon.exe 需随程序分发到运行目录)"); + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "关闭U盘后 devcon remove+rescan 执行失败"); + } + + // Step 5: 重试打开 HID 设备(等待就绪 + 重连) + for (int i = 0; i < 10; i++) + { + await Task.Delay(500); + try + { + bool deviceExists = DeviceList.Local.GetHidDevices(_vendorId, _productId).Any(); + if (deviceExists && IsConnected) + { + OnLog("✅ 设备已恢复为 HID 模式并就绪"); + return; + } + if (deviceExists && !IsConnected) + { + OnLog("🔌 设备已出现,尝试重新连接..."); + await TryReconnectAsync(); + } + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "重试连接 HID 设备异常"); + } + } + OnLog("⚠️ 等待设备就绪超时,将依赖自动重连机制"); + } + #endregion + #region Hid设备保活 + private async Task KeepDeviceAliveAsync(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + //if (_lastHidResponseUtc != DateTime.MinValue) + //{ + // var silenceDuration = DateTime.UtcNow - _lastHidResponseUtc; + // if (silenceDuration.TotalMilliseconds >= HID_RESPONSE_WARNING_MS) + // { + // var warning = $"⚠️ HID 已 {silenceDuration.TotalSeconds:F0} 秒未收到设备响应,设备可能忙碌或即将断联"; + // _logger.LogWarning(warning); + // OnLog(warning, true); + // } + //} + + // 发送连接心跳命令 + await SendAsync(K3ToolUSBProtocol.ConnectData); + _consecutiveKeepAliveFailures = 0; + } + catch (Exception ex) + { + //_consecutiveKeepAliveFailures++; + //if (UsbCopyGuard.ShouldEmitRepeatedWarning(_consecutiveKeepAliveFailures)) + //{ + // var warning = $"⚠️ 保活命令发送失败,第{_consecutiveKeepAliveFailures}次。{ex.Message}"; + // _logger.LogWarning(ex, warning); + // OnLog(warning, true); + //} + } + + // 等待 50 秒,或被取消 + await Task.Delay(KEEP_ALIVE_INTERVAL_MS, cancellationToken); + } + } + catch (OperationCanceledException) + { + // 正常取消,不处理 + } + } + + public async Task KeepDeviceAliveAsync() + { + CancellationToken cancellationToken = default; + // 启动保活任务(每50秒发送一次 ConnectData) + using var keepAliveCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var keepAliveTask = KeepDeviceAliveAsync(keepAliveCts.Token); + } + #endregion + + #region 文件复制进度上下文 + private class CopyProgressContext + { + public long CopiedBytes { get; set; } + public long TotalBytes { get; } + public IProgress Progress { get; } + + public CopyProgressContext(long totalBytes, IProgress progress) + { + TotalBytes = totalBytes; + Progress = progress; + } + + /// + /// 更新已复制字节数并上报进度 + /// + public void ReportCopied(long additionalBytes) + { + CopiedBytes += additionalBytes; + if (TotalBytes > 0) + { + double percentage = Math.Min(100.0, (double)CopiedBytes / TotalBytes * 100 * 0.79) + 20; + Progress?.Report(percentage); + } + } + } + + public static class UsbCopyGuard + { + private const int WriteTimeoutMs = 30000; + + public readonly record struct CopyThrottleSettings( + int BufferSize, + int ChunkDelayMs, + int FileDelayMs, + int FlushIntervalBytes); + + public enum CopyFailureCategory + { + Unknown, + DeviceDisconnected, + WriteTimeout, + MscFileSystemCorruption, + IoFailure + } + + public static void ThrowIfCopyTargetUnavailable( + ILogger logger, + TransferState state, + string? driveRoot, + string destinationPath, + int chunkIndex, + long copiedBytes) + { + if (state.IsConnected && state.IsDataConnected) + return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var driveText = string.IsNullOrWhiteSpace(driveRoot) ? "未知盘符" : driveRoot; + var message = $"[{timestamp}] 设备断联,停止复制。目标盘符:{driveText},文件:{destinationPath},chunk:{chunkIndex},已复制:{copiedBytes}字节"; + logger.LogWarning(message); + throw new IOException(message); + } + + public static void ProbeCopyTargetHealth(string destinationPath) + { + var directoryPath = Path.GetDirectoryName(destinationPath); + if (string.IsNullOrWhiteSpace(directoryPath)) + { + throw new IOException($"无法确定目标目录。文件:{destinationPath}"); + } + + var directoryInfo = new DirectoryInfo(directoryPath); + _ = directoryInfo.Exists; + _ = directoryInfo.GetFiles().Length; + } + + /// + /// 清理MSC上损坏的目标文件,按“删除 → 重命名 → 强制覆盖”逐级降级。 + /// 返回 true 表示目标已清理(或原本不存在),可以继续复制;false 表示清理失败,应跳过该文件。 + /// + public static bool TryClearCorruptedTarget(ILogger logger, string destPath, out string detail) + { + detail = string.Empty; + + // 1. 直接删除(文件不存在时 File.Delete 不会抛异常) + try + { + if (File.Exists(destPath)) + { + File.Delete(destPath); + return true; + } + return true; // 目标不存在,无需清理 + } + catch (Exception ex) + { + logger?.LogWarning("损坏目标文件直接删除失败:{Dest},错误:{Msg}", destPath, ex.Message); + } + + // 2. 重命名 → 再删除(FAT 目录项重写通常比直接删除更宽容) + string newName = destPath + ".corrupt_" + Guid.NewGuid().ToString("N").Substring(0, 8); + try + { + File.Move(destPath, newName); + logger?.LogWarning("损坏目标文件已重命名:{Dest} → {NewName}", destPath, newName); + try { File.Delete(newName); } catch { } + return true; + } + catch (Exception ex) + { + logger?.LogWarning("损坏目标文件重命名失败:{Dest} → {NewName},错误:{Msg}", destPath, newName, ex.Message); + } + + // 3. 强制覆盖:FileMode.Create 重建文件(可能跳过损坏的目录项) + try + { + using (var fs = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096)) + { + fs.SetLength(0); + } + return true; + } + catch (Exception ex) + { + detail = ex.Message; + logger?.LogWarning("损坏目标文件强制覆盖失败:{Dest},错误:{Msg}", destPath, ex.Message); + return false; + } + } + + public static CopyFailureCategory ClassifyCopyException(Exception ex) + { + if (ex is IOException ioEx) + { + var message = ioEx.Message ?? string.Empty; + if (message.IndexOf("设备断联", StringComparison.OrdinalIgnoreCase) >= 0) + { + return CopyFailureCategory.DeviceDisconnected; + } + + if (message.IndexOf("设备写入超时", StringComparison.OrdinalIgnoreCase) >= 0) + { + return CopyFailureCategory.WriteTimeout; + } + + if (message.IndexOf("文件或目录损坏且无法读取", StringComparison.OrdinalIgnoreCase) >= 0) + { + return CopyFailureCategory.MscFileSystemCorruption; + } + + return CopyFailureCategory.IoFailure; + } + + return CopyFailureCategory.Unknown; + } + + public static string GetFailureLabel(CopyFailureCategory category) + { + return category switch + { + CopyFailureCategory.DeviceDisconnected => "HID断联", + CopyFailureCategory.WriteTimeout => "写入超时", + CopyFailureCategory.MscFileSystemCorruption => "MSC文件系统损坏", + CopyFailureCategory.IoFailure => "MSC写入异常", + _ => "未知异常" + }; + } + + public static CopyThrottleSettings GetCopyThrottleSettings() + { + return new CopyThrottleSettings( + COPY_BUFFER_SIZE, + COPY_CHUNK_DELAY_MS, + COPY_FILE_DELAY_MS, + FILE_FLUSH_INTERVAL_BYTES); + } + + public static bool ShouldEmitRepeatedWarning(int occurrence) + { + return occurrence <= 1 || occurrence % 3 == 0; + } + + public static async Task WriteChunkWithTimeoutAsync( + FileStream destStream, + byte[] buffer, + int bytesToWrite, + CancellationToken cancellationToken, + string destinationPath, + int chunkIndex, + long copiedBytes) + { + var writeTask = destStream.WriteAsync(buffer, 0, bytesToWrite, cancellationToken); + var delayTask = Task.Delay(WriteTimeoutMs); + + var completedTask = await Task.WhenAny(writeTask, delayTask); + + if (completedTask == delayTask) + { + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var message = $"[{timestamp}] 设备写入超时,停止复制。文件:{destinationPath},chunk:{chunkIndex},已复制:{copiedBytes}字节"; + throw new IOException(message); + } + + // 确保写入任务完成,避免数据丢失 + await writeTask; + } + } + #endregion + + #region 压缩包操作 + public async Task CopyDeltaDirectoryToUdiskAsync( + string localRootDir, + IProgress progress, + CancellationToken ct) + { + // 内部打开U盘 + await OpenUDiskAndDetectDriveAsync(); + + string? targetDrive = await FindAssociatedUsbDisk(_vendorId, _productId); + if (string.IsNullOrEmpty(targetDrive)) + throw new InvalidOperationException("未检测到U盘设备"); + + // 保活机制(防止断开) + using var keepAliveCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + var keepAliveTask = KeepDeviceAliveAsync(keepAliveCts.Token); + + try + { + string destRoot = Path.Combine(targetDrive, usbdiskDir); + Directory.CreateDirectory(destRoot); + + // ============================== + // 差异对比 + // ============================== + var localFiles = ScanLocalFiles(localRootDir); + var diskFiles = await ScanUdiskFilesAsync(ct); + var changedFiles = GetChangedFiles(localFiles, diskFiles); + + if (changedFiles.Count == 0) + { + progress.Report(100); + return; + } + + // 计算总大小(精准进度) + long totalBytes = 0; + foreach (var rel in changedFiles) + { + string file = Path.Combine(localRootDir, rel); + if (File.Exists(file)) + totalBytes += new FileInfo(file).Length; + } + + // ============================== + // 只复制差异文件 → 直写U盘(超快) + // ============================== + long processed = 0; + int skippedFiles = 0; + foreach (var relPath in changedFiles) + { + ct.ThrowIfCancellationRequested(); + + string srcFile = Path.Combine(localRootDir, relPath); + string destFile = Path.Combine(destRoot, relPath); + + var (ok, copied) = await CopyDeltaFileWithRecoveryAsync(srcFile, destFile, ct); + if (!ok) skippedFiles++; + processed += copied; + progress.Report(totalBytes > 0 ? (double)processed / totalBytes * 100 : 100); + } + + progress.Report(100); + + if (skippedFiles > 0) + { + OnLog($"⚠️ 云数据同步完成,但 {skippedFiles} 个文件因损坏被跳过,建议对U盘执行 chkdsk 修复"); + } + } + finally + { + keepAliveCts.Cancel(); + try { await keepAliveTask; } catch { } + } + } + + /// + /// 复制单个差异文件;若目标文件损坏(删除/重命名/覆盖失败等)则清理后重试一次。 + /// 返回 (是否成功, 复制的字节数)。设备断联/写入超时等设备级异常会原样抛出,由上层中断整个同步。 + /// + private async Task<(bool ok, long copied)> CopyDeltaFileWithRecoveryAsync( + string srcFile, + string destFile, + CancellationToken ct) + { + try + { + long copied = await CopyDeltaFileCoreAsync(srcFile, destFile, ct); + return (true, copied); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + var category = UsbCopyGuard.ClassifyCopyException(ex); + if (category == UsbCopyGuard.CopyFailureCategory.DeviceDisconnected || + category == UsbCopyGuard.CopyFailureCategory.WriteTimeout) + { + throw; // 设备级问题:重试无意义,中断整个同步 + } + + _logger?.LogWarning("差异文件复制失败({Label}),尝试清理损坏目标后重试:{Dest},错误:{Msg}", + UsbCopyGuard.GetFailureLabel(category), destFile, ex.Message); + + if (UsbCopyGuard.TryClearCorruptedTarget(_logger, destFile, out var clearDetail)) + { + try + { + long copied = await CopyDeltaFileCoreAsync(srcFile, destFile, ct); + return (true, copied); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception retryEx) + { + var retryCategory = UsbCopyGuard.ClassifyCopyException(retryEx); + if (retryCategory == UsbCopyGuard.CopyFailureCategory.DeviceDisconnected || + retryCategory == UsbCopyGuard.CopyFailureCategory.WriteTimeout) + { + throw; + } + _logger?.LogWarning("清理后重试仍失败,跳过该文件:{Dest},错误:{Msg}", destFile, retryEx.Message); + return (false, 0); + } + } + + _logger?.LogError("损坏目标文件无法清理,跳过:{Dest},错误:{Detail}", destFile, clearDetail); + return (false, 0); + } + } + + /// + /// 差异文件直写U盘核心逻辑(不含容错)。 + /// + private async Task CopyDeltaFileCoreAsync(string srcFile, string destFile, CancellationToken ct) + { + string destDir = Path.GetDirectoryName(destFile)!; + Directory.CreateDirectory(destDir); + + using var fsRead = new FileStream(srcFile, FileMode.Open, FileAccess.Read, FileShare.Read, 32768, true); + using var fsWrite = new FileStream(destFile, FileMode.Create, FileAccess.Write, FileShare.None, 32768, true); + + byte[] buffer = new byte[32768]; + long copied = 0; + int read; + while ((read = await fsRead.ReadAsync(buffer, 0, buffer.Length, ct)) > 0) + { + await fsWrite.WriteAsync(buffer, 0, read, ct); + copied += read; + } + + return copied; + } + + // 扫描U盘中所有文件 (相对路径 + 文件信息) + public async Task> ScanUdiskFilesAsync(CancellationToken ct) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + string? drive = await FindAssociatedUsbDisk(_vendorId, _productId); + if (string.IsNullOrEmpty(drive)) return dict; + + string root = Path.Combine(drive, usbdiskDir); + if (!Directory.Exists(root)) return dict; + + await Task.Run(() => + { + // 逐目录容错递归:单个目录损坏时跳过该目录,不影响其余文件 + ScanUdiskDirectoryRecursive(root, root, dict, ct); + }, ct); + + return dict; + } + + // 逐目录容错递归扫描:某个目录损坏/无法枚举时跳过该目录,保证整体扫描不中断 + private void ScanUdiskDirectoryRecursive( + string dir, + string root, + Dictionary dict, + CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + string[] files; + try + { + files = Directory.GetFiles(dir); + } + catch (Exception ex) + { + _logger?.LogWarning("U盘目录枚举失败,跳过该目录:{Dir},错误:{Msg}", dir, ex.Message); + return; + } + + foreach (var file in files) + { + ct.ThrowIfCancellationRequested(); + try + { + var fi = new FileInfo(file); + string rel = GetRelativePath(root, file); + if (rel == ".DS_Store") continue; + dict[rel] = fi; + } + catch (Exception ex) + { + _logger?.LogWarning("U盘文件信息读取失败,跳过:{File},错误:{Msg}", file, ex.Message); + } + } + + string[] subDirs; + try + { + subDirs = Directory.GetDirectories(dir); + } + catch (Exception ex) + { + _logger?.LogWarning("U盘子目录枚举失败,跳过该目录:{Dir},错误:{Msg}", dir, ex.Message); + return; + } + + foreach (var subDir in subDirs) + { + ScanUdiskDirectoryRecursive(subDir, root, dict, ct); + } + } + + // 扫描本地文件 + public Dictionary ScanLocalFiles(string localRootDir) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (!Directory.Exists(localRootDir)) return dict; + + foreach (var file in Directory.EnumerateFiles(localRootDir, "*.*", SearchOption.AllDirectories)) + { + var fi = new FileInfo(file); + string rel = GetRelativePath(localRootDir, file); + if (rel == ".DS_Store") continue; + dict[rel] = fi; + } + return dict; + } + + // 对比 → 得到需要更新的文件列表 + public List GetChangedFiles( + Dictionary localFiles, + Dictionary udiskFiles) + { + var changed = new List(); + + foreach (var kv in localFiles) + { + string rel = kv.Key; + var local = kv.Value; + + if (!udiskFiles.TryGetValue(rel, out var disk)) + { + changed.Add(rel); // 新增 + continue; + } + + if (local.Length != disk.Length) + { + changed.Add(rel); // 大小不一样 + continue; + } + + //if (local.LastWriteTimeUtc > disk.LastWriteTimeUtc.AddSeconds(5)) + //{ + // changed.Add(rel); // 本地更新 + //} + } + + return changed; + } + + public async Task<(string zipPath, int count)> CreateDeltaZipAsync( + string localRootDir, + List changedRelativePaths, + CancellationToken ct) + { + string zipPath = Path.Combine(Path.GetTempPath(), $"delta_{Guid.NewGuid():N}.zip"); + + await Task.Run(() => + { + using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create); + foreach (var rel in changedRelativePaths) + { + ct.ThrowIfCancellationRequested(); + string src = Path.Combine(localRootDir, rel); + if (!File.Exists(src)) continue; + + zip.CreateEntryFromFile(src, rel, CompressionLevel.Fastest); + } + }, ct); + + return (zipPath, changedRelativePaths.Count); + } + #endregion + + /// + /// 从语言字典中获取值,兼容 .NET Framework 4.8(替代 Dictionary.GetValueOrDefault) + /// + private static string GetLanguageValueOrDefault(string key) + { + return AiKLanguage.LanguageKey.TryGetValue(key, out var value) ? value : null; + } + + /// + /// 计算相对路径,兼容 .NET Framework 4.8(替代 Path.GetRelativePath) + /// + private static string GetRelativePath(string relativeTo, string path) + { + if (!relativeTo.EndsWith(Path.DirectorySeparatorChar.ToString())) + relativeTo += Path.DirectorySeparatorChar; + + var relUri = new Uri(relativeTo).MakeRelativeUri(new Uri(path)); + var relPath = Uri.UnescapeDataString(relUri.ToString()); + + return relPath.Replace('/', Path.DirectorySeparatorChar); + } + } +} diff --git a/AIK.Service/Service/HttpClientService.cs b/AIK.Service/Service/HttpClientService.cs new file mode 100644 index 0000000..9d3301b --- /dev/null +++ b/AIK.Service/Service/HttpClientService.cs @@ -0,0 +1,169 @@ +using AIK.Models.HttpPolicy; +using AIK.Service.IService; +using Flurl; +using Flurl.Http; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class HttpClientService : IHttpClientService + { + private readonly ILogger _logger; + private readonly RetryPolicyConfig _config; + private readonly IFlurlClient _flurlClient; + private readonly BouncyCastleTlsHandler _tlsHandler; + + public HttpClientService(IHttpClientFactory httpClientFactory, ILogger logger, RetryPolicyConfig config = null) + { + _logger = logger; + _config = config ?? new RetryPolicyConfig(); + + // Win7 系统 SChannel 未启用 TLS 1.2 时,.NET Framework 的 HttpClient/HttpWebRequest + // (底层全部走 SChannel)无法协商 TLS 1.2。改用 BouncyCastle 纯托管 TLS 实现 + // (BouncyCastleTlsHandler),完全绕过 SChannel,在 Win7 上可直接与只接受 TLS 1.2+ + // 的服务器(key.aikkey.cn)握手。Handler 内部已处理 SNI、证书链校验与 gzip/deflate 解压。 + _tlsHandler = new BouncyCastleTlsHandler(); + _flurlClient = new FlurlClient(new HttpClient(_tlsHandler)); + } + + // GET 方法 + public async Task GetStringWithTimeoutAsync(string url, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var response = await ExecuteAsync(url, HttpMethod.Get, null, timeout, null, cancellationToken); + ThrowIfNotSuccess(response); + return await response.Content.ReadAsStringAsync(); + } + + // POST JSON 方法 + public async Task PostJsonWithTimeoutAsync(string url, string jsonData, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); + var response = await ExecuteAsync(url, HttpMethod.Post, content, timeout, null, cancellationToken); + ThrowIfNotSuccess(response); + return await response.Content.ReadAsStringAsync(); + } + + public async Task GetWithTimeoutAsync(string url, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + return await ExecuteAsync(url, HttpMethod.Get, null, timeout, null, cancellationToken); + } + + public async Task PostWithTimeoutAsync(string url, HttpContent content, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + return await ExecuteAsync(url, HttpMethod.Post, content, timeout, null, cancellationToken); + } + + public async Task GetByteArrayWithTimeoutAsync(string url, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var response = await ExecuteAsync(url, HttpMethod.Get, null, timeout, null, cancellationToken); + ThrowIfNotSuccess(response); + return await response.Content.ReadAsByteArrayAsync(); + } + + /// + /// 流式下载文件到指定路径。直接走 BouncyCastleTlsHandler 的流式通道(SendStreamingAsync), + /// 响应体逐段读取、每段带空闲超时(60s)、无总时长限制,避免大文件因 30s 总超时而失败。 + /// 支持断点续传:resumeOffset>0 时携带 Range 头,服务器返回 206 则从文件末尾追加写入; + /// 服务器忽略 Range 返回 200 时则整文件覆盖重写。 + /// + public async Task DownloadToFileWithProgressAsync(string url, string filePath, IProgress progress = null, CancellationToken cancellationToken = default, long resumeOffset = 0) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + if (resumeOffset > 0) + { + request.Headers.Range = new RangeHeaderValue(resumeOffset, null); + } + using var response = await _tlsHandler.SendStreamingAsync(request, cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"下载失败: HTTP {(int)response.StatusCode} ({response.StatusCode}) - {url}"); + } + + // 判断服务器是否接受 Range(206 = 部分内容,Content-Range 提供完整文件总大小) + var contentRange = response.Content.Headers.ContentRange; + bool resumed = resumeOffset > 0 && contentRange != null && contentRange.HasRange && contentRange.Length.HasValue; + + long baseOffset = resumed ? resumeOffset : 0; + FileMode mode = resumed ? FileMode.Append : FileMode.Create; + long totalBytes = resumed + ? contentRange.Length.Value + : (response.Content.Headers.ContentLength ?? -1) + baseOffset; + long receivedBytes = baseOffset; + + using (var source = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var target = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, 81920, useAsync: true)) + { + var buffer = new byte[81920]; + int read; + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) + { + await target.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false); + receivedBytes += read; + if (totalBytes > 0 && progress != null) + { + progress.Report(Math.Min(100.0, receivedBytes * 100.0 / totalBytes)); + } + } + await target.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + // 完整性校验:连接被中途掐断时 EOF 提前结束,实际字节数 < 总大小,必须视为失败(由调用方重试续传) + if (totalBytes > 0 && receivedBytes != totalBytes) + { + throw new HttpRequestException($"下载不完整: 预期 {totalBytes} 字节,实际 {receivedBytes} 字节 - {url}"); + } + + progress?.Report(100.0); + } + + #region 附带Token权限的方法 + public async Task GetStringWithTimeoutAndTokenAsync(string url, string token, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var response = await ExecuteAsync(url, HttpMethod.Get, null, timeout, token, cancellationToken); + ThrowIfNotSuccess(response); + return await response.Content.ReadAsStringAsync(); + } + + public async Task PostJsonWithTimeoutAndTokenAsync(string url, string jsonData, string token, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); + var response = await ExecuteAsync(url, HttpMethod.Post, content, timeout, token, cancellationToken); + ThrowIfNotSuccess(response); + return await response.Content.ReadAsStringAsync(); + } + #endregion + + // 每个请求独立设置超时,避免并发请求互相覆盖;Flurl 的 SendAsync 不自动抛非 2xx,由 ThrowIfNotSuccess 统一处理 + private async Task ExecuteAsync(string url, HttpMethod method, HttpContent content, TimeSpan? timeout, string token, CancellationToken cancellationToken) + { + var request = _flurlClient.Request(url) + .WithTimeout(timeout ?? _config.DefaultTimeout); + if (!string.IsNullOrEmpty(token)) + { + request = request.WithHeader("token", token); + } + return (await request.SendAsync(method, content, HttpCompletionOption.ResponseContentRead, cancellationToken)).ResponseMessage; + } + + private static void ThrowIfNotSuccess(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return; + } + + var message = $"Response status code does not indicate success: {(int)response.StatusCode} ({response.StatusCode})."; + throw new HttpRequestException(message); + } + } +} \ No newline at end of file diff --git a/AIK.Service/Service/LocalizationService.cs b/AIK.Service/Service/LocalizationService.cs new file mode 100644 index 0000000..5551528 --- /dev/null +++ b/AIK.Service/Service/LocalizationService.cs @@ -0,0 +1,145 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Models.SystemSettings; +using AIK.Service.IService; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Service.Service +{ + public class LocalizationService : ILocalizationService + { + private const string ASSEMBLY_NAME = "AIK.Assets"; + private const string RESOURCE_PATH = "Languages/Strings.{0}.xaml"; + private readonly Dictionary _loadedDictionaries = new(); + private CultureInfo _currentCulture; + + public CultureInfo CurrentCulture => _currentCulture; + public event EventHandler LanguageChanged; + public IEnumerable SupportedCultures => new[] + { + new CultureInfo("en"), + new CultureInfo("zh"), + new CultureInfo("es"), + new CultureInfo("pt"), + //new CultureInfo("ru") + }; + + public IEnumerable SupportedLanguages => new[] + { + new LanguageItems { Code = "zh", Name = "中文 (简体)", NativeName = "Chinese", FlagEmoji = "🇨🇳" }, + new LanguageItems { Code = "en", Name = "英语", NativeName = "English", FlagEmoji = "🇺🇸" }, + new LanguageItems { Code = "pt", Name = "葡萄牙语", NativeName = "Português", FlagEmoji = "🇧🇷" }, + new LanguageItems { Code = "es", Name = "西班牙语", NativeName = "Español", FlagEmoji = "🇪🇸" }, + new LanguageItems { Code = "pt-BR", Name = "葡萄牙语(巴西)", NativeName = "Português (Brasil)", FlagEmoji = "🇧🇷" }, + new LanguageItems { Code = "in", Name = "印度", NativeName = "Indian", FlagEmoji = "🇮🇳" }, + new LanguageItems { Code = "fr", Name = "法语", NativeName = "Français", FlagEmoji = "🇫🇷" }, + new LanguageItems { Code = "fa", Name = "波斯语", NativeName = "فارسی", FlagEmoji = "🇮🇷" }, + new LanguageItems { Code = "he", Name = "希伯来语", NativeName = "עברית", FlagEmoji = "🇮🇱" }, + new LanguageItems { Code = "hi", Name = "印地语", NativeName = "हिन्दी", FlagEmoji = "🇮🇳" }, + new LanguageItems { Code = "id", Name = "印尼语", NativeName = "Bahasa Indonesia", FlagEmoji = "🇮🇩" }, + new LanguageItems { Code = "it", Name = "意大利语", NativeName = "Italiano", FlagEmoji = "🇮🇹" }, + new LanguageItems { Code = "ko", Name = "朝鲜语", NativeName = "한국어", FlagEmoji = "🇰🇷" }, + new LanguageItems { Code = "pl", Name = "波兰语", NativeName = "Polski", FlagEmoji = "🇵🇱" }, + new LanguageItems { Code = "ru", Name = "俄语", NativeName = "Русский", FlagEmoji = "🇷🇺" }, + new LanguageItems { Code = "th", Name = "泰语", NativeName = "ไทย", FlagEmoji = "🇹🇭" }, + new LanguageItems { Code = "tr", Name = "土耳其语", NativeName = "Türkçe", FlagEmoji = "🇹🇷" }, + new LanguageItems { Code = "vi", Name = "越南语", NativeName = "Tiếng Việt", FlagEmoji = "🇻🇳" }, + new LanguageItems { Code = "de", Name = "德语", NativeName = "Deutsch", FlagEmoji = "🇩🇪" }, + new LanguageItems { Code = "uk", Name = "乌克兰语", NativeName = "Українська", FlagEmoji = "🇺🇦" }, + new LanguageItems { Code = "ms", Name = "马来语", NativeName = "Bahasa Melayu", FlagEmoji = "🇲🇾" } + }; + public async Task InitializeAsync(string language) + { + // 从设置加载或使用系统默认语言 + SetLanguage(language); + } + public void SetLanguage(string cultureCode) + { + try + { + var culture = new CultureInfo(cultureCode); + LoadResourceDictionaryFromAssembly(culture.TwoLetterISOLanguageName); + + // 更新线程区域性 + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + + // 触发语言更改事件 + LanguageChanged?.Invoke(culture, EventArgs.Empty); + } + catch (Exception ex) + { + Console.WriteLine($"切换语言失败: {ex.Message}"); + // 回退到中文 + LoadResourceDictionaryFromAssembly("zh"); + } + } + + + private void LoadResourceDictionaryFromAssembly(string languageCode) + { + try + { + // 移除现有的语言资源 + RemoveExistingLanguageResources(); + + // 从外部程序集加载资源字典 + var resourceUri = GetResourceUri(languageCode); + var resourceDict = new ResourceDictionary { Source = resourceUri }; + + Application.Current.Resources.MergedDictionaries.Add(resourceDict); + + Console.WriteLine($"✅ 成功加载语言资源: {languageCode}"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 加载语言资源失败: {ex.Message}"); + // 尝试回退到英语 + if (languageCode != "en") + { + LoadResourceDictionaryFromAssembly("en"); + } + } + } + + private Uri GetResourceUri(string languageCode) + { + // 格式: pack://application:,,,/程序集名称;component/资源路径 + return new Uri( + $"pack://application:,,,/{ASSEMBLY_NAME};component/{string.Format(RESOURCE_PATH, languageCode)}", + UriKind.Absolute + ); + } + + private void RemoveExistingLanguageResources() + { + var dictionariesToRemove = Application.Current.Resources.MergedDictionaries + .Where(d => d.Source?.OriginalString.Contains("Strings.") == true || + d.Source?.OriginalString.Contains("/Languages/") == true) + .ToList(); + + foreach (var dict in dictionariesToRemove) + { + Application.Current.Resources.MergedDictionaries.Remove(dict); + } + } + + public string GetString(string key) + { + return Application.Current.TryFindResource(key) as string ?? $"[{key}]"; + } + + public string GetString(string key, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Service/Service/LogService.cs b/AIK.Service/Service/LogService.cs new file mode 100644 index 0000000..108fd8c --- /dev/null +++ b/AIK.Service/Service/LogService.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class LogService : ILogService + { + private readonly ILogger _logger; + private readonly ConcurrentQueue _logBuffer; + private readonly int _maxBufferSize = 1000; + + public LogService(ILogger logger) + { + _logger = logger; + _logBuffer = new ConcurrentQueue(); + } + + public void LogDebug(string message, params object[] args) + { + _logger.LogDebug(message, args); + AddToBuffer("DEBUG", string.Format(message, args)); + } + + public void LogInformation(string message, params object[] args) + { + _logger.LogInformation(message, args); + AddToBuffer("INFO", string.Format(message, args)); + } + + public void LogWarning(string message, params object[] args) + { + _logger.LogWarning(message, args); + AddToBuffer("WARN", string.Format(message, args)); + } + + public void LogError(Exception? exception, string message, params object[] args) + { + _logger.LogError(exception, message, args); + AddToBuffer("ERROR", $"{string.Format(message, args)} - {exception}"); + } + + public void LogError(string message, params object[] args) + { + _logger.LogError(message, args); + AddToBuffer("ERROR", string.Format(message, args)); + } + + private void AddToBuffer(string level, string message) + { + var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}"; + + _logBuffer.Enqueue(logEntry); + + // 保持缓冲区大小 + while (_logBuffer.Count > _maxBufferSize && _logBuffer.TryDequeue(out _)) ; + } + + public IReadOnlyList GetRecentLogs(int maxCount = 100) + { + return _logBuffer.Take(maxCount).ToList().AsReadOnly(); + } + + public Task UploadLogsAsync() + { + // 上传逻辑在单独的LogUploadService中实现 + + //预留日志记录上传服务器 + return Task.FromResult(true); + } + + public void ClearBuffer() + { + while (_logBuffer.TryDequeue(out _)) ; + } + } +} diff --git a/AIK.Service/Service/MembershipService.cs b/AIK.Service/Service/MembershipService.cs new file mode 100644 index 0000000..a49558f --- /dev/null +++ b/AIK.Service/Service/MembershipService.cs @@ -0,0 +1,221 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Common.Interface; +using AIK.Common.SysCommon; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Membership; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; + +namespace AIK.Service.Service +{ + public class MembershipService : IMembershipService + { + private static readonly JsonSerializerSettings JsonSettings = new() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }; + + private readonly IHttpClientService _httpClientService; + private readonly IConfigurationService _configurationService; + private readonly ISettingsService _settingsService; + private readonly IRetryPolicyService _retryPolicyService; + private readonly ILogger _logger; + + public MembershipService( + IHttpClientService httpClientService, + IConfigurationService configurationService, + ISettingsService settingsService, + IRetryPolicyService retryPolicyService, + ILogger logger) + { + _httpClientService = httpClientService; + _configurationService = configurationService; + _settingsService = settingsService; + _retryPolicyService = retryPolicyService; + _logger = logger; + } + + public Task> GetModuleListAsync(string model = "key", CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + var url = BuildUrl(WebApiAddress.MembershipModuleList, new() + { + ["model"] = string.IsNullOrWhiteSpace(model) ? "key" : model, + ["language"] = GetLanguage() + }); + + var response = await _httpClientService.GetStringWithTimeoutAsync(url, TimeSpan.FromSeconds(15), cancellationToken); + var apiResponse = DeserializeResponse>(response, nameof(GetModuleListAsync)); + return IsSuccess(apiResponse) ? apiResponse.Data ?? new List() : new List(); + }, nameof(GetModuleListAsync), cancellationToken); + } + + public Task> FindOpenedFunctionsAsync(string token, MembershipFindRequest request, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + request.Language ??= GetLanguage(); + var url = BuildUrl(WebApiAddress.MembershipFind); + var json = JsonConvert.SerializeObject(request, JsonSettings); + var response = await _httpClientService.PostJsonWithTimeoutAndTokenAsync(url, json, token, TimeSpan.FromSeconds(15), cancellationToken); + return DeserializePageResponse(response, nameof(FindOpenedFunctionsAsync)); + }, nameof(FindOpenedFunctionsAsync), cancellationToken); + } + + public Task> OpenFunctionAsync(string token, OpenMembershipRequest request, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + request.Language ??= GetLanguage(); + return await PostWithTokenAsync(WebApiAddress.MembershipOpen, token, request, nameof(OpenFunctionAsync), cancellationToken); + }, nameof(OpenFunctionAsync), cancellationToken); + } + + public Task BindDeviceAsync(string token, BindMembershipDeviceRequest request, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + request.Language ??= GetLanguage(); + var response = await PostWithTokenAsync(WebApiAddress.MembershipBind, token, request, nameof(BindDeviceAsync), cancellationToken); + return ToUntypedResponse(response); + }, nameof(BindDeviceAsync), cancellationToken); + } + + public Task CheckPermissionAsync(string token, CheckMembershipPermissionRequest request, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + request.Language ??= GetLanguage(); + var response = await PostWithTokenAsync(WebApiAddress.MembershipCheck, token, request, nameof(CheckPermissionAsync), cancellationToken); + return IsSuccess(response) && response.Data; + }, nameof(CheckPermissionAsync), cancellationToken); + } + + public Task ActivateDeviceAsync(string token, ActivateDeviceRequest request, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + request.Language ??= GetLanguage(); + var response = await PostWithTokenAsync(WebApiAddress.MembershipActivate, token, request, nameof(ActivateDeviceAsync), cancellationToken); + return ToUntypedResponse(response); + }, nameof(ActivateDeviceAsync), cancellationToken); + } + + public Task GetBurnCountInfoAsync(string token, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + var url = BuildUrl(WebApiAddress.MembershipFindCount); + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync(url, token, TimeSpan.FromSeconds(15), cancellationToken); + var apiResponse = DeserializeResponse(response, nameof(GetBurnCountInfoAsync)); + return IsSuccess(apiResponse) ? apiResponse.Data : null; + }, nameof(GetBurnCountInfoAsync), cancellationToken); + } + + public Task AddBurnCountAsync(string token, string type = "ysjs", CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + var url = BuildUrl(WebApiAddress.MembershipAddCount, new() + { + ["type"] = string.IsNullOrWhiteSpace(type) ? "ysjs" : type + }); + + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync(url, token, TimeSpan.FromSeconds(15), cancellationToken); + return ToUntypedResponse(DeserializeResponse(response, nameof(AddBurnCountAsync))); + }, nameof(AddBurnCountAsync), cancellationToken); + } + + private async Task> PostWithTokenAsync(string path, string token, object request, string operationName, CancellationToken cancellationToken) + { + var url = BuildUrl(path); + var json = JsonConvert.SerializeObject(request, JsonSettings); + var response = await _httpClientService.PostJsonWithTimeoutAndTokenAsync(url, json, token, TimeSpan.FromSeconds(15), cancellationToken); + return DeserializeResponse(response, operationName); + } + + private string BuildUrl(string path, Dictionary? query = null) + { + var url = $"{_configurationService.ApiSettings.BaseUrl}{path}"; + if (query == null || query.Count == 0) + { + return url; + } + + var queryString = string.Join("&", query + .Where(item => !string.IsNullOrWhiteSpace(item.Value)) + .Select(item => $"{WebUtility.UrlEncode(item.Key)}={WebUtility.UrlEncode(item.Value)}")); + return string.IsNullOrEmpty(queryString) ? url : $"{url}?{queryString}"; + } + + private string GetLanguage() + { + return string.IsNullOrWhiteSpace(_settingsService.Load().Language) ? "zh" : _settingsService.Load().Language; + } + + private ApiResponse DeserializeResponse(string response, string operationName) + { + var apiResponse = JsonConvert.DeserializeObject>(response) ?? new ApiResponse(); + if (!IsSuccess(apiResponse)) + { + _logger.LogWarning("{OperationName} failed. Code: {Code}, Message: {Message}, Response: {Response}", operationName, apiResponse.Code, apiResponse.Msg, response); + } + + return apiResponse; + } + + private ApiPageResult DeserializePageResponse(string response, string operationName) + { + var pageResponse = JsonConvert.DeserializeObject>(response) ?? new ApiPageResponse(); + if (pageResponse.Code != 1) + { + _logger.LogWarning("{OperationName} failed. Code: {Code}, Message: {Message}, Response: {Response}", operationName, pageResponse.Code, pageResponse.Msg, response); + return new ApiPageResult(); + } + + return new ApiPageResult + { + Records = pageResponse.Records ?? new List(), + Total = pageResponse.Total, + Current = pageResponse.Current, + Size = pageResponse.Size, + Pages = pageResponse.Pages + }; + } + + private static bool IsSuccess(ApiResponse response) + { + return response.Code == 1; + } + + private static ApiResponse ToUntypedResponse(ApiResponse response) + { + return new ApiResponse + { + Code = response.Code, + Msg = response.Msg, + I18nMsg = response.I18nMsg, + Data = response.Data, + Dict = response.Dict + }; + } + + private sealed class ApiPageResponse : ApiPageResult + { + public int Code { get; set; } + public string? Msg { get; set; } + public string? I18nMsg { get; set; } + public Dictionary? Dict { get; set; } + } + } +} diff --git a/AIK.Service/Service/PaymentOrderService.cs b/AIK.Service/Service/PaymentOrderService.cs new file mode 100644 index 0000000..5f7630d --- /dev/null +++ b/AIK.Service/Service/PaymentOrderService.cs @@ -0,0 +1,121 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Common.Interface; +using AIK.Common.SysCommon; +using AIK.Models; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Payment; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; + +namespace AIK.Service.Service +{ + public class PaymentOrderService : IPaymentOrderService + { + private readonly IHttpClientService _httpClientService; + private readonly IConfigurationService _configurationService; + private readonly ISettingsService _settingsService; + private readonly IRetryPolicyService _retryPolicyService; + private readonly ILogger _logger; + + public PaymentOrderService( + IHttpClientService httpClientService, + IConfigurationService configurationService, + ISettingsService settingsService, + IRetryPolicyService retryPolicyService, + ILogger logger) + { + _httpClientService = httpClientService; + _configurationService = configurationService; + _settingsService = settingsService; + _retryPolicyService = retryPolicyService; + _logger = logger; + } + + public Task CreateIntegralOrderAsync(string token, CreateIntegralOrderRequest request, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + var url = BuildUrl(WebApiAddress.PayIntegralOrder, new() + { + ["amount"] =request.Amount.ToString(CultureInfo.InvariantCulture), + ["payType"] = request.PayType.ToString(CultureInfo.InvariantCulture), + ["channel"] = request.Channel, + ["qrcode"] = 1.ToString() + }); + + var response = await _httpClientService.GetStringWithTimeoutAndTokenAsync(url, token, TimeSpan.FromSeconds(15), cancellationToken); + var apiResponse = DeserializeResponse(response, nameof(CreateIntegralOrderAsync)); + return IsSuccess(apiResponse) ? apiResponse.Data : null; + }, nameof(CreateIntegralOrderAsync), cancellationToken); + } + + public Task GetOrderPayStatusAsync(long orderId, int type = 2, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + var url = BuildUrl($"{WebApiAddress.OrderPayStatus}/{orderId}", new() + { + ["type"] = type.ToString(CultureInfo.InvariantCulture) + }); + + var response = await _httpClientService.GetStringWithTimeoutAsync(url, TimeSpan.FromSeconds(15), cancellationToken); + var apiResponse = DeserializeResponse(response, nameof(GetOrderPayStatusAsync)); + return IsSuccess(apiResponse) ? apiResponse.Data : null; + }, nameof(GetOrderPayStatusAsync), cancellationToken); + } + + public Task RefreshPaypalOrderAsync(long orderId, int type = 2, CancellationToken cancellationToken = default) + { + return _retryPolicyService.ExecuteWithRetryAsync(async () => + { + var url = BuildUrl($"{WebApiAddress.OrderPaypal}/{orderId}", new() + { + ["type"] = type.ToString(CultureInfo.InvariantCulture) + }); + + var response = await _httpClientService.GetStringWithTimeoutAsync(url, TimeSpan.FromSeconds(15), cancellationToken); + var apiResponse = DeserializeResponse(response, nameof(RefreshPaypalOrderAsync)); + return IsSuccess(apiResponse) ? apiResponse.Data : null; + }, nameof(RefreshPaypalOrderAsync), cancellationToken); + } + + private string BuildUrl(string path, Dictionary? query = null) + { + var url = $"{_configurationService.ApiSettings.BaseUrl}{path}"; + if (query == null || query.Count == 0) + { + return url; + } + + var queryString = string.Join("&", query + .Where(item => !string.IsNullOrWhiteSpace(item.Value)) + .Select(item => $"{WebUtility.UrlEncode(item.Key)}={WebUtility.UrlEncode(item.Value)}")); + return string.IsNullOrEmpty(queryString) ? url : $"{url}?{queryString}"; + } + + private ApiResponse DeserializeResponse(string response, string operationName) + { + var apiResponse = JsonConvert.DeserializeObject>(response) ?? new ApiResponse(); + if (!IsSuccess(apiResponse)) + { + _logger.LogWarning("{OperationName} failed. Code: {Code}, Message: {Message}, Response: {Response}", operationName, apiResponse.Code, apiResponse.Msg, response); + } + + return apiResponse; + } + + private static bool IsSuccess(ApiResponse response) + { + return response.Code == 1; + } + + } +} diff --git a/AIK.Service/Service/RemoteImageCacheService.cs b/AIK.Service/Service/RemoteImageCacheService.cs new file mode 100644 index 0000000..e8e0557 --- /dev/null +++ b/AIK.Service/Service/RemoteImageCacheService.cs @@ -0,0 +1,569 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Common; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Imaging; + +namespace AIK.Service.Service +{ + public class RemoteImageCacheService : IImageCacheService + { + private readonly ConcurrentDictionary _memoryCache; + private readonly ConcurrentDictionary _memoryPathCache; + // 缓存正在下载或已完成的任务(URL/Key -> Task) + private readonly ConcurrentDictionary> _pendingDownloads = new(); + private readonly string _cacheBaseDirectory; + private readonly TimeSpan _cacheDuration = TimeSpan.FromDays(7); // 缓存时效7天 + private readonly IHttpClientService _httpClient; + private readonly IConfigurationService _configurationService; + private readonly ILogger _logger; + private string GetCacheFilePath(string cacheKey) => Path.Combine(_cacheBaseDirectory, $"{cacheKey}.jpg"); + public RemoteImageCacheService(IConfigurationService configurationService, IHttpClientService httpClient, ILogger logger) + { + _configurationService = configurationService; + _memoryCache = new ConcurrentDictionary(); + _memoryPathCache = new ConcurrentDictionary(); + _httpClient = httpClient; + _logger = logger; + // 设置缓存目录,例如在用户本地应用数据文件夹下 + _cacheBaseDirectory = CommonString.ImageCacheDirectory; + Directory.CreateDirectory(_cacheBaseDirectory); // 确保目录存在 + _cacheDuration = _configurationService.CacheSettings.ImageCacheExpiry; + } + + #region 基础方法 + public async Task GetImageAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(imageUrl)) + return GetPlaceholderImage(); + + // 生成缓存键,如果未提供则使用URL的MD5值 + var key = cacheKey ?? GenerateCacheKey(imageUrl); + + // 1. 检查内存缓存 + if (_memoryCache.TryGetValue(key, out var cachedImage)) + { + //_logger.LogDebug("内存缓存命中: {Key}", key); + return cachedImage; + } + + // 2. 检查磁盘缓存 + string filePath = GetCacheFilePath(key); + if (File.Exists(filePath) && !IsCacheExpired(filePath)) + { + //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); + // 从磁盘加载并放入内存缓存 + var bitmapImage = await LoadImageFromDiskAsync(filePath); + if (bitmapImage != null) + { + _memoryCache[key] = bitmapImage; + } + return bitmapImage; + } + + // 3. 从网络下载 + try + { + var fullImgUrl = _configurationService.ApiSettings.FileUrl + imageUrl; + _logger.LogDebug("开始下载图片: {Url}", fullImgUrl); + + // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) + byte[] imageBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); + + if (imageBytes == null || imageBytes.Length == 0) + { + _logger.LogWarning("图片数据为空: {Url}", fullImgUrl); + return GetPlaceholderImage(); + } + + // 验证图片数据有效性 + if (!IsValidImageData(imageBytes)) + { + _logger.LogWarning("图片数据无效: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); + return GetPlaceholderImage(); + } + + // 保存到磁盘缓存 + await SaveImageToDiskAsync(imageBytes, filePath); + + // 从字节数组直接创建BitmapImage + var newImage = await CreateBitmapImageFromBytesAsync(imageBytes); + if (newImage != null) + { + _memoryCache[key] = newImage; + _logger.LogDebug("图片下载缓存成功: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); + return newImage; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "下载图片失败: {ImageUrl}", imageUrl); + } + + return GetPlaceholderImage(); + } + /// + /// 若本地图片存在,直接加载图片(去掉读取文件流操作)优化速率大概是280ms->10ms + /// + /// + /// + /// + /// + public async Task GetImageByLocalPathAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(imageUrl)) + return GetPlaceholderImage(); + + // 生成缓存键,如果未提供则使用URL的MD5值 + var key = cacheKey ?? GenerateCacheKey(imageUrl); + + // 1. 检查内存缓存 + if (_memoryCache.TryGetValue(key, out var cachedImage)) + { + //_logger.LogDebug("内存缓存命中: {Key}", key); + return cachedImage; + } + // 2. 检查磁盘缓存 + string filePath = GetCacheFilePath(key); + if (File.Exists(filePath) && !IsCacheExpired(filePath)) + { + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.UriSource = new Uri(filePath); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + if (bitmapImage != null) + { + _memoryCache[key] = bitmapImage; + } + return bitmapImage; + } + // 3. 从网络下载 + try + { + var fullImgUrl = _configurationService.ApiSettings.FileUrl + imageUrl; + _logger.LogDebug("开始下载图片: {Url}", fullImgUrl); + + // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) + byte[] imageBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); + + if (imageBytes == null || imageBytes.Length == 0) + { + _logger.LogWarning("图片数据为空: {Url}", fullImgUrl); + return GetPlaceholderImage(); + } + + // 验证图片数据有效性 + if (!IsValidImageData(imageBytes)) + { + _logger.LogWarning("图片数据无效: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); + return GetPlaceholderImage(); + } + + // 保存到磁盘缓存 + await SaveImageToDiskAsync(imageBytes, filePath); + + // 从字节数组直接创建BitmapImage + var newImage = await CreateBitmapImageFromBytesAsync(imageBytes); + if (newImage != null) + { + _memoryCache[key] = newImage; + _logger.LogDebug("图片下载缓存成功: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); + return newImage; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "下载图片失败: {ImageUrl}", imageUrl); + } + + return GetPlaceholderImage(); + + } + public async Task GetImagePathAsync(string imageUrl, string cacheKey = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(imageUrl)) + return string.Empty; + + // 生成缓存键,如果未提供则使用URL的MD5值 + var key = cacheKey ?? GenerateCacheKey(imageUrl); + + // 1. 检查内存缓存 + if (_memoryPathCache.TryGetValue(key, out var cachedImage)) + { + //_logger.LogDebug("内存缓存命中: {Key}", key); + return cachedImage; + } + + // 2. 检查磁盘缓存 + string filePath = GetCacheFilePath(key); + if (File.Exists(filePath) && !IsCacheExpired(filePath)) + { + //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); + // 从磁盘加载并放入内存缓存 + + _memoryPathCache[key] = filePath; + return filePath; + } + var downloadTask = _pendingDownloads.GetOrAdd(key, async k => + { + // 3. 从网络下载 + try + { + var fullImgUrl = _configurationService.ApiSettings.FileUrl + imageUrl; + _logger.LogDebug("开始下载图片: {Url}", fullImgUrl); + + // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) + byte[] imageBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl, null, cancellationToken); + + if (imageBytes == null || imageBytes.Length == 0) + { + _logger.LogWarning("图片数据为空: {Url}", fullImgUrl); + return string.Empty; + } + + // 验证图片数据有效性 + if (!IsValidImageData(imageBytes)) + { + _logger.LogWarning("图片数据无效: {Url}, 大小: {Size}字节", fullImgUrl, imageBytes.Length); + return string.Empty; + } + // 保存到磁盘缓存 + await SaveImageToDiskAsyncV2(imageBytes, filePath, fullImgUrl,cancellationToken); + _memoryPathCache[key] = filePath; + return filePath; + } + catch (Exception ex) + { + _logger.LogError(ex, "下载图片失败: {ImageUrl}", imageUrl); + return string.Empty; + } + finally + { + // 清理已完成的任务(可选,防止长期驻留) + _pendingDownloads.TryRemove(k, out _); + } + }); + return await downloadTask; + } + #region 辅助方法 + + /// + /// 从字节数组创建BitmapImage + /// + private async Task CreateBitmapImageFromBytesAsync(byte[] imageBytes) + { + return await Task.Run(() => + { + try + { + + var bitmapImage = new BitmapImage(); + var memoryStream = new MemoryStream(imageBytes); + bitmapImage.BeginInit(); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + //bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreImageCache; + bitmapImage.DecodePixelWidth = 1000; + bitmapImage.StreamSource = memoryStream; + bitmapImage.EndInit(); + memoryStream.Close(); // 或 Dispose() + // 必须冻结以实现线程安全 + if (bitmapImage.CanFreeze) + { + bitmapImage.Freeze(); + } + return bitmapImage; + } + catch (Exception ex) + { + _logger.LogError("执行方法:{Method}加载图片失败:{Message}", nameof(LoadImageViaMemoryStreamAsync), ex.Message); + return GetPlaceholderImage(); + //throw new InvalidOperationException($"通过内存流加载图片失败: {filePath}", ex); + } + }); + } + + /// + /// 保存字节数组到磁盘 + /// + private async Task SaveImageToDiskAsync(byte[] imageBytes, string filePath) + { + try + { + // 使用临时文件避免写入过程中出错 + var tempFilePath = filePath + ".tmp"; + + File.WriteAllBytes(tempFilePath, imageBytes); + + // 原子性操作:重命名临时文件 + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.Move(tempFilePath, filePath); + + _logger.LogDebug("图片保存成功: {FilePath}, 大小: {Size}字节", filePath, imageBytes.Length); + } + catch (Exception ex) + { + _logger.LogError(ex, "保存图片到磁盘失败: {FilePath}", filePath); + + // 清理临时文件 + var tempFilePath = filePath + ".tmp"; + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + + private async Task SaveImageToDiskAsyncV2(byte[] imageBytes, string targetPath, string fullpath, CancellationToken ct=default) + { + // 确保目录存在 + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); + + // 使用临时文件 + 原子移动,避免读写冲突 + string tempFile = Path.Combine(Path.GetDirectoryName(targetPath)!, Path.GetRandomFileName() + ".tmp"); + + try + { + File.WriteAllBytes(tempFile, imageBytes); + // 先删除目标文件(如果存在),再移动(实现覆盖效果) + if (File.Exists(targetPath)) + { + File.Delete(targetPath); + } + File.Move(tempFile, targetPath); // Windows 上同目录 Move 是原子的 + _logger.LogDebug("图片保存成功: {Url} -> {Path}", fullpath, targetPath); + } + catch (Exception ex) + { + _logger.LogError(ex, "保存图片到磁盘失败: {Path}=>{Url}", targetPath, fullpath); + throw; // 让上层捕获 + } + finally + { + // 清理残留临时文件 + if (File.Exists(tempFile)) + File.Delete(tempFile); + } + } + + /// + /// 验证图片数据有效性 + /// + private bool IsValidImageData(byte[] imageData) + { + if (imageData == null || imageData.Length < 8) + return false; + + // 检查常见的图片文件头 + // JPEG: FF D8 FF + if (imageData[0] == 0xFF && imageData[1] == 0xD8 && imageData[2] == 0xFF) + return true; + + // PNG: 89 50 4E 47 + if (imageData[0] == 0x89 && imageData[1] == 0x50 && + imageData[2] == 0x4E && imageData[3] == 0x47) + return true; + + // GIF: 47 49 46 + if (imageData[0] == 0x47 && imageData[1] == 0x49 && imageData[2] == 0x46) + return true; + + // BMP: 42 4D + if (imageData[0] == 0x42 && imageData[1] == 0x4D) + return true; + + // WebP: RIFF + if (imageData[0] == 0x52 && imageData[1] == 0x49 && + imageData[2] == 0x46 && imageData[3] == 0x46) + return true; + + return false; + } + + /// + /// 优化的磁盘加载方法 + /// + private async Task LoadImageFromDiskAsync(string filePath) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + { + return GetPlaceholderImage(); + } + + try + { + // 直接读取文件字节数组 + byte[] imageBytes = File.ReadAllBytes(filePath); + + if (imageBytes == null || imageBytes.Length == 0) + { + _logger.LogWarning("磁盘缓存文件为空: {FilePath}", filePath); + // 删除损坏的文件 + File.Delete(filePath); + return GetPlaceholderImage(); + } + + // 验证数据有效性 + if (!IsValidImageData(imageBytes)) + { + _logger.LogWarning("磁盘缓存文件数据无效: {FilePath}", filePath); + File.Delete(filePath); + return GetPlaceholderImage(); + } + + return await CreateBitmapImageFromBytesAsync(imageBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "从磁盘加载图片失败: {FilePath}", filePath); + return GetPlaceholderImage(); + } + } + + /// + /// 优化的内存流加载方法 + /// + private async Task LoadImageViaMemoryStreamAsync(string filePath) + { + return await Task.Run(() => + { + try + { + // 直接读取文件字节数组 + byte[] imageData = File.ReadAllBytes(filePath); + + if (imageData == null || imageData.Length == 0) + { + _logger.LogWarning("文件为空: {FilePath}", filePath); + return GetPlaceholderImage(); + } + + using var memoryStream = new MemoryStream(imageData); + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreImageCache; + bitmapImage.StreamSource = memoryStream; + bitmapImage.EndInit(); + + if (bitmapImage.Width > 0 && bitmapImage.Height > 0) + { + bitmapImage.Freeze(); + return bitmapImage; + } + + return GetPlaceholderImage(); + } + catch (Exception ex) + { + _logger.LogError(ex, "通过内存流加载图片失败: {FilePath}", filePath); + return GetPlaceholderImage(); + } + }); + } + + #endregion + + #region 清理和工具方法 + + public async Task ClearExpiredCacheAsync() + { + await Task.Run(() => + { + try + { + var directory = new DirectoryInfo(_cacheBaseDirectory); + if (!directory.Exists) return; + + int deletedCount = 0; + foreach (var file in directory.GetFiles("*.jpg")) + { + if (IsCacheExpired(file.FullName)) + { + try + { + file.Delete(); + deletedCount++; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "删除缓存文件失败: {FilePath}", file.FullName); + } + } + } + + _logger.LogInformation("清理过期缓存完成,删除文件数: {DeletedCount}", deletedCount); + } + catch (Exception ex) + { + _logger.LogError(ex, "清理过期缓存失败"); + } + }); + } + + public async Task ClearAllCacheAsync() + { + await Task.Run(() => + { + try + { + // 清空内存缓存 + _memoryCache.Clear(); + _memoryPathCache.Clear(); + + var directory = new DirectoryInfo(_cacheBaseDirectory); + if (directory.Exists) + { + directory.Delete(true); + Directory.CreateDirectory(_cacheBaseDirectory); + _logger.LogInformation("清空所有缓存完成"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "清空所有缓存失败"); + } + }); + } + + private bool IsCacheExpired(string filePath) + { + var fileInfo = new FileInfo(filePath); + return !fileInfo.Exists || (DateTime.Now - fileInfo.LastWriteTime) > _cacheDuration; + } + private string GenerateCacheKey(string url) + { + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(url); + byte[] hashBytes = md5.ComputeHash(inputBytes); + return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").Substring(0, 10); + } + } + #endregion + #endregion + private BitmapImage GetPlaceholderImage() + { + // 返回一个内置的占位图,或者根据需求返回null + return new BitmapImage(); + } + + + } +} diff --git a/AIK.Service/Service/RetryPolicyService.cs b/AIK.Service/Service/RetryPolicyService.cs new file mode 100644 index 0000000..ca218bd --- /dev/null +++ b/AIK.Service/Service/RetryPolicyService.cs @@ -0,0 +1,89 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Models.HttpPolicy; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + public class RetryPolicyService : IRetryPolicyService + { + private readonly ILogger _logger; + private readonly RetryPolicyConfig _config; + + public RetryPolicyService(ILogger logger, RetryPolicyConfig config = null) + { + _logger = logger; + _config = config ?? new RetryPolicyConfig(); + } + + public async Task ExecuteWithRetryAsync(Func> operation, string operationName, CancellationToken cancellationToken = default) + { + var exceptions = new List(); + + for (int retryCount = 0; retryCount < _config.MaxRetryCount; retryCount++) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + return await operation(); + } + catch (Exception ex) when (IsTransientException(ex) && retryCount < _config.MaxRetryCount - 1) + { + exceptions.Add(ex); + _logger.LogWarning(ex, + "操作 {OperationName} 第 {RetryCount} 次失败,{RetryDelay}ms 后重试", + operationName, retryCount + 1, _config.RetryDelay.TotalMilliseconds); + + await Task.Delay(_config.RetryDelay, cancellationToken); + } + catch (Exception ex) when (IsTransientException(ex)) + { + exceptions.Add(ex); + _logger.LogError(ex, + "操作 {OperationName} 最终失败,已重试 {MaxRetryCount} 次", + operationName, _config.MaxRetryCount); + + throw new AggregateException($"操作 {operationName} 失败,已重试 {_config.MaxRetryCount} 次", exceptions); + } + } + + throw new InvalidOperationException("不应该执行到这里,未知的情况"); + } + + public async Task ExecuteWithRetryAsync(Func operation, string operationName, CancellationToken cancellationToken = default) + { + await ExecuteWithRetryAsync(async () => + { + await operation(); + return null; + }, operationName, cancellationToken); + } + + private bool IsTransientException(Exception ex) + { + return ex is TimeoutException || + ex is TaskCanceledException || + ex is HttpRequestException || + (ex is OperationCanceledException && !(ex is TaskCanceledException)) || + IsNetworkRelatedException(ex); + } + + private bool IsNetworkRelatedException(Exception ex) + { + var message = ex.Message.ToLower(); + return message.Contains("connection") || + message.Contains("network") || + message.Contains("timeout") || + message.Contains("socket") || + message.Contains("dns"); + } + } +} diff --git a/AIK.Service/Service/SqliteCacheService.cs b/AIK.Service/Service/SqliteCacheService.cs new file mode 100644 index 0000000..693331c --- /dev/null +++ b/AIK.Service/Service/SqliteCacheService.cs @@ -0,0 +1,1017 @@ +using AIK.DataAccess; +using AIK.Models; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Brand; +using AIK.Models.SQLiteModels; +using AIK.Service.IService; +using Dapper; +using HidSharp; +using System.Data.SQLite; +using Microsoft.VisualBasic; +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace AIK.Service.Service +{ + public class SqliteCacheService : IDataCacheService, IDisposable + { + + //private readonly SQLiteConnection _connection;//弃用(原单例模式) + private readonly string _databasePath; + private bool _disposed = false; + + public SqliteCacheService() + { + //_connection = SQLiteCore.CreateConnection();//弃用(原单例模式) + _databasePath = SQLiteCore.DatabasePath; + InitializeDatabase(); + } + + private void InitializeDatabase() + { + using var pooledConn = SQLiteCore.GetConnection(); + try + { + var conn = pooledConn.InnerConnection; // 借用连接 + #region 创建表 + // 创建缓存表 + var createTableSql = @" + CREATE TABLE IF NOT EXISTS CacheItems ( + CacheKey TEXT PRIMARY KEY, + Data TEXT NOT NULL, + DataType TEXT NOT NULL, + CreatedAt DATETIME NOT NULL, + ExpiresAt DATETIME NOT NULL, + LastAccessed DATETIME NOT NULL, + AccessCount INTEGER DEFAULT 0, + Metadata TEXT + ); + + CREATE INDEX IF NOT EXISTS IX_CacheItems_ExpiresAt ON CacheItems(ExpiresAt); + CREATE INDEX IF NOT EXISTS IX_CacheItems_LastAccessed ON CacheItems(LastAccessed); + CREATE INDEX IF NOT EXISTS IX_CacheItems_DataType ON CacheItems(DataType); + "; + + // 3. 创建拆分缓存父表 SplitCache_Parent(无 COMMENT) + var createSplitParentTableSql = @" + CREATE TABLE IF NOT EXISTS SplitCache_Parent ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + CacheKey TEXT NOT NULL, + ParentData TEXT NOT NULL, -- 父级核心数据(无列表) + SubItemCount INTEGER DEFAULT 0, -- 子级数据条数 + DataType TEXT NOT NULL, -- 父级数据类型 + CreatedAt DATETIME NOT NULL, + ExpiresAt DATETIME NOT NULL, + LastAccessed DATETIME NOT NULL, + AccessCount INTEGER DEFAULT 0, + Metadata TEXT + );"; + + // 4. 为 SplitCache_Parent 表创建独立索引 + var createSplitParentIndexSql = @" + CREATE INDEX IF NOT EXISTS IX_SplitCacheParent_ExpiresAt ON SplitCache_Parent(ExpiresAt); + CREATE INDEX IF NOT EXISTS IX_SplitCacheParent_LastAccessed ON SplitCache_Parent(LastAccessed);"; + + // 5. 创建拆分缓存子表 SplitCache_ModelList(无 COMMENT,字段含义用代码注释说明) + var createSplitModelListTableSql = @" + CREATE TABLE IF NOT EXISTS SplitCache_ModelList ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ParentTableId INTEGER NOT NULL, + CacheKey TEXT NOT NULL, -- 关联 SplitCache_Parent.CacheKey + ParentID INTEGER NOT NULL, -- 关联 VehModelClass.ID + + KeyId INTEGER NOT NULL, -- 对应 VehKeyClass.id + KeyKeyId TEXT NOT NULL, -- 对应 VehKeyClass.keyId + Name TEXT NOT NULL, -- 对应 VehKeyClass.Name + ModelName TEXT NOT NULL, -- 对应 VehKeyClass.modelName + ModelEngName TEXT NOT NULL, -- 对应 VehKeyClass.modelEngName + ModelPuName TEXT NOT NULL, -- 对应 VehKeyClass.modelPuName + ModelXiName TEXT NOT NULL, -- 对应 VehKeyClass.modelXiName + ModelInName TEXT NOT NULL, -- 对应 VehKeyClass.modelInName + Rate TEXT NOT NULL, -- 对应 VehKeyClass.rate(频率) + Usable TEXT NOT NULL, -- 对应 VehKeyClass.usable(子机中文描述) + UsableEn TEXT NOT NULL, -- 对应 VehKeyClass.usableEn(子机英文描述) + UsablePu TEXT NOT NULL, -- 对应 VehKeyClass.usablePu + UsableXi TEXT NOT NULL, -- 对应 VehKeyClass.usableXi + Information TEXT, -- 对应 VehKeyClass.information(参考信息) + PicUrl TEXT NOT NULL, -- 对应 VehKeyClass.picUrl(图片路径) + FirmwareNo TEXT NOT NULL, -- 对应 VehKeyClass.firmwareNo(Bin文件名) + SourceUrl TEXT NOT NULL, -- 对应 VehKeyClass.sourceUrl(Bin文件路径) + SpCode TEXT NOT NULL, -- 对应 VehKeyClass.spCode(烧录提示) + Support TEXT NOT NULL, -- 对应 VehKeyClass.support(支持设备类型) + ChipType TEXT, -- 对应 VehKeyClass.chipType(芯片类型) + CreateTime TEXT NOT NULL, -- 对应 VehKeyClass.createTime(创建时间) + + KeyMatchContent TEXT, -- 对应 keyMatch.content(匹配方法中文) + KeyMatchContentEn TEXT, -- 对应 keyMatch.contentEn(匹配方法英文) + KeyMatchContentPu TEXT, -- 对应 keyMatch.contentPu + KeyMatchContentXi TEXT, -- 对应 keyMatch.contentXi + KeyMatchContentIn TEXT, -- 对应 keyMatch.contentIn + + IsSelected BOOLEAN NOT NULL DEFAULT 0, -- 对应 VehKeyClass.IsSelected + LocalImagePath TEXT, -- 对应 VehKeyClass.LocalImagePath(本地图片路径) + + -- 外键约束:删除父表数据时,自动删除子表关联数据 + FOREIGN KEY (ParentTableId) REFERENCES SplitCache_Parent(Id) ON DELETE CASCADE + );"; + + var createKZInfoListTableSql = @"CREATE TABLE IF NOT EXISTS SplitCache_KzInfoList ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + ModelListId INTEGER NOT NULL, -- 关键:指向 SplitCache_ModelList.Id + + binAddress TEXT, + binAddressArray TEXT, -- JSON: [""0x1000"", ...] + chipType TEXT, + cloudRemote BOOLEAN NOT NULL DEFAULT 0, + dzxp BOOLEAN NOT NULL DEFAULT 0, + firmwareNo TEXT, + fixedByte BOOLEAN NOT NULL DEFAULT 0, + fixedByteArray TEXT, -- JSON + fixedByteArrays TEXT, + sourceUrl TEXT, + sp BOOLEAN NOT NULL DEFAULT 0, + spCode TEXT, + tsRand BOOLEAN NOT NULL DEFAULT 0, + tsRandStr TEXT, + type TEXT, + updateByte BOOLEAN NOT NULL DEFAULT 0, + usable TEXT, + ysSource TEXT, + ysSourceNo TEXT, + zsId TEXT, + CreateTime TEXT NOT NULL DEFAULT (datetime('now')), + + FOREIGN KEY (ModelListId) REFERENCES SplitCache_ModelList(Id) ON DELETE CASCADE + );"; + + + // 6. 为 SplitCache_ModelList 表创建独立索引 + var createSplitModelListIndexSql = @" + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_ParentTableId ON SplitCache_ModelList(ParentTableId); -- 外键关联查询(核心索引) + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_CacheKey ON SplitCache_ModelList(CacheKey); -- 按 CacheKey 批量查询 + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_ParentID ON SplitCache_ModelList(ParentID); -- 按业务 ParentID 查询 + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_KeyId ON SplitCache_ModelList(KeyId); + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_FirmwareNo ON SplitCache_ModelList(FirmwareNo); + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_Support ON SplitCache_ModelList(Support); + CREATE INDEX IF NOT EXISTS IX_SplitCacheModelList_ChipType ON SplitCache_ModelList(ChipType);"; + + + //6.为 SplitCache_KzInfoList 表创建独立索引 + var createSplitKzInfoListIndexSql = @" + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_ModelListId ON SplitCache_KzInfoList(ModelListId); -- 外键关联查询(核心索引) + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_ChipType ON SplitCache_KzInfoList(chipType); -- 按芯片类型筛选 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_FirmwareNo ON SplitCache_KzInfoList(firmwareNo); -- 按固件编号查询 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_SpCode ON SplitCache_KzInfoList(spCode); -- 按子机code快速定位 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_Type ON SplitCache_KzInfoList(type); -- 按所属子机类型分类查询 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_Usable ON SplitCache_KzInfoList(usable); -- 按可用子机描述匹配 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_SourceUrl ON SplitCache_KzInfoList(sourceUrl); -- 按固件下载地址查询(用于去重或校验) + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_ZsId ON SplitCache_KzInfoList(zsId); -- 按遥控再生类型ID查询 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_Sp ON SplitCache_KzInfoList(sp); -- 快速筛选特殊子机 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_CloudRemote ON SplitCache_KzInfoList(cloudRemote); -- 按是否云遥控过滤 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_Dzxp ON SplitCache_KzInfoList(dzxp); -- 按是否电子芯片过滤 + CREATE INDEX IF NOT EXISTS IX_SplitCacheKzInfoList_UpdateByte ON SplitCache_KzInfoList(updateByte); -- 按是否需修改bin文件筛选"; + + // 用户token表 + var createUserTokenTableSql = @" + CREATE TABLE IF NOT EXISTS SysUserToken ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Token TEXT NOT NULL, + CreatedAt DATETIME NOT NULL, + ExpiresAt DATETIME NOT NULL + );"; + //用户信息表 + var createUserInfoTableSql = @" + CREATE TABLE IF NOT EXISTS SysUser ( + Id INTEGER PRIMARY KEY, + CreateTime TEXT, + DeptId INTEGER, + Email TEXT, + Image TEXT, + Local TEXT, + NickName TEXT, + Phone TEXT, + Remark TEXT, + Sex TEXT, + UpdateTime TEXT, + UserName TEXT, + UserStatus TEXT, + UserType TEXT, + Uuid TEXT UNIQUE, + WorkerAccount TEXT, + WorkerId INTEGER + );"; + #endregion + + conn.Execute(createTableSql); + + conn.Execute(createSplitParentTableSql); + conn.Execute(createSplitParentIndexSql); // 独立执行索引创建 + conn.Execute(createSplitModelListTableSql); + conn.Execute(createSplitModelListIndexSql); // 独立执行索引创建 + conn.Execute(createKZInfoListTableSql); + conn.Execute(createSplitKzInfoListIndexSql); // 独立执行索引创建 + + conn.Execute(createUserTokenTableSql); + conn.Execute(createUserInfoTableSql); + + + // 清理过期缓存 + _ = ClearExpiredAsync(); + _ = ClearSplitCacheExpiredAsync(); // 新增拆分缓存的过期清理 + } + catch (Exception ex) + { + throw new Exception($"初始化缓存数据库失败: {ex.Message}", ex); + } + } + #region 拆分缓存 + #region 拆分缓存-写入(拆分 VehModelClass 到父子表) + /// + /// 拆分 VehModelClass 数据(父级+子级列表)到拆分缓存表 + /// + public async Task SetSplitVehModelDataAsync(string cacheKey, VehModelClass vehModel, TimeSpan expiry) + { + if (vehModel == null || vehModel.modelList == null || !vehModel.modelList.Any()) + throw new ArgumentNullException(nameof(vehModel), "VehModelClass 或 modelList 不能为空"); + using var pooledConn = SQLiteCore.GetConnection(); + var conn = pooledConn.InnerConnection; + // 事务保证原子性(仅操作新表) + using (var transaction = conn.BeginTransaction()) + { + try + { + // 1. 序列化父级核心数据(VehModelClass 非列表字段) + var parentData = JsonConvert.SerializeObject(new + { + vehModel.ID, + vehModel.typeName, + vehModel.typeEngName, + vehModel.typePuName, + vehModel.typeXiName, + vehModel.typeInName + }, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None // 紧凑序列化,减少存储体积 + }); + + // 2. 写入父表 SplitCache_Parent + var splitParent = new SplitCacheParent + { + CacheKey = cacheKey, + ParentData = parentData, + SubItemCount = vehModel.modelList.Count, + DataType = typeof(VehModelClass).FullName, + CreatedAt = DateTime.Now, + ExpiresAt = DateTime.Now.Add(expiry), + LastAccessed = DateTime.Now, + AccessCount = 0, + Metadata = JsonConvert.SerializeObject(new + { + VehModelID = vehModel.ID, + ParentDataSize = Encoding.UTF8.GetByteCount(parentData), + SubItemCount = vehModel.modelList.Count, + CacheExpiryMinutes = expiry.TotalMinutes + }) + }; + + var insertParentSql = @" + INSERT INTO SplitCache_Parent ( + CacheKey, ParentData, SubItemCount, DataType, CreatedAt, ExpiresAt, LastAccessed, AccessCount, Metadata + ) VALUES ( + @CacheKey, @ParentData, @SubItemCount, @DataType, @CreatedAt, @ExpiresAt, @LastAccessed, @AccessCount, @Metadata + )"; + + await conn.ExecuteAsync(insertParentSql, splitParent, transaction); + var parentTableId = await conn.ExecuteScalarAsync( + "SELECT last_insert_rowid()", + transaction: transaction); + // 2. 插入 ModelList 并收集 KzInfo 待插入 + var allKzInfoItems = new List(); + + foreach (var keyItem in vehModel.modelList) + { + // 插入单条 ModelList + var modelListItem = new + { + ParentTableId = (int)parentTableId, + CacheKey = cacheKey, + ParentID = vehModel.ID, + KeyId = keyItem.id, + KeyKeyId = keyItem.keyId ?? string.Empty, + Name = keyItem.Name ?? string.Empty, + ModelName = keyItem.modelName ?? string.Empty, + ModelEngName = keyItem.modelEngName ?? string.Empty, + ModelPuName = keyItem.modelPuName ?? string.Empty, + ModelXiName = keyItem.modelXiName ?? string.Empty, + ModelInName = keyItem.modelInName ?? string.Empty, + Rate = keyItem.rate ?? string.Empty, + Usable = keyItem.usable ?? string.Empty, + UsableEn = keyItem.usableEn ?? string.Empty, + UsablePu = keyItem.usablePu ?? string.Empty, + UsableXi = keyItem.usableXi ?? string.Empty, + Information = keyItem.information ?? string.Empty, + PicUrl = keyItem.picUrl ?? string.Empty, + FirmwareNo = keyItem.firmwareNo ?? string.Empty, + SourceUrl = keyItem.sourceUrl ?? string.Empty, + SpCode = keyItem.spCode ?? string.Empty, + Support = keyItem.support ?? string.Empty, + ChipType = keyItem.chipType ?? string.Empty, + CreateTime = keyItem.createTime ?? string.Empty, + KeyMatchContent = keyItem.keyMatch?.content ?? string.Empty, + KeyMatchContentEn = keyItem.keyMatch?.contentEn ?? string.Empty, + KeyMatchContentPu = keyItem.keyMatch?.contentPu ?? string.Empty, + KeyMatchContentXi = keyItem.keyMatch?.contentXi ?? string.Empty, + KeyMatchContentIn = keyItem.keyMatch?.contentIn ?? string.Empty, + IsSelected = keyItem.IsSelected, + LocalImagePath = keyItem.LocalImagePath ?? string.Empty + }; + + await conn.ExecuteAsync(@" + INSERT INTO SplitCache_ModelList ( + ParentTableId, CacheKey, ParentID, KeyId, KeyKeyId, Name, ModelName, ModelEngName, ModelPuName, ModelXiName, ModelInName, + Rate, Usable, UsableEn, UsablePu, UsableXi, Information, PicUrl, FirmwareNo, SourceUrl, SpCode, + Support, ChipType, CreateTime, KeyMatchContent, KeyMatchContentEn, KeyMatchContentPu, + KeyMatchContentXi, KeyMatchContentIn, IsSelected, LocalImagePath + ) VALUES ( + @ParentTableId, @CacheKey, @ParentID, @KeyId, @KeyKeyId, @Name, @ModelName, @ModelEngName, @ModelPuName, @ModelXiName, @ModelInName, + @Rate, @Usable, @UsableEn, @UsablePu, @UsableXi, @Information, @PicUrl, @FirmwareNo, @SourceUrl, @SpCode, + @Support, @ChipType, @CreateTime, @KeyMatchContent, @KeyMatchContentEn, @KeyMatchContentPu, + @KeyMatchContentXi, @KeyMatchContentIn, @IsSelected, @LocalImagePath + )", modelListItem, transaction); + + var modelListId = await conn.ExecuteScalarAsync("SELECT last_insert_rowid()", transaction: transaction); + + // 3. 收集该 keyItem 下的所有 KzInfo + if (keyItem.kzInfoList != null && keyItem.kzInfoList.Any()) + { + foreach (var kz in keyItem.kzInfoList) + { + allKzInfoItems.Add(new SplitCacheKzInfoListItem + { + ModelListId = (int)modelListId, // 关键:关联到刚插入的 ModelList 记录 + binAddress = kz.binAddress ?? string.Empty, + binAddressArray = kz.binAddressArray != null ? JsonConvert.SerializeObject(kz.binAddressArray) : "[]", + chipType = kz.chipType ?? string.Empty, + cloudRemote = kz.cloudRemote, + dzxp = kz.dzxp, + firmwareNo = kz.firmwareNo ?? string.Empty, + fixedByte = kz.fixedByte, + fixedByteArray = kz.fixedByteArray != null ? JsonConvert.SerializeObject(kz.fixedByteArray) : "[]", + fixedByteArrays = kz.fixedByteArrays ?? string.Empty, + sourceUrl = kz.sourceUrl ?? string.Empty, + sp = kz.sp, + spCode = kz.spCode ?? string.Empty, + tsRand = kz.tsRand, + tsRandStr = kz.tsRandStr ?? string.Empty, + type = kz.type ?? string.Empty, + updateByte = kz.updateByte, + usable = kz.usable ?? string.Empty, + ysSource = kz.ysSource ?? string.Empty, + ysSourceNo = kz.ysSourceNo ?? string.Empty, + zsId = kz.zsId ?? string.Empty, + CreateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + }); + } + } + } + + // 4. 批量插入所有 KzInfo + if (allKzInfoItems.Any()) + { + await conn.ExecuteAsync(@" + INSERT INTO SplitCache_KzInfoList ( + ModelListId, binAddress, binAddressArray, chipType, cloudRemote, dzxp, firmwareNo, + fixedByte, fixedByteArray, fixedByteArrays, sourceUrl, sp, spCode, tsRand, tsRandStr, + type, updateByte, usable, ysSource, ysSourceNo, zsId, CreateTime + ) VALUES ( + @ModelListId, @binAddress, @binAddressArray, @chipType, @cloudRemote, @dzxp, @firmwareNo, + @fixedByte, @fixedByteArray, @fixedByteArrays, @sourceUrl, @sp, @spCode, @tsRand, @tsRandStr, + @type, @updateByte, @usable, @ysSource, @ysSourceNo, @zsId, @CreateTime + )", allKzInfoItems, transaction); + } + + // 提交事务 + transaction.Commit(); + //System.Diagnostics.Debug.WriteLine($"拆分缓存写入成功:CacheKey={cacheKey},父级ID={vehModel.ID},子级条数={vehModel.modelList.Count}"); + } + catch (Exception ex) + { + transaction.Rollback(); + //System.Diagnostics.Debug.WriteLine($"写入拆分缓存失败:{ex.Message}"); + throw new Exception($"写入拆分缓存失败(CacheKey={cacheKey})", ex); + } + } + } + #endregion + #region 拆分缓存-查询(合并父子表为 VehModelClass) + /// + /// 查询拆分缓存,返回完整的 List 实体 + /// + public async Task> GetSplitVehModelDataAsync(string cacheKey) + { + if (string.IsNullOrEmpty(cacheKey)) + throw new ArgumentNullException(nameof(cacheKey), "缓存Key不能为空"); + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + try + { + // 1. 查父表,验证缓存是否存在且未过期 + var getParentSql = @" + SELECT * FROM SplitCache_Parent + WHERE CacheKey = @CacheKey AND ExpiresAt > @Now"; + + var splitParents = await _connection.QueryAsync(getParentSql, new + { + CacheKey = cacheKey, + Now = DateTime.Now + }); + var splitParentList = splitParents.ToList(); + + if (splitParentList.Count == 0) + { + //System.Diagnostics.Debug.WriteLine($"拆分缓存未命中:CacheKey={cacheKey}"); + return null; + } + + // 2. 更新父表访问统计 + await UpdateSplitParentAccessStatsAsync(cacheKey); + + // 3. 查子表(关联 CacheKey,获取所有 VehKeyClass 对应的记录) + var getChildSql = "SELECT * FROM SplitCache_ModelList WHERE CacheKey = @CacheKey"; + var modelListItems = (await _connection.QueryAsync(getChildSql, new + { + CacheKey = cacheKey + })).ToList(); + var result = new List(); + // 4.批量查询所有关联的 KzInfo(通过 ModelList.Id) + var modelListIds = modelListItems.Select(m => m.Id).ToArray(); + var getKzInfoSql = $"SELECT * FROM SplitCache_KzInfoList WHERE ModelListId IN ({string.Join(",", modelListIds)})"; + var kzInfoItems = (await _connection.QueryAsync(getKzInfoSql)).ToList(); + + // 按 ModelListId 分组,便于快速查找 + var kzInfoLookup = kzInfoItems + .GroupBy(k => k.ModelListId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var splitParent in splitParentList) + { + // 4. 校验子级数据完整性 + if (modelListItems.Count != splitParent.SubItemCount) + { + var errorMsg = $"拆分缓存子级数据不完整:CacheKey={cacheKey},预期 {splitParent.SubItemCount} 条,实际 {modelListItems.Count} 条"; + //System.Diagnostics.Debug.WriteLine(errorMsg); + await DeleteSplitVehModelDataAsync(cacheKey); // 删除无效缓存 + return null; + } + + // 5. 反序列化父级数据(VehModelClass 核心字段) + var parentCoreData = JsonConvert.DeserializeObject(splitParent.ParentData); + var vehModel = new VehModelClass + { + ID = parentCoreData?.ID ?? 0, + typeName = parentCoreData?.typeName ?? string.Empty, + typeEngName = parentCoreData?.typeEngName ?? string.Empty, + typePuName = parentCoreData?.typePuName ?? string.Empty, + typeXiName = parentCoreData?.typeXiName ?? string.Empty, + typeInName = parentCoreData?.typeInName ?? string.Empty, + modelList = new List() + }; + + // 6. 转换子表数据为 VehKeyClass 实体(含 keyMatch 嵌套类) + foreach (var item in modelListItems) + { + var vehKey = new VehKeyClass + { + parentId = item.ParentID, + id = item.KeyId, + keyId = item.KeyKeyId, + Name = item.Name, + modelName = item.ModelName, + modelEngName = item.ModelEngName, + modelPuName = item.ModelPuName, + modelXiName = item.ModelXiName, + modelInName = item.ModelInName, + rate = item.Rate, + usable = item.Usable, + usableEn = item.UsableEn, + usablePu = item.UsablePu, + usableXi = item.UsableXi, + information = item.Information, + picUrl = item.PicUrl, + firmwareNo = item.FirmwareNo, + sourceUrl = item.SourceUrl, + spCode = item.SpCode, + support = item.Support, + chipType = item.ChipType, + createTime = item.CreateTime, + // 重组 keyMatch 嵌套类 + keyMatch = new keyMatch + { + content = item.KeyMatchContent, + contentEn = item.KeyMatchContentEn, + contentPu = item.KeyMatchContentPu, + contentXi = item.KeyMatchContentXi, + contentIn = item.KeyMatchContentIn + }, + // UI 相关字段(BitmapImage 需业务层自行加载,此处仅设置 LocalImagePath) + LocalImagePath = item.LocalImagePath, + IsSelected = item.IsSelected, + kzInfoList = new List() // 初始化 + }; + // 挂载对应的 KzInfo 列表 + if (kzInfoLookup.TryGetValue(item.Id, out var kzList)) + { + foreach (var kzItem in kzList) + { + vehKey.kzInfoList.Add(new KzInfo + { + binAddress = kzItem.binAddress, + binAddressArray = !string.IsNullOrEmpty(kzItem.binAddressArray) + ? JsonConvert.DeserializeObject>(kzItem.binAddressArray) + : new List(), + chipType = kzItem.chipType, + cloudRemote = kzItem.cloudRemote, + dzxp = kzItem.dzxp, + firmwareNo = kzItem.firmwareNo, + fixedByte = kzItem.fixedByte, + fixedByteArray = !string.IsNullOrEmpty(kzItem.fixedByteArray) + ? JsonConvert.DeserializeObject>(kzItem.fixedByteArray) + : new List(), + fixedByteArrays = kzItem.fixedByteArrays, + sourceUrl = kzItem.sourceUrl, + sp = kzItem.sp, + spCode = kzItem.spCode, + tsRand = kzItem.tsRand, + tsRandStr = kzItem.tsRandStr, + type = kzItem.type, + updateByte = kzItem.updateByte, + usable = kzItem.usable, + ysSource = kzItem.ysSource, + ysSourceNo = kzItem.ysSourceNo, + zsId = kzItem.zsId + }); + } + } + vehModel.modelList.Add(vehKey); + } + result.Add(vehModel); + } + return result; + } + catch (Exception ex) + { + //System.Diagnostics.Debug.WriteLine($"查询拆分缓存失败:{ex.Message}"); + return null; + } + } + #endregion + + #region 拆分缓存-删除(同步删除父子表数据) + /// + /// 删除指定 CacheKey 的拆分缓存(父子表同步删除) + /// + public async Task DeleteSplitVehModelDataAsync(string cacheKey) + { + if (string.IsNullOrEmpty(cacheKey)) + throw new ArgumentNullException(nameof(cacheKey), "缓存Key不能为空"); + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + using (var transaction = _connection.BeginTransaction()) + { + try + { + // 1. 先删除子表(外键约束 ON DELETE CASCADE 也会自动删除,但手动删除更清晰) + var deleteChildCount = await _connection.ExecuteAsync( + "DELETE FROM SplitCache_ModelList WHERE CacheKey = @CacheKey", + new { CacheKey = cacheKey }, + transaction + ); + + // 2. 再删除父表 + var deleteParentCount = await _connection.ExecuteAsync( + "DELETE FROM SplitCache_Parent WHERE CacheKey = @CacheKey", + new { CacheKey = cacheKey }, + transaction + ); + + transaction.Commit(); + System.Diagnostics.Debug.WriteLine($"拆分缓存删除成功:CacheKey={cacheKey},删除子级{deleteChildCount}条,父级{deleteParentCount}条"); + } + catch (Exception ex) + { + transaction.Rollback(); + System.Diagnostics.Debug.WriteLine($"删除拆分缓存失败:{ex.Message}"); + throw new Exception($"删除拆分缓存失败(CacheKey={cacheKey})", ex); + } + } + } + #endregion + + #region 拆分缓存-过期清理(独立清理,不影响旧缓存) + /// + /// 清理拆分缓存中过期的数据(父子表同步清理) + /// + public async Task ClearSplitCacheExpiredAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + using (var transaction = _connection.BeginTransaction()) + { + try + { + // 1. 查询所有过期的父表 CacheKey + var expiredKeysSql = "SELECT CacheKey FROM SplitCache_Parent WHERE ExpiresAt <= @Now"; + var expiredKeys = (await _connection.QueryAsync(expiredKeysSql, new { Now = DateTime.Now }, transaction)).ToList(); + + if (!expiredKeys.Any()) + { + transaction.Commit(); + //System.Diagnostics.Debug.WriteLine("无过期拆分缓存数据"); + return; + } + + // 2. 批量删除过期子表数据 + var deleteChildCount = await _connection.ExecuteAsync( + "DELETE FROM SplitCache_ModelList WHERE CacheKey IN @ExpiredKeys", + new { ExpiredKeys = expiredKeys }, + transaction + ); + + // 3. 批量删除过期父表数据 + var deleteParentCount = await _connection.ExecuteAsync( + "DELETE FROM SplitCache_Parent WHERE CacheKey IN @ExpiredKeys", + new { ExpiredKeys = expiredKeys }, + transaction + ); + + transaction.Commit(); + //System.Diagnostics.Debug.WriteLine($"清理过期拆分缓存成功:共{expiredKeys.Count}组,删除子级{deleteChildCount}条,父级{deleteParentCount}条"); + } + catch (Exception ex) + { + transaction.Rollback(); + //System.Diagnostics.Debug.WriteLine($"清理过期拆分缓存失败:{ex.Message}"); + } + } + } + #endregion + + #region 拆分缓存-辅助方法 + /// + /// 更新拆分缓存父表的访问统计 + /// + private async Task UpdateSplitParentAccessStatsAsync(string cacheKey) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = @" + UPDATE SplitCache_Parent + SET LastAccessed = @LastAccessed, AccessCount = AccessCount + 1 + WHERE CacheKey = @CacheKey"; + + await _connection.ExecuteAsync(sql, new + { + CacheKey = cacheKey, + LastAccessed = DateTime.Now + }); + } + + /// + /// 检查拆分缓存是否存在(未过期) + /// + public async Task SplitCacheExistsAsync(string cacheKey) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = "SELECT 1 FROM SplitCache_Parent WHERE CacheKey = @CacheKey AND ExpiresAt > @Now"; + var result = await _connection.ExecuteScalarAsync(sql, new + { + CacheKey = cacheKey, + Now = DateTime.Now + }); + return result.HasValue; + } + #endregion + #endregion + + #region 缓存基础方法 + public async Task GetOrCreateAsync(string key, Func> dataFetchFunc, TimeSpan expiry) where T : class + { + // 先尝试从缓存获取 + var cachedData = await GetAsync(key); + if (cachedData != null) + { + if (cachedData switch + { + ICollection { Count: > 0 } => true, + Array { Length: > 0 } => true, + string { Length: > 0 } => true, + _ => false + }) + { + return cachedData; + } + //return cachedData; + } + + // 缓存未命中,从数据源获取 + var freshData = await dataFetchFunc(); + + if (freshData != null) + { + // 异步保存到缓存(不阻塞当前操作) + _ = SetAsync(key, freshData, expiry); + } + + return freshData; + } + + public async Task GetAsync(string key) where T : class + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + try + { + var sql = @" + SELECT * FROM CacheItems + WHERE CacheKey = @Key AND ExpiresAt > @Now"; + + var cacheItem = await _connection.QueryFirstOrDefaultAsync(sql, new + { + Key = key, + Now = DateTime.Now + }); + + if (cacheItem != null) + { + // 更新访问统计 + await UpdateAccessStatsAsync(key); + + // 反序列化数据 + return DeserializeData(cacheItem.Data); + } + + return null; + } + catch (Exception ex) + { + //System.Diagnostics.Debug.WriteLine($"缓存读取失败: {ex.Message}"); + return null; + } + } + + public async Task SetAsync(string key, T data, TimeSpan expiry) where T : class + { + if (data == null) return; + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + try + { + var cacheItem = new CacheItem + { + CacheKey = key, + Data = SerializeData(data), + DataType = typeof(T).FullName, + CreatedAt = DateTime.Now, + ExpiresAt = DateTime.Now.Add(expiry), + LastAccessed = DateTime.Now, + AccessCount = 0, + Metadata = JsonConvert.SerializeObject(new + { + DataSize = GetDataSize(data), + ItemCount = data is System.Collections.ICollection collection ? collection.Count : 0 + }) + }; + + var sql = @" + INSERT OR REPLACE INTO CacheItems + (CacheKey, Data, DataType, CreatedAt, ExpiresAt, LastAccessed, AccessCount, Metadata) + VALUES + (@CacheKey, @Data, @DataType, @CreatedAt, @ExpiresAt, @LastAccessed, @AccessCount, @Metadata)"; + + await _connection.ExecuteAsync(sql, cacheItem); + } + catch (Exception ex) + { + //System.Diagnostics.Debug.WriteLine($"缓存保存失败: {ex.Message}"); + + } + } + + public async Task ExistsAsync(string key) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = "SELECT 1 FROM CacheItems WHERE CacheKey = @Key AND ExpiresAt > @Now"; + var result = await _connection.ExecuteScalarAsync(sql, new + { + Key = key, + Now = DateTime.Now + }); + return result.HasValue; + } + + public async Task RemoveAsync(string key) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + await _connection.ExecuteAsync("DELETE FROM CacheItems WHERE CacheKey = @Key", new { Key = key }); + } + + public async Task ClearExpiredAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + await _connection.ExecuteAsync("DELETE FROM CacheItems WHERE ExpiresAt <= @Now", new { Now = DateTime.Now }); + } + + public async Task ClearAllAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + await _connection.ExecuteAsync("DELETE FROM CacheItems"); + await _connection.ExecuteAsync("DELETE FROM SplitCache_Parent"); + await _connection.ExecuteAsync("DELETE FROM SplitCache_ModelList"); + } + + public async Task> GetCacheStatsAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + return await _connection.QueryAsync( + "SELECT * FROM CacheItems ORDER BY LastAccessed DESC"); + } + + public async Task GetCacheSizeAsync() + { + try + { + var fileInfo = new FileInfo(_databasePath); + return fileInfo.Exists ? fileInfo.Length : 0; + } + catch + { + return 0; + } + } + + private async Task UpdateAccessStatsAsync(string key) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = @" + UPDATE CacheItems + SET LastAccessed = @LastAccessed, AccessCount = AccessCount + 1 + WHERE CacheKey = @Key"; + + await _connection.ExecuteAsync(sql, new + { + Key = key, + LastAccessed = DateTime.Now + }); + } + + private string SerializeData(T data) + { + return JsonConvert.SerializeObject(data, new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore + }); + } + + private T DeserializeData(string data) + { + return JsonConvert.DeserializeObject(data, new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto + }); + } + + private long GetDataSize(T data) + { + try + { + var json = SerializeData(data); + return System.Text.Encoding.UTF8.GetByteCount(json); + } + catch + { + return 0; + } + } + + public void Dispose() + { + if (!_disposed) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + _connection?.Close(); + _connection?.Dispose(); + _disposed = true; + } + } + #endregion + + #region 用户Token + public async Task StoreUserTokenAsync(string token, TimeSpan? expiry = null) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var createdAt = DateTime.Now; + DateTime? expiresAt = DateTime.Now.AddYears(10); + if (expiry.HasValue) + { + expiresAt = createdAt.Add(expiry.Value); + } + var sql = @" + INSERT INTO SysUserToken (Token, CreatedAt, ExpiresAt) + VALUES (@Token, @CreatedAt, @ExpiresAt)"; + await _connection.ExecuteAsync(sql, new + { + Token = token, + CreatedAt = createdAt, + ExpiresAt = expiresAt + }); + } + public async Task IsUserTokenValidAsync(string token) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = @" + SELECT 1 FROM SysUserToken + WHERE Token = @Token AND ( ExpiresAt > @Now)"; + var result = await _connection.ExecuteScalarAsync(sql, new + { + Token = token, + Now = DateTime.Now + }); + return result.HasValue; + } + public async Task ClearUserTokenAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = "DELETE FROM SysUserToken "; + await _connection.ExecuteAsync(sql); + } + public async Task ClearExpiredUserTokensAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = "DELETE FROM SysUserToken WHERE ExpiresAt <= @Now"; + await _connection.ExecuteAsync(sql, new { Now = DateTime.Now }); + } + public async Task GetLatestUserTokenAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = @" + SELECT Token FROM SysUserToken + WHERE ExpiresAt > @Now + ORDER BY CreatedAt DESC LIMIT 1"; + return await _connection.ExecuteScalarAsync(sql, new { Now = DateTime.Now }); + } + public async Task StoreUserInfoAsync(UserModel user) + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = @" + INSERT OR REPLACE INTO SysUser + (Id, CreateTime, DeptId, Email, Image, Local, NickName, Phone, Remark, + Sex, UpdateTime, UserName, UserStatus, UserType, Uuid, WorkerAccount, WorkerId) + VALUES + (@Id, @CreateTime, @DeptId, @Email, @Image, @Local, @NickName, @Phone, @Remark, + @Sex, @UpdateTime, @UserName, @UserStatus, @UserType, @Uuid, @WorkerAccount, @WorkerId)"; + var rowsAffected = await _connection.ExecuteAsync(sql, user); + return rowsAffected > 0; + } + public async Task ClearUserInfoAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = "DELETE FROM SysUser"; + await _connection.ExecuteAsync(sql); + } + public async Task GetUserInfoAsync() + { + using var pooledConn = SQLiteCore.GetConnection(); + var _connection = pooledConn.InnerConnection; + var sql = @" + SELECT * FROM SysUser LIMIT 1"; + return await _connection.QueryFirstOrDefaultAsync(sql); + } + #endregion + } +} diff --git a/AIK.Service/Service/UsbDevconHelper.cs b/AIK.Service/Service/UsbDevconHelper.cs new file mode 100644 index 0000000..4d8e666 --- /dev/null +++ b/AIK.Service/Service/UsbDevconHelper.cs @@ -0,0 +1,139 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Principal; +using System.Threading.Tasks; + +namespace AIK.Service.Service +{ + /// + /// devcon.exe 封装:用于 Win7 上解决 USB 设备热切换(HID ↔ U 盘模式)时枚举失败(Code 10)的问题。 + /// devcon 是 Windows Driver Kit(WDK)自带工具,**不是** Windows 系统自带,需要随程序分发: + /// 将 devcon.exe(x86/x64 与程序位数一致)放到程序运行目录,或放在 LocateDevcon 指定的路径。 + /// 注意:devcon remove / restart / rescan 需要**管理员权限**,建议程序以管理员身份运行。 + /// 调用失败时全部降级返回 false,不抛异常,不影响原流程。 + /// + public static class UsbDevconHelper + { + /// + /// 查找 devcon.exe。优先按当前进程位数查找运行目录下的 x64 或 x86 子目录, + /// 其次回退到运行目录根下的 devcon.exe;找不到返回 null。 + /// + public static string LocateDevcon() + { + var candidates = new[] + { + AppDomain.CurrentDomain.BaseDirectory, + AppDomain.CurrentDomain.RelativeSearchPath ?? string.Empty + }; + + // 优先位数匹配的子目录(x64/x86),程序 AnyCPU 编译时按运行进程位数选择 + string archDir = IntPtr.Size == 8 ? "x64" : "x86"; + foreach (var dir in candidates) + { + if (string.IsNullOrEmpty(dir)) continue; + var archExe = Path.Combine(dir, archDir, "devcon.exe"); + if (File.Exists(archExe)) return archExe; + } + + // 回退:运行目录根下的 devcon.exe(兼容手动放置的场景) + foreach (var dir in candidates) + { + if (string.IsNullOrEmpty(dir)) continue; + var exe = Path.Combine(dir, "devcon.exe"); + if (File.Exists(exe)) return exe; + } + return null; + } + + /// 是否以管理员权限运行(devcon remove/restart/rescan 需要)。 + public static bool IsAdministrator() + { + try + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + catch + { + return false; + } + } + + /// 执行 devcon rescan(重新扫描硬件)。 + public static Task RescanAsync(int timeoutMs = 30000) + { + return RunAsync("rescan", timeoutMs); + } + + /// 执行 devcon restart(重启设备:disable + enable,强制重新加载驱动)。 + public static Task RestartDeviceAsync(int vid, int pid, int timeoutMs = 30000) + { + // 用通配符匹配设备实例 ID(形如 USB\VID_28E9&PID_FFF0\xxxxx) + var hwid = $"USB\\VID_{vid:X4}&PID_{pid:X4}"; + return RunAsync($"restart \"{hwid}\"", timeoutMs); + } + + /// + /// 执行 devcon remove + rescan(删除设备实例后重新扫描),等价于"软件拔插", + /// 比 restart(disable+enable)更彻底,对 Win7 热切换 Code 10 最有效。 + /// + public static async Task RemoveAndRescanAsync(int vid, int pid, int timeoutMs = 30000) + { + var hwid = $"USB\\VID_{vid:X4}&PID_{pid:X4}"; + bool removed = await RunAsync($"remove \"{hwid}\"", timeoutMs).ConfigureAwait(false); + // 无论 remove 是否成功都执行 rescan(设备可能已不在,rescan 无副作用) + bool rescanned = await RunAsync("rescan", timeoutMs).ConfigureAwait(false); + return removed || rescanned; + } + + /// 执行任意 devcon 命令;找不到 exe / 非管理员 / 超时 / 非零退出码均返回 false。 + public static async Task RunAsync(string arguments, int timeoutMs = 30000) + { + var devcon = LocateDevcon(); + if (devcon == null) + { + return false; // 未分发 devcon.exe,静默降级 + } + if (!IsAdministrator()) + { + return false; // 非管理员时 devcon 无法操作设备,静默降级 + } + + Process process = null; + try + { + var psi = new ProcessStartInfo + { + FileName = devcon, + Arguments = arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + process = Process.Start(psi); + + // .NET Framework 4.8 无 WaitForExitAsync,用 Task.Run 包装 + 超时控制 + var exitTask = Task.Run(() => process.WaitForExit(timeoutMs)); + bool exited = await exitTask.ConfigureAwait(false); + if (!exited) + { + try { process.Kill(); } catch { /* 忽略 */ } + return false; + } + + return process.ExitCode == 0; + } + catch + { + return false; + } + finally + { + try { process?.Dispose(); } catch { /* 忽略 */ } + } + } + } +} diff --git a/AIK.Service/Service/UserSessionCoordinator.cs b/AIK.Service/Service/UserSessionCoordinator.cs new file mode 100644 index 0000000..9623449 --- /dev/null +++ b/AIK.Service/Service/UserSessionCoordinator.cs @@ -0,0 +1,558 @@ +using AIK.Common.Enum; +using AIK.Common.Interface; +using AIK.Common.StompNet; +using AIK.Models.ApiModels; +using AIK.Models.ApiModels.Integral; +using AIK.Models.SystemSettings; +using AIK.Service.IService; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace AIK.Service.Service; + +public class UserSessionCoordinator : IUserSessionCoordinator +{ + private const string DefaultLogoPath = "AIK.Assets.Images.logo.png"; + private const int RequestedHeartbeatInterval = 4000; + // 服务器心跳超时倍数:由 1.5 放宽到 3,容纳 2 个心跳周期(8s)的抖动, + // 避免 Win7 网络波动导致二维码在几秒内被误判过期 + private const double HeartbeatTimeoutMultiplier = 3.0; + // 二维码过期时间下限:即使协商的心跳间隔很短,也保证二维码至少 1 分钟内不因心跳超时过期 + private static readonly TimeSpan MinQrCodeExpireTime = TimeSpan.FromMinutes(1); + + private readonly IConfigurationService _configurationService; + private readonly IQRCodeService _qrCodeService; + private readonly IDataCacheService _dataCacheService; + private readonly IAikDataService _aikDataService; + private readonly ILogger _logger; + private readonly SemaphoreSlim _loginChannelLock = new(1, 1); + + private UserSessionState _state = new(); + private BouncyCastleWebSocketClient? _webSocket; + private CancellationTokenSource? _cts; + private CancellationTokenSource? _heartbeatCts; + private int _clientSendInterval = RequestedHeartbeatInterval; + private int _serverSendInterval = RequestedHeartbeatInterval; + private DateTime _lastServerHeartbeat = DateTime.UtcNow; + private string? _sessionId; + private string? _userToken; + + public event EventHandler? StateChanged; + + public UserSessionCoordinator( + IConfigurationService configurationService, + IQRCodeService qrCodeService, + IDataCacheService dataCacheService, + IAikDataService aikDataService, + ILogger logger) + { + _configurationService = configurationService; + _qrCodeService = qrCodeService; + _dataCacheService = dataCacheService; + _aikDataService = aikDataService; + _logger = logger; + } + + public async Task InitializeAsync() + { + // 容错:SQLite 缓存数据库异常(损坏/缺失/被占用)时,不能阻断扫码登录。 + // 否则二维码发布流程(PublishWaitingForQrCodeAsync)不会执行,Win7 上表现为二维码不显示。 + string? token = null; + try + { + token = await _dataCacheService.GetLatestUserTokenAsync(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "读取本地登录缓存失败(SQLite),按未登录处理,继续展示二维码。"); + } + + _userToken = token; + + if (string.IsNullOrWhiteSpace(_userToken)) + { + await PublishWaitingForQrCodeAsync(); + return; + } + + await RestoreSessionAsync(_userToken); + } + + public virtual async Task RefreshIntegralAsync() + { + var token = _userToken ?? await _dataCacheService.GetLatestUserTokenAsync(); + if (string.IsNullOrWhiteSpace(token) || !_state.IsLoggedIn) + { + return; + } + + var integral = await _aikDataService.GetUserIntegral(token); + PublishState(CloneState(integralInfo: integral)); + } + + public virtual async Task LogoutAsync() + { + await _loginChannelLock.WaitAsync(); + try + { + await StopLoginChannelAsync(); + _userToken = null; + await _dataCacheService.ClearUserInfoAsync(); + await _dataCacheService.ClearUserTokenAsync(); + await PublishWaitingForQrCodeAsync(); + } + finally + { + _loginChannelLock.Release(); + } + } + + /// + /// 账密登录成功后写入会话:停止扫码通道、保存 Token、拉取用户信息与积分并发布登录状态。 + /// 返回是否成功应用(用户信息拉取失败返回 false)。 + /// + public virtual async Task LoginWithTokenAsync(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + return false; + } + + await _loginChannelLock.WaitAsync(); + try + { + await StopLoginChannelAsync(); + _userToken = token; + await _dataCacheService.ClearUserTokenAsync(); + await _dataCacheService.StoreUserTokenAsync(_userToken); + + var user = await _aikDataService.GetUserInfo(_userToken); + if (user is null) + { + PublishState(CloneState( + isLoggedIn: false, + statusText: "登录失败,无法获取用户信息", + userStatus: UserStatus.NotLoggedIn, + userInfo: null, + integralInfo: null, + canShowAuthorizedMenus: false)); + _userToken = null; + return false; + } + + await _dataCacheService.StoreUserInfoAsync(user); + var integral = await _aikDataService.GetUserIntegral(_userToken); + PublishState(CloneState( + isLoggedIn: true, + isQrCodeExpired: false, + statusText: "登录成功!", + userStatus: UserStatus.LoggedInOnline, + userInfo: user, + integralInfo: integral, + canShowAuthorizedMenus: true)); + return true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "账密登录写入会话失败"); + return false; + } + finally + { + _loginChannelLock.Release(); + } + } + + public virtual async Task RefreshQrCodeAsync() + { + await _loginChannelLock.WaitAsync(); + try + { + await StopLoginChannelAsync(); + await PublishWaitingForQrCodeAsync(); + } + finally + { + _loginChannelLock.Release(); + } + } + + protected virtual async Task StartLoginChannelAsync(CancellationToken cancellationToken) + { + ResetHeartbeatState(); + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + // 使用 BouncyCastle 纯托管 TLS 建立扫码登录通道: + // Win7 SChannel 未启用 TLS 1.2 时,websocket-sharp(内部 new SslStream)握手必然失败(1015)。 + // BouncyCastleWebSocketClient 用 BouncyCastleTlsHelper 完成 TLS 1.2 握手(含 SNI + 证书校验), + // 自行实现 RFC 6455 帧编解码,完全绕过 SChannel。 + var webSocket = new BouncyCastleWebSocketClient(_configurationService.ApiSettings.WSUrl); + _webSocket = webSocket; + webSocket.WaitTime = TimeSpan.FromSeconds(15); + + webSocket.OnMessage += OnWebSocketMessage; + webSocket.OnError += (sender, e) => _logger.LogWarning("Login WebSocket error: {Message}", e.Message); + webSocket.OnClose += (sender, e) => _logger.LogInformation("Login WebSocket closed: {Code} {Reason}", e.Code, e.Reason); + + try + { + await Task.Run(() => webSocket.Connect(), _cts.Token); + } + catch (Exception ex) + { + _logger.LogError(ex, "Login WebSocket connection failed: {WSUrl}", _configurationService.ApiSettings.WSUrl); + try + { + _webSocket = null; + webSocket.Close(CloseStatusCode.Abnormal, string.Empty); + } + catch + { + // 关闭失败无需处理,连接已不可用 + } + PublishState(CloneState( + isQrCodeExpired: true, + statusText: $"登录服务连接失败: {ex.Message}", + userStatus: UserStatus.NotLoggedIn)); + return; + } + + var connectFrame = StompFrameHelper.BuildFrame("CONNECT", new Dictionary + { + ["UUID"] = _sessionId!, + ["accept-version"] = "1.2,1.1,1.0", + ["heart-beat"] = $"{RequestedHeartbeatInterval},{RequestedHeartbeatInterval}" + }); + webSocket.Send(connectFrame); + } + + protected virtual async Task StopLoginChannelAsync() + { + try + { + _heartbeatCts?.Cancel(); + _heartbeatCts?.Dispose(); + _heartbeatCts = null; + + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + + if (_webSocket is { ReadyState: WebSocketState.Open }) + { + _webSocket.Close(CloseStatusCode.Normal, string.Empty); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to stop login channel cleanly."); + } + finally + { + _webSocket = null; + } + } + + protected void PublishState(UserSessionState state) + { + _state = state; + StateChanged?.Invoke(this, new UserSessionStateChangedEventArgs(state)); + } + + protected byte[] GenerateQrCode(string text) => _qrCodeService.GenerateQRCodeWithLogo(text, DefaultLogoPath, 12); + + private UserSessionState CloneState( + bool? isLoggedIn = null, + bool? isQrCodeExpired = null, + string? statusText = null, + UserStatus? userStatus = null, + UserModel? userInfo = null, + IntegralModel? integralInfo = null, + byte[]? qrCodeUserBytes = null, + bool? canShowAuthorizedMenus = null) + { + return new UserSessionState + { + IsLoggedIn = isLoggedIn ?? _state.IsLoggedIn, + IsQrCodeExpired = isQrCodeExpired ?? _state.IsQrCodeExpired, + StatusText = statusText ?? _state.StatusText, + UserStatus = userStatus ?? _state.UserStatus, + UserInfo = userInfo ?? _state.UserInfo, + IntegralInfo = integralInfo ?? _state.IntegralInfo, + QrCodeUserBytes = qrCodeUserBytes ?? _state.QrCodeUserBytes, + CanShowAuthorizedMenus = canShowAuthorizedMenus ?? _state.CanShowAuthorizedMenus + }; + } + + private async Task PublishWaitingForQrCodeAsync() + { + _sessionId = Guid.NewGuid().ToString("N"); + var qrBytes = GenerateQrCode($"login:{_sessionId}"); + PublishState(new UserSessionState + { + IsLoggedIn = false, + IsQrCodeExpired = false, + StatusText = "等待扫码登录...", + UserStatus = UserStatus.NotLoggedIn, + QrCodeUserBytes = qrBytes, + CanShowAuthorizedMenus = false, + IntegralInfo = null, + UserInfo = null + }); + + await StartLoginChannelAsync(CancellationToken.None); + } + + private async Task RestoreSessionAsync(string token) + { + _userToken = token; + var user = await _aikDataService.GetUserInfo(token); + if (user is not null) + { + var integral = await _aikDataService.GetUserIntegral(token); + await _dataCacheService.ClearUserInfoAsync(); + await _dataCacheService.StoreUserInfoAsync(user); + PublishState(new UserSessionState + { + IsLoggedIn = true, + StatusText = "已登录", + UserStatus = UserStatus.LoggedInOnline, + UserInfo = user, + IntegralInfo = integral, + CanShowAuthorizedMenus = true + }); + return; + } + + var cachedUser = await _dataCacheService.GetUserInfoAsync(); + if (cachedUser is not null) + { + PublishState(new UserSessionState + { + IsLoggedIn = true, + StatusText = "已登录(离线模式)", + UserStatus = UserStatus.LoggedInOffline, + UserInfo = cachedUser, + IntegralInfo = null, + CanShowAuthorizedMenus = false + }); + return; + } + + _userToken = null; + await PublishWaitingForQrCodeAsync(); + } + + private static TimeSpan? GetServerHeartbeatTimeout(int serverSendInterval) + { + if (serverSendInterval <= 0) + { + return null; + } + + // 实际超时取「协商间隔 × 倍数」与 30 秒下限中的较大值 + var computed = TimeSpan.FromMilliseconds(serverSendInterval * HeartbeatTimeoutMultiplier); + return computed > MinQrCodeExpireTime ? computed : MinQrCodeExpireTime; + } + + private static int NegotiateHeartbeatInterval(int localInterval, int remoteInterval) + { + return localInterval > 0 && remoteInterval > 0 + ? Math.Max(localInterval, remoteInterval) + : 0; + } + + private void ResetHeartbeatState() + { + _clientSendInterval = RequestedHeartbeatInterval; + _serverSendInterval = RequestedHeartbeatInterval; + _lastServerHeartbeat = DateTime.UtcNow; + } + + private void StartHeartbeat(BouncyCastleWebSocketClient webSocket) + { + _heartbeatCts?.Cancel(); + _heartbeatCts?.Dispose(); + _heartbeatCts = new CancellationTokenSource(); + var token = _heartbeatCts.Token; + var clientSendInterval = _clientSendInterval; + var serverHeartbeatTimeout = GetServerHeartbeatTimeout(_serverSendInterval); + + if (clientSendInterval > 0) + { + _ = Task.Run(async () => + { + while (!token.IsCancellationRequested) + { + try + { + await Task.Delay(clientSendInterval, token); + if (webSocket.ReadyState == WebSocketState.Open) + { + // STOMP 心跳:发送单个 0x0A 帧 + webSocket.Send(new byte[] { 0x0A }); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send STOMP heartbeat."); + } + } + }, token); + } + + if (serverHeartbeatTimeout is null) + { + return; + } + var timeout = serverHeartbeatTimeout.Value; + + _ = Task.Run(async () => + { + while (!token.IsCancellationRequested && !_state.IsLoggedIn && !_state.IsQrCodeExpired) + { + try + { + await Task.Delay(1500, token); + if (DateTime.UtcNow - _lastServerHeartbeat <= timeout) + { + continue; + } + + if (webSocket.ReadyState == WebSocketState.Open) + { + webSocket.Close(CloseStatusCode.ServerError, "Heartbeat timeout"); + } + + if (!token.IsCancellationRequested && !_state.IsLoggedIn) + { + PublishState(CloneState(isQrCodeExpired: true)); + } + + break; + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Heartbeat monitor failed."); + break; + } + } + }, token); + } + + /// + /// websocket-sharp 事件模型:收到任何帧都会触发(在接收线程执行)。 + /// 0x0A 单字节帧 = STOMP 服务器心跳;其余按文本帧解析 STOMP 协议。 + /// + private void OnWebSocketMessage(object? sender, MessageEventArgs e) + { + if (e.IsBinary && e.RawData.Length == 1 && e.RawData[0] == 0x0A) + { + _lastServerHeartbeat = DateTime.UtcNow; + return; + } + + if (!e.IsText || string.IsNullOrEmpty(e.Data)) + { + return; + } + + var frame = StompFrameHelper.ParseFrame(Encoding.UTF8.GetBytes(e.Data)); + if (frame.HasValue) + { + _ = HandleStompFrameAsync(frame.Value, CancellationToken.None); + } + } + + private async Task HandleStompFrameAsync((string command, Dictionary headers, string body) frame, CancellationToken ct) + { + var (command, headers, body) = frame; + + switch (command.ToUpperInvariant()) + { + case "CONNECTED": + _lastServerHeartbeat = DateTime.UtcNow; + if (headers.TryGetValue("heart-beat", out var heartBeat)) + { + var parts = heartBeat.Split(','); + if (parts.Length == 2 && + int.TryParse(parts[0], out var serverSendInterval) && + int.TryParse(parts[1], out var serverReceiveInterval)) + { + _serverSendInterval = NegotiateHeartbeatInterval(RequestedHeartbeatInterval, serverSendInterval); + _clientSendInterval = NegotiateHeartbeatInterval(RequestedHeartbeatInterval, serverReceiveInterval); + StartHeartbeat(_webSocket!); + } + } + + var subscribeFrame = StompFrameHelper.BuildFrame("SUBSCRIBE", new Dictionary + { + ["id"] = "sub-0", + ["destination"] = "/user/topic/session" + }); + _webSocket?.Send(subscribeFrame); + break; + + case "MESSAGE": + try + { + using var doc = JsonDocument.Parse(body); + _userToken = doc.RootElement.GetProperty("msg").GetString(); + if (string.IsNullOrWhiteSpace(_userToken)) + { + return; + } + + await _dataCacheService.ClearUserTokenAsync(); + await _dataCacheService.StoreUserTokenAsync(_userToken); + + var user = await _aikDataService.GetUserInfo(_userToken); + if (user is null) + { + PublishState(CloneState( + isLoggedIn: false, + statusText: "登录失败,无法获取用户信息", + userStatus: UserStatus.NotLoggedIn, + userInfo: null, + integralInfo: null, + canShowAuthorizedMenus: false)); + _userToken = null; + return; + } + + await _dataCacheService.StoreUserInfoAsync(user); + var integral = await _aikDataService.GetUserIntegral(_userToken); + PublishState(CloneState( + isLoggedIn: true, + isQrCodeExpired: false, + statusText: "登录成功!", + userStatus: UserStatus.LoggedInOnline, + userInfo: user, + integralInfo: integral, + canShowAuthorizedMenus: true)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to process login MESSAGE frame."); + } + break; + + case "ERROR": + PublishState(CloneState(statusText: $"STOMP 错误: {body}")); + break; + } + } +} diff --git a/AIK.Service/Service/VehDataService.cs b/AIK.Service/Service/VehDataService.cs new file mode 100644 index 0000000..de9b548 --- /dev/null +++ b/AIK.Service/Service/VehDataService.cs @@ -0,0 +1,311 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.DataAccess; +using AIK.Models; +using AIK.Models.SQLiteModels; +using AIK.Service.IService; +using Dapper; +using System.Data.SQLite; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; + +namespace AIK.Service.Service +{ + public class VehDataService : IVehDataService + { + private readonly IDataCacheService _cacheService; + private readonly ICarfactoryService _apiService; + private readonly IConfigurationService _configurationService; + + private readonly TimeSpan _cacheExpiry = TimeSpan.FromHours(3);//默认3个小时有效期 + private readonly ILogger _logger; + + private string GetCacheKey(int id) => $"{DbCore.CacheModelKey}_{id}"; + + private string GetCacheVehModelKey(int id) => $"{DbCore.CacheVehModelKey}_{id}"; + + public VehDataService(IDataCacheService cacheService, ICarfactoryService apiService, ILogger logger, IConfigurationService configurationService) + { + _cacheService = cacheService; + _apiService = apiService; + _logger = logger; + _configurationService = configurationService; + _cacheExpiry = TimeSpan.FromHours(_configurationService.CacheSettings.DataCacheExpiryHours); + } + #region 获取品牌详情 + public async Task> GetVehDataAsync(string vhetype, bool forceRefresh = false) + { + if (forceRefresh) + { + await _cacheService.RemoveAsync(DbCore.CacheKey); + } + + var result = await _cacheService.GetOrCreateAsync>( + key: DbCore.CacheKey, + dataFetchFunc: () => GetVehDataFromApiAsync(vhetype), + expiry: _cacheExpiry + ); + + return result ?? new List(); + } + + public async Task> GetVehDataFromApiAsync(string vhetype) + { + try + { + // 调用第三方API获取数据 + var data = await _apiService.GetCarfactoryClassListAsync(vhetype); + + //// 可以在这里添加数据验证和转换逻辑 + //if (data != null && data.Count > 0) + //{ + // return data; + //} + return data; + } + catch (Exception ex) + { + // 记录日志 + //System.Diagnostics.Debug.WriteLine($"API调用失败: {ex.Message}"); + _logger.LogError("执行方法:{Method},API调用失败:{Message}", nameof(GetVehDataFromApiAsync), ex.Message); + throw new Exception($"获取品牌数据失败: {ex.Message}"); + } + } + #endregion + + #region 帮助方法 + public async Task IsCacheValidAsync() + { + return await _cacheService.ExistsAsync(DbCore.CacheKey); + } + + public async Task ClearCacheAsync() + { + await _cacheService.RemoveAsync(DbCore.CacheKey); + } + + public async Task GetCacheInfoAsync() + { + var exists = await _cacheService.ExistsAsync(DbCore.CacheKey); + if (!exists) + { + return new CacheInfo { IsValid = false }; + } + + var data = await _cacheService.GetAsync>(DbCore.CacheKey); + var stats = await _cacheService.GetCacheStatsAsync(); + var cacheItem = await GetCacheItemAsync(DbCore.CacheKey); + + return new CacheInfo + { + IsValid = true, + LastUpdateTime = cacheItem?.CreatedAt, + DataCount = data?.Count ?? 0, + CacheSize = await _cacheService.GetCacheSizeAsync(), + TimeToExpire = cacheItem?.ExpiresAt - DateTime.Now + }; + } + + private async Task GetCacheItemAsync(string key) + { + var sql = "SELECT * FROM CacheItems WHERE CacheKey = @Key"; + using var connection = new SQLiteConnection($"Data Source={SQLiteCore.DatabasePath}"); + return await connection.QueryFirstOrDefaultAsync(sql, new { Key = key }); + } + + #endregion + + #region 获取钥匙详情 + public async Task> GetVehKeyDataAsync(int id, bool forceRefresh = false) + { + var cacheKey = GetCacheKey(id); + + if (forceRefresh) + { + await _cacheService.RemoveAsync(cacheKey); + } + + var result = await _cacheService.GetOrCreateAsync>( + key: cacheKey, + dataFetchFunc: () => GetVehKeyDataFromApiAsync(id), + expiry: _cacheExpiry + ); + + return result ?? new List(); + } + + public async Task> GetVehKeyDataFromApiAsync(int id) + { + try + { + // 调用第三方API获取数据 + var data = await _apiService.GetVehKeyClassListAsync(id); + return data; + //// 可以在这里添加数据验证和转换逻辑 + //if (data != null && data.Count > 0) + //{ + // return data; + //} + + //throw new Exception("API返回数据为空"); + } + catch (Exception ex) + { + // 记录日志 + _logger.LogError("执行方法:{Method},API调用失败:{Message}", nameof(GetVehDataFromApiAsync), ex.Message); + throw new Exception($"获取数据失败: {ex.Message}"); + } + } + + public async Task>> GetBatchVehKeyDataAsync(List ids, bool forceRefresh = false) + { + var results = new Dictionary>(); + var tasks = new List(); + + foreach (var id in ids) + { + var task = GetVehKeyDataAsync(id, forceRefresh).ContinueWith(t => + { + if (t.IsCompleted) + { + results[id] = t.Result; + } + }); + tasks.Add(task); + } + + await Task.WhenAll(tasks); + return results; + } + public async Task ClearVehKeyCacheAsync(int id) + { + var cacheKey = GetCacheKey(id); + await _cacheService.RemoveAsync(cacheKey); + } + + public async Task ClearAllVehKeyCacheAsync() + { + // 获取所有车辆钥匙相关的缓存键并清除 + var cachedKeys = await GetCachedVehKeyIdsAsync(); + foreach (var id in cachedKeys) + { + await ClearVehKeyCacheAsync(id); + } + } + + public async Task> GetCachedVehKeyIdsAsync() + { + // 获取所有缓存项并解析出ID + var cacheStats = await _cacheService.GetCacheStatsAsync(); + var ids = new List(); + + foreach (var item in cacheStats) + { + if (item.CacheKey.StartsWith(DbCore.CacheModelKey)) + { + var idPart = item.CacheKey.Substring(DbCore.CacheModelKey.Length + 1); // +1 是为了去掉下划线 + if (int.TryParse(idPart, out int id)) + { + ids.Add(id); + } + } + } + + return ids; + } + public async Task> GetVehKeyCacheInfoAsync() + { + var cacheStats = await _cacheService.GetCacheStatsAsync(); + var result = new Dictionary(); + + foreach (var item in cacheStats) + { + if (item.CacheKey.StartsWith(DbCore.CacheModelKey)) + { + var idPart = item.CacheKey.Substring(DbCore.CacheModelKey.Length + 1); + if (int.TryParse(idPart, out int id)) + { + result[id] = item.LastAccessed; + } + } + } + + return result; + } + #endregion + + + public async Task GetVehKeyImgAsync(string url, bool forceRefresh = false) + { + if (forceRefresh) + { + await _cacheService.RemoveAsync(DbCore.CacheKey); + } + + var result = await _cacheService.GetOrCreateAsync( + key: DbCore.CacheKey, + dataFetchFunc: () => GetVehKeyImgDataFromApiAsync(url), + expiry: _cacheExpiry + ); + + return result ?? new BitmapImage(); + } + + public Task GetVehKeyImgDataFromApiAsync(string url) + { + try + { + // 调用第三方API获取数据 + var data = _apiService.GetVehKeyImg(url); + + // 可以在这里添加数据验证和转换逻辑 + if (data != null) + { + return data; + } + + throw new Exception("API返回数据为空"); + } + catch (Exception ex) + { + // 记录日志 + _logger.LogError("执行方法:{Method},API调用失败:{Message}", nameof(GetVehDataFromApiAsync), ex.Message); + throw new Exception($"获取数据失败: {ex.Message}"); + } + } + + #region 获取钥匙详情(拆表版) + + public async Task> GetVehKeyDataByIdAsync(int id) + { + var cacheKey = GetCacheVehModelKey(id); + + var result = new List(); + var resultCache = await _cacheService.GetSplitVehModelDataAsync(cacheKey); + + if (resultCache == null) + { + var vehKeydataList = await GetVehKeyDataFromApiAsync(id); + result.AddRange(vehKeydataList); + foreach (var item in vehKeydataList) + { + await _cacheService.SetSplitVehModelDataAsync(cacheKey, item, _cacheExpiry); + } + } + else + { + result.AddRange(resultCache); + } + return result; + } + + #endregion + } +} diff --git a/AIK.Service/Service/WindowService.cs b/AIK.Service/Service/WindowService.cs new file mode 100644 index 0000000..5a13a76 --- /dev/null +++ b/AIK.Service/Service/WindowService.cs @@ -0,0 +1,126 @@ +using System.Threading.Tasks; +using System.Threading; +using AIK.Service.Extensions; +using AIK.Service.IService; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Service.Service +{ + public class WindowService : IWindowService + { + private readonly IServiceProvider _serviceProvider; + private readonly IWindowFactory _windowFactory; + private readonly Dictionary _windowViewModelMappings; + private readonly Dictionary _windowKeyMappings; + + private class WindowMapping + { + public Type WindowType { get; set; } + public Type ViewModelType { get; set; } + } + + public WindowService(IServiceProvider serviceProvider, IWindowFactory windowFactory) + { + _serviceProvider = serviceProvider; + _windowFactory = windowFactory; + _windowViewModelMappings = new Dictionary(); + _windowKeyMappings = new Dictionary(); + + } + + + public void RegisterWindow() + where TWindow : Window + where TViewModel : class + { + _windowViewModelMappings[typeof(TViewModel)] = typeof(TWindow); + } + + // 基于字符串键的实现 + public void Show(string windowKey) + { + if (_windowKeyMappings.TryGetValue(windowKey, out var mapping)) + { + var window = _windowFactory.CreateWindow(mapping.WindowType); + window.Show(); + } + else + { + throw new ArgumentException($"未找到键为 '{windowKey}' 的窗口映射"); + } + } + + public void Show(string windowKey, TViewModel viewModel) where TViewModel : class + { + if (_windowKeyMappings.TryGetValue(windowKey, out var mapping)) + { + // 使用非泛型方法替代反射 + var window = _windowFactory.CreateWindow(mapping.WindowType, typeof(TViewModel), viewModel); + window?.Show(); + } + else + { + throw new ArgumentException($"未找到键为 '{windowKey}' 的窗口映射"); + } + } + + public void Show(string windowKey, TViewModel viewModel, Action initializeAction) where TViewModel : class + { + if (_windowKeyMappings.TryGetValue(windowKey, out var mapping)) + { + // 使用非泛型方法替代反射 + var window = _windowFactory.CreateWindow(mapping.WindowType, typeof(TViewModel), viewModel); + initializeAction?.Invoke(window, viewModel); + window?.Show(); + } + else + { + throw new ArgumentException($"未找到键为 '{windowKey}' 的窗口映射"); + } + } + + public bool? ShowDialog(string windowKey) + { + if (_windowKeyMappings.TryGetValue(windowKey, out var mapping)) + { + var window = _windowFactory.CreateWindow(mapping.WindowType); + return window.ShowDialog(); + } + else + { + throw new ArgumentException($"未找到键为 '{windowKey}' 的窗口映射"); + } + } + + public bool? ShowDialog(string windowKey, TViewModel viewModel) where TViewModel : class + { + if (_windowKeyMappings.TryGetValue(windowKey, out var mapping)) + { + var method = typeof(IWindowFactory).GetMethod(nameof(IWindowFactory.CreateWindow)) + ?.MakeGenericMethod(mapping.WindowType, typeof(TViewModel)); + + var window = method?.Invoke(_windowFactory, new object[] { viewModel }) as Window; + return window?.ShowDialog(); + } + else + { + throw new ArgumentException($"未找到键为 '{windowKey}' 的窗口映射"); + } + } + + public void RegisterWindow(string windowKey, Type windowType, Type viewModelType = null) + { + _windowKeyMappings[windowKey] = new WindowMapping + { + WindowType = windowType, + ViewModelType = viewModelType + }; + } + } +} diff --git a/AIK.Views/AIK.Views.csproj b/AIK.Views/AIK.Views.csproj new file mode 100644 index 0000000..9c6954e --- /dev/null +++ b/AIK.Views/AIK.Views.csproj @@ -0,0 +1,44 @@ + + + + net48 + enable + true + 12 + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AIK.Views/AccountPages/AccountLoginWindow.xaml b/AIK.Views/AccountPages/AccountLoginWindow.xaml new file mode 100644 index 0000000..4157c6c --- /dev/null +++ b/AIK.Views/AccountPages/AccountLoginWindow.xaml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/AccountPages/MainWindow.xaml.cs b/AIK.Views/AccountPages/MainWindow.xaml.cs new file mode 100644 index 0000000..f880f33 --- /dev/null +++ b/AIK.Views/AccountPages/MainWindow.xaml.cs @@ -0,0 +1,190 @@ +using AIK.Common.Language; +using AIK.ViewModels; +using AIK.Views.MenuPages.CommonPage; +using AIK.Views.Utils; +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace AIK.Views.AccountPages +{ + /// + /// MainWindow.xaml 的交互逻辑 + /// + public partial class MainWindow : Window + { + private Storyboard _slideIn; + private Storyboard _slideOut; + public MainWindow() + { + InitializeComponent(); + this.MaxHeight = SystemParameters.PrimaryScreenHeight;//防止最大化时系统任务栏被遮盖 + DataContext = Ioc.Default.GetService(); + // 加载动画 + _slideIn = (Storyboard)FindResource("SlideInStoryboard"); + _slideOut = (Storyboard)FindResource("SlideOutStoryboard"); + + // Esc 关闭面板 + this.KeyDown += (s, e) => + { + if (e.Key == Key.Escape && UserPanelContainer.Visibility == Visibility.Visible) + { + _slideOut.Begin(); + e.Handled = true; + } + }; + } + private void UserAvatarButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (UserPanelContainer.Visibility == Visibility.Visible) + _slideOut.Begin(); + else + _slideIn.Begin(); + + e.Handled = true; + } + private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + // 如果面板未显示,直接返回 + if (UserPanelContainer.Visibility != Visibility.Visible) + return; + + // 获取点击的原始元素 + var clickedElement = e.OriginalSource as DependencyObject; + + // 检查是否点击在 UserPanelContainer 或其子元素上 + if (IsAncestorOrSelf(UserPanelContainer, clickedElement)) + { + // 点击在面板内部 → 不关闭 + return; + } + + // 点击在外部 → 关闭面板 + _slideOut.Begin(); + e.Handled = true; + } + private bool IsAncestorOrSelf(DependencyObject target, DependencyObject element) + { + try + { + while (element != null) + { + if (element == target) + return true; + element = VisualTreeHelper.GetParent(element); + } + return false; + } + catch (Exception ex) + { + //忽略异常,防止因视觉树问题导致崩溃 + return false; + } + + } + protected override void OnClosed(EventArgs e) + { + // 清理数据上下文 + this.DataContext = null; + + base.OnClosed(e); + } + private void Button_Click(object sender, RoutedEventArgs e) + { + Application.Current.Shutdown(); + } + + private void BtnMax_Click(object sender, RoutedEventArgs e) + { + this.WindowState = this.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + UpdateMaximizeButton(); + } + // 更新最大化按钮图标 + private void UpdateMaximizeButton() + { + if (BtnMax != null) + { + BtnMax.Content = this.WindowState switch + { + WindowState.Maximized => "\ue653", // 还原图标 + _ => "\ue651" // 最大化图标 + }; + } + } + private void BtnMin_Click(object sender, RoutedEventArgs e) + { + this.WindowState = WindowState.Minimized; + } + // 内存不足时的处理 + private void Application_Startup(object sender, StartupEventArgs e) + { + AppDomain.CurrentDomain.UnhandledException += (s, args) => + { + if (IsOutOfMemoryException(args.ExceptionObject as Exception)) + { + // 紧急内存清理 + EmergencyMemoryCleanup(); + MessageBox.Show("内存不足,已尝试清理。请保存工作并重启应用。"); + } + }; + } + + #region 按钮点击 + private void WeChatArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + string wechat = (this.FindName("TxtWeChat") as TextBlock)?.Text; + if (!string.IsNullOrEmpty(wechat)) + { + _ = SafeClipboard.SetTextAsync(wechat); + //ShowCopyTip("微信号已复制"); + LightTipWindow.ShowTip(this, AiKLanguage.LanguageKey.GetValueOrDefault("ContactWechatCopy")); + } + } + private void EmailArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + string email = (this.FindName("TxtEmail") as TextBlock)?.Text; + if (!string.IsNullOrEmpty(email)) + { + _ = SafeClipboard.SetTextAsync(email); + //ShowCopyTip("微信号已复制"); + LightTipWindow.ShowTip(this, AiKLanguage.LanguageKey.GetValueOrDefault("ContactEmailCopy")); + } + } + // 账密登录入口:在应用中心弹出账密登录窗口 + private void AccountLoginLink_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + var loginWindow = new AccountLoginWindow { Owner = this }; + loginWindow.ShowDialog(); + e.Handled = true; + } + #endregion + + + private bool IsOutOfMemoryException(Exception ex) + { + return ex is OutOfMemoryException || + (ex?.InnerException is OutOfMemoryException); + } + + private void EmergencyMemoryCleanup() + { + // 立即强制GC + GC.Collect(2, GCCollectionMode.Forced, true); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + } +} diff --git a/AIK.Views/AccountPages/NavMenu.xaml b/AIK.Views/AccountPages/NavMenu.xaml new file mode 100644 index 0000000..7db6e63 --- /dev/null +++ b/AIK.Views/AccountPages/NavMenu.xaml @@ -0,0 +1,226 @@ + + + + + + + + + #3B82F6 + #F1F5F9 + #1E293B + #64748B + #F8FAFC + #FFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/AccountPages/NavMenu.xaml.cs b/AIK.Views/AccountPages/NavMenu.xaml.cs new file mode 100644 index 0000000..9621f20 --- /dev/null +++ b/AIK.Views/AccountPages/NavMenu.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.AccountPages +{ + /// + /// NavMenu.xaml 的交互逻辑 + /// + public partial class NavMenu : UserControl + { + public NavMenu() + { + InitializeComponent(); + } + } +} diff --git a/AIK.Views/AccountPages/logo.ico b/AIK.Views/AccountPages/logo.ico new file mode 100644 index 0000000..58bb011 Binary files /dev/null and b/AIK.Views/AccountPages/logo.ico differ diff --git a/AIK.Views/Converters/BoolToExpandedStyleConverter.cs b/AIK.Views/Converters/BoolToExpandedStyleConverter.cs new file mode 100644 index 0000000..301bb26 --- /dev/null +++ b/AIK.Views/Converters/BoolToExpandedStyleConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class BoolToExpandedStyleConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isExpanded && isExpanded) + { + return Application.Current.FindResource("ExpandedParentMenuButtonStyle"); + } + return Application.Current.FindResource("ParentMenuButtonStyle"); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/BoolToSelectedStyleConverter.cs b/AIK.Views/Converters/BoolToSelectedStyleConverter.cs new file mode 100644 index 0000000..4cd7605 --- /dev/null +++ b/AIK.Views/Converters/BoolToSelectedStyleConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class BoolToSelectedStyleConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isSelected && isSelected) + { + return Application.Current.FindResource("SelectedChildMenuButtonStyle"); + } + return Application.Current.FindResource("ChildMenuButtonStyle"); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/BoolToStringConverter.cs b/AIK.Views/Converters/BoolToStringConverter.cs new file mode 100644 index 0000000..204cdb1 --- /dev/null +++ b/AIK.Views/Converters/BoolToStringConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class BoolToStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is string param && param.Split(';') is string[] parts && parts.Length == 2) + { + return (bool)value ? parts[0] : parts[1]; + } + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/AIK.Views/Converters/BoolToVisibilityConverter.cs b/AIK.Views/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..0211bab --- /dev/null +++ b/AIK.Views/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class BoolToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + bool invert = parameter?.ToString()?.Equals("inverse", StringComparison.OrdinalIgnoreCase) == true; + bool result = invert ? !boolValue : boolValue; + return result ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is Visibility visibility && visibility == Visibility.Visible; + } + } +} diff --git a/AIK.Views/Converters/BoolToVisibilityInverseConverter.cs b/AIK.Views/Converters/BoolToVisibilityInverseConverter.cs new file mode 100644 index 0000000..56079c9 --- /dev/null +++ b/AIK.Views/Converters/BoolToVisibilityInverseConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class BoolToVisibilityInverseConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? Visibility.Collapsed : Visibility.Visible; + } + return Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/ByteArrayToImageSourceConverter.cs b/AIK.Views/Converters/ByteArrayToImageSourceConverter.cs new file mode 100644 index 0000000..ae6f137 --- /dev/null +++ b/AIK.Views/Converters/ByteArrayToImageSourceConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace AIK.Views.Converters +{ + public class ByteArrayToImageSourceConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not byte[] bytes || bytes.Length == 0) + return null; + + try + { + using var stream = new MemoryStream(bytes); + var image = new BitmapImage(); + image.BeginInit(); + image.StreamSource = stream; + image.CacheOption = BitmapCacheOption.OnLoad; // 避免流关闭后图像失效 + image.EndInit(); + image.Freeze(); // 允许跨线程使用(可选但推荐) + return image; + } + catch + { + return null; // 或返回默认占位图 + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/DoubleToDiameterConverter.cs b/AIK.Views/Converters/DoubleToDiameterConverter.cs new file mode 100644 index 0000000..2b79ba5 --- /dev/null +++ b/AIK.Views/Converters/DoubleToDiameterConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class DoubleToDiameterConverter : IValueConverter + { + public static DoubleToDiameterConverter Instance { get; } = new DoubleToDiameterConverter(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double radius) + { + return radius * 2; // 半径转直径 + } + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/ExpandArrowConverter.cs b/AIK.Views/Converters/ExpandArrowConverter.cs new file mode 100644 index 0000000..285a0c1 --- /dev/null +++ b/AIK.Views/Converters/ExpandArrowConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class ExpandArrowConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is bool isExpanded && isExpanded ? 90 : 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/ProgressToGlowVisibilityConverter.cs b/AIK.Views/Converters/ProgressToGlowVisibilityConverter.cs new file mode 100644 index 0000000..a6c550f --- /dev/null +++ b/AIK.Views/Converters/ProgressToGlowVisibilityConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; + +namespace AIK.Views.Converters +{ + public class ProgressToGlowVisibilityConverter : IValueConverter + { + /// + /// 转换逻辑:进度>0且<100 → 显示发光条;否则隐藏 + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + // 1. 校验进度值类型 + if (!(value is double progressValue)) + { + return Visibility.Collapsed; + } + + // 2. 校验是否为ProgressBar(可选:处理Indeterminate状态) + if (parameter is ProgressBar progressBar && progressBar.IsIndeterminate) + { + return Visibility.Collapsed; + } + + // 3. 核心逻辑:进度在1~99之间显示发光条 + return (progressValue > 0 && progressValue < 100) + ? Visibility.Visible + : Visibility.Collapsed; + } + + /// + /// 反向转换(无需实现) + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException("无需反向转换"); + } + } +} diff --git a/AIK.Views/Converters/SelectedToBackgroundConverter.cs b/AIK.Views/Converters/SelectedToBackgroundConverter.cs new file mode 100644 index 0000000..c009215 --- /dev/null +++ b/AIK.Views/Converters/SelectedToBackgroundConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using System.Windows.Media; + +namespace AIK.Views.Converters +{ + public class SelectedToBackgroundConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? + new SolidColorBrush(Color.FromArgb(255, 227, 242, 253)) : // 选中状态颜色 + Brushes.Transparent; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AIK.Views/Converters/UserStatusToColorConverter.cs b/AIK.Views/Converters/UserStatusToColorConverter.cs new file mode 100644 index 0000000..75b7e89 --- /dev/null +++ b/AIK.Views/Converters/UserStatusToColorConverter.cs @@ -0,0 +1,29 @@ +using AIK.Common.Enum; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using System.Windows.Media; + +namespace AIK.Views.Converters +{ + public class UserStatusToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value switch + { + UserStatus.LoggedInOnline => new SolidColorBrush(Color.FromRgb(76, 175, 80)), // #4CAF50 + UserStatus.LoggedInOffline => new SolidColorBrush(Color.FromRgb(255, 167, 38)), // #FFA726 + UserStatus.NotLoggedIn => new SolidColorBrush(Color.FromRgb(158, 158, 158)), // #9E9E9E + _ => new SolidColorBrush(Colors.Gray) + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); + } +} diff --git a/AIK.Views/DictionaryExtensions.cs b/AIK.Views/DictionaryExtensions.cs new file mode 100644 index 0000000..a1fab9a --- /dev/null +++ b/AIK.Views/DictionaryExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace AIK.Views +{ + /// + /// .NET Framework 4.8 兼容扩展方法(替代 Dictionary.GetValueOrDefault,该 API 在 net48 中不存在) + /// + internal static class DictionaryExtensions + { + internal static TValue? GetValueOrDefault(this Dictionary dictionary, TKey key) + { + return dictionary.TryGetValue(key, out var value) ? value : default; + } + } +} \ No newline at end of file diff --git a/AIK.Views/MenuPages/ChipGeneration/ChipGenerationPage.xaml b/AIK.Views/MenuPages/ChipGeneration/ChipGenerationPage.xaml new file mode 100644 index 0000000..4f7db82 --- /dev/null +++ b/AIK.Views/MenuPages/ChipGeneration/ChipGenerationPage.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/MenuPages/CloudData/CloudDataPage.xaml.cs b/AIK.Views/MenuPages/CloudData/CloudDataPage.xaml.cs new file mode 100644 index 0000000..85d941c --- /dev/null +++ b/AIK.Views/MenuPages/CloudData/CloudDataPage.xaml.cs @@ -0,0 +1,31 @@ +using AIK.ViewModels.CloudData; +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.MenuPages.CloudData +{ + /// + /// CloudDataPage.xaml 的交互逻辑 + /// + public partial class CloudDataPage : UserControl + { + public CloudDataPage() + { + InitializeComponent(); + DataContext = Ioc.Default.GetRequiredService(); + } + } +} diff --git a/AIK.Views/MenuPages/CommonPage/BrandListPage.xaml b/AIK.Views/MenuPages/CommonPage/BrandListPage.xaml new file mode 100644 index 0000000..b7dd270 --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/BrandListPage.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/AIK.Views/MenuPages/CommonPage/BrandListPage.xaml.cs b/AIK.Views/MenuPages/CommonPage/BrandListPage.xaml.cs new file mode 100644 index 0000000..6a58aca --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/BrandListPage.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.MenuPages.CommonPage +{ + /// + /// BrandListPage.xaml 的交互逻辑 + /// + public partial class BrandListPage : UserControl + { + public BrandListPage() + { + InitializeComponent(); + } + } +} diff --git a/AIK.Views/MenuPages/CommonPage/DialogWindow.xaml b/AIK.Views/MenuPages/CommonPage/DialogWindow.xaml new file mode 100644 index 0000000..4f0e5bf --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/DialogWindow.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/MenuPages/CommonPage/KeyModelPage.xaml.cs b/AIK.Views/MenuPages/CommonPage/KeyModelPage.xaml.cs new file mode 100644 index 0000000..d7c4f7e --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/KeyModelPage.xaml.cs @@ -0,0 +1,41 @@ +using AIK.ViewModels.CommonViewModel; +using AIK.ViewModels.RemoteCopy; +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.MenuPages.CommonPage +{ + /// + /// KeyModelPage.xaml 的交互逻辑 + /// + public partial class KeyModelPage : UserControl + { + public KeyModelPage() + { + InitializeComponent(); + ////DataContext= Ioc.Default.GetService(); + //if (!IsInDesignMode()) + //{ + // DataContext = Ioc.Default.GetService(); + //} + } + private static bool IsInDesignMode() + { + return DesignerProperties.GetIsInDesignMode(new DependencyObject()); + } + } +} diff --git a/AIK.Views/MenuPages/CommonPage/LightTipWindow.xaml b/AIK.Views/MenuPages/CommonPage/LightTipWindow.xaml new file mode 100644 index 0000000..ce5dea1 --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/LightTipWindow.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/AIK.Views/MenuPages/CommonPage/LightTipWindow.xaml.cs b/AIK.Views/MenuPages/CommonPage/LightTipWindow.xaml.cs new file mode 100644 index 0000000..9bd8374 --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/LightTipWindow.xaml.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using System.Windows.Threading; + +namespace AIK.Views.MenuPages.CommonPage +{ + /// + /// LightTipWindow.xaml 的交互逻辑 + /// + public partial class LightTipWindow : Window + { + public static readonly DependencyProperty MessageProperty = + DependencyProperty.Register(nameof(Message), typeof(string), typeof(LightTipWindow)); + + + public string Message + { + get => (string)GetValue(MessageProperty); + set => SetValue(MessageProperty, value); + } + + + private readonly DispatcherTimer _closeTimer; + + public LightTipWindow(string message, int autoCloseMs = 2000) + { + InitializeComponent(); + DataContext = this; + Message = message; + ShowInTaskbar = false; + // 自动关闭定时器 + _closeTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(autoCloseMs) }; + _closeTimer.Tick += (s, e) => { Close(); _closeTimer.Stop(); }; + _closeTimer.Start(); + } + + // 静态调用方法(简化使用) + public static void ShowTip(Window owner, string message, int autoCloseMs = 2000) + { + var tipWindow = new LightTipWindow(message, autoCloseMs) { Owner = owner }; + tipWindow.Show(); + } + } +} diff --git a/AIK.Views/MenuPages/CommonPage/PercentProgressBar.xaml b/AIK.Views/MenuPages/CommonPage/PercentProgressBar.xaml new file mode 100644 index 0000000..3a288e6 --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/PercentProgressBar.xaml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/AIK.Views/MenuPages/CommonPage/PercentProgressBar.xaml.cs b/AIK.Views/MenuPages/CommonPage/PercentProgressBar.xaml.cs new file mode 100644 index 0000000..07e6625 --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/PercentProgressBar.xaml.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.MenuPages.CommonPage +{ + /// + /// PercentProgressBar.xaml 的交互逻辑 + /// + public partial class PercentProgressBar : UserControl + { + public static readonly DependencyProperty ValueProperty = + DependencyProperty.Register("Value", typeof(double), typeof(PercentProgressBar), + new PropertyMetadata(0.0, OnValueChanged)); + + public static readonly DependencyProperty MaximumProperty = + DependencyProperty.Register("Maximum", typeof(double), typeof(PercentProgressBar), + new PropertyMetadata(100.0, OnValueChanged)); + public double Value + { + get => (double)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + public double Maximum + { + get => (double)GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + public new Brush Foreground + { + get => (Brush)GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public new Brush Background + { + get => (Brush)GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + public PercentProgressBar() + { + InitializeComponent(); + } + private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is PercentProgressBar bar) + bar.UpdateIndicator(); + } + + private void UpdateIndicator() + { + if (Maximum <= 0 || RootGrid.ActualWidth <= 0) return; + + var percent = Value / Maximum; + Indicator.Width = RootGrid.ActualWidth * percent; + PercentText.Text = $"{percent * 100:F0}%"; + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + base.OnRenderSizeChanged(sizeInfo); + UpdateIndicator(); + } + } +} diff --git a/AIK.Views/MenuPages/CommonPage/ProgressDialogWindow.xaml b/AIK.Views/MenuPages/CommonPage/ProgressDialogWindow.xaml new file mode 100644 index 0000000..ea69790 --- /dev/null +++ b/AIK.Views/MenuPages/CommonPage/ProgressDialogWindow.xaml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/MenuPages/Home/HomePage.xaml.cs b/AIK.Views/MenuPages/Home/HomePage.xaml.cs new file mode 100644 index 0000000..90c9263 --- /dev/null +++ b/AIK.Views/MenuPages/Home/HomePage.xaml.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.MenuPages.Home +{ + /// + /// HomePage.xaml 的交互逻辑 + /// + public partial class HomePage : UserControl + { + public HomePage() + { + InitializeComponent(); + DataContext = Ioc.Default.GetService(); + } + } +} diff --git a/AIK.Views/MenuPages/IDCardCopy/AccessCardCopyPage.xaml b/AIK.Views/MenuPages/IDCardCopy/AccessCardCopyPage.xaml new file mode 100644 index 0000000..dfacd6a --- /dev/null +++ b/AIK.Views/MenuPages/IDCardCopy/AccessCardCopyPage.xaml @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/MenuPages/Integral/IntegralRechargeWindow.xaml.cs b/AIK.Views/MenuPages/Integral/IntegralRechargeWindow.xaml.cs new file mode 100644 index 0000000..4a5957d --- /dev/null +++ b/AIK.Views/MenuPages/Integral/IntegralRechargeWindow.xaml.cs @@ -0,0 +1,60 @@ +using AIK.ViewModels.Integral; +using AIK.Common.Language; +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Windows; + +namespace AIK.Views.MenuPages.Integral +{ + public partial class IntegralRechargeWindow : Window + { + private readonly IntegralRechargeViewModel _viewModel; + + public IntegralRechargeWindow() + { + InitializeComponent(); + _viewModel = Ioc.Default.GetRequiredService(); + _viewModel.PaymentCreated += OnPaymentCreated; + DataContext = _viewModel; +#if !(CN || TEST) + PaymentMethodRadioButton.Content = AiKLanguage.LanguageKey?.GetValueOrDefault("PaymentMethodPaypal") ?? "PayPal"; +#endif + } + + public void SetCurrentIntegral(int currentIntegral) + { + _viewModel.CurrentIntegral = currentIntegral; + } + + protected override void OnClosed(EventArgs e) + { + _viewModel.PaymentCreated -= OnPaymentCreated; + base.OnClosed(e); + } + + private void OnPaymentCreated(AIK.Models.ApiModels.Payment.PaymentParametersPC payment, decimal amount, byte[]? qrCodeBytes) + { +#if CN || TEST + if (!amount.Equals(_viewModel.Amount.GetValueOrDefault())) + { + return; + } + + var window = new PaymentQrCodeWindow(payment, amount, qrCodeBytes) + { + Owner = this + }; +#else + var window = new PaymentQrCodeWindow(payment, amount, null) + { + Owner = this + }; +#endif + var result = window.ShowDialog(); + if (result == true) + { + Close(); + } + } + } +} diff --git a/AIK.Views/MenuPages/Integral/IntegralRecordPage.xaml b/AIK.Views/MenuPages/Integral/IntegralRecordPage.xaml new file mode 100644 index 0000000..c377635 --- /dev/null +++ b/AIK.Views/MenuPages/Integral/IntegralRecordPage.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIK.Views/MenuPages/UpdateCenter/UpdateCenterPage.xaml.cs b/AIK.Views/MenuPages/UpdateCenter/UpdateCenterPage.xaml.cs new file mode 100644 index 0000000..f47153e --- /dev/null +++ b/AIK.Views/MenuPages/UpdateCenter/UpdateCenterPage.xaml.cs @@ -0,0 +1,31 @@ +using AIK.ViewModels.UpdateCenter; +using CommunityToolkit.Mvvm.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AIK.Views.MenuPages.UpdateCenter +{ + /// + /// UpdateCenterPage.xaml 的交互逻辑 + /// + public partial class UpdateCenterPage : UserControl + { + public UpdateCenterPage() + { + InitializeComponent(); + this.DataContext = Ioc.Default.GetService(); + } + } +} diff --git a/AIK.Views/Properties/AssemblyInfo.cs b/AIK.Views/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..037ad19 --- /dev/null +++ b/AIK.Views/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +[assembly: System.Windows.ThemeInfo(System.Windows.ResourceDictionaryLocation.None, System.Windows.ResourceDictionaryLocation.SourceAssembly)] + +// 在此类的 SDK 样式项目中,现在,在此文件中早前定义的几个程序集属性将在生成期间自动添加,并使用在项目属性中定义的值进行填充。有关包含的属性以及如何定制此过程的详细信息,请参阅 +// https://aka.ms/assembly-info-properties + + +// 将 ComVisible 设置为 false 会使此程序集中的类型对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型,请将该类型的 ComVisible +// 属性设置为 true。 + +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于 typelib 的 ID。 + +[assembly: Guid("36cb9889-2aec-4f06-80ee-aef3ab1cf8a8")] diff --git a/AIK.Views/UIServices/ModalDialogService.cs b/AIK.Views/UIServices/ModalDialogService.cs new file mode 100644 index 0000000..17ad10b --- /dev/null +++ b/AIK.Views/UIServices/ModalDialogService.cs @@ -0,0 +1,208 @@ +using System.Threading; +using AIK.Common.Enum; +using AIK.Common.IconString; +using AIK.Common.Interface; +using AIK.Common.Language; +using AIK.Views.MenuPages.CommonPage; +using AIK.Views.UIViewModels; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; + +namespace AIK.Views.UIServices +{ + public class ModalDialogService : IModalDialogService + { + private readonly IServiceProvider _serviceProvider; + + // 通过构造函数注入 IServiceProvider + public ModalDialogService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task ShowDialogAsync(string title = "请稍候", string message = "正在处理...", MessageTypeEnum messageType = MessageTypeEnum.Info, bool canCancel = true, bool isAutoClose = true, int closeTimeSpan = 3000) + { + title = title == "请稍候" ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogPleaseWait") ?? string.Empty : title; + message = message == "正在处理..." ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogProcessing") ?? string.Empty : message; + var vm = new DialogMessageViewModel(title, message, messageType, canCancel); + var window = new DialogWindow + { + DataContext = vm, + Owner = Application.Current.MainWindow // 可选:设为模态父窗 + }; + + // 启动后台任务(不在 UI 线程阻塞) + _ = Task.Run(async () => + { + try + { + + // 切回 UI 线程更新消息并关闭 + await Application.Current.Dispatcher.InvokeAsync(async () => + { + vm.Title = title; + vm.Message = message; + vm.MessageType = messageType; + if (isAutoClose) + { + var closets = TimeSpan.FromMilliseconds(closeTimeSpan).TotalSeconds; + vm.Message += $"\r\n{closets}秒自动关闭"; + await Task.Delay(closeTimeSpan); + window.Close(); + } + }); + + + } + catch (OperationCanceledException) + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + vm.Message = AiKLanguage.LanguageKey.GetValueOrDefault("DialogCancelled") ?? string.Empty; + window.Close(); + }); + } + catch (Exception ex) + { + await Application.Current.Dispatcher.InvokeAsync(async () => + { + vm.Message = $"{AiKLanguage.LanguageKey.GetValueOrDefault("DialogErrorPrefix") ?? string.Empty}: {ex.Message}"; + //await Task.Delay(1500); + window.Close(); + }); + } + }, vm.CancellationToken); + + // 显示模态窗口(阻塞调用方直到关闭) + window.ShowDialog(); + } + + public async Task ShowProgressAsync(Func, CancellationToken, Task<(bool, string)>> work, string title = "请稍候", string message = "正在烧录...", bool canCancel = true) + { + title = title == "请稍候" ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogPleaseWait") ?? string.Empty : title; + var vm = new ProgressDialogViewModel(title, message, canCancel); + var window = new ProgressDialogWindow + { + DataContext = vm, + Owner = Application.Current.MainWindow // 可选:设为模态父窗 + }; + Stopwatch stopwatch = Stopwatch.StartNew(); + var timer = new DispatcherTimer(TimeSpan.FromMilliseconds(100), DispatcherPriority.Background, (s, e) => + { + vm.TimeTotalText = stopwatch.Elapsed.ToString(@"mm\:ss\.ff"); + }, Application.Current.Dispatcher); + // 启动后台任务(不在 UI 线程阻塞) + _ = Task.Run(async () => + { + try + { + + var progress = new Progress(p => + { + // Progress 回调可能在后台线程,需调度到 UI + Application.Current.Dispatcher.Invoke(() => vm.ReportProgress(p)); + }); + var (success, resultMessage) = await work(progress, vm.CancellationToken); + // 切回 UI 线程更新消息并关闭 + await Application.Current.Dispatcher.InvokeAsync(async () => + { + vm.Message = resultMessage; + if (success) + { + var ts = stopwatch.Elapsed; + vm.TimeTotalText = $"{AiKLanguage.LanguageKey.GetValueOrDefault("DialogElapsedPrefix") ?? string.Empty}-{ts.Minutes:D2}:{ts.Seconds:D2}.{ts.Milliseconds.ToString().PadLeft(3, '0').Substring(0, 2)}"; + } + + vm.IsCompleted = true; + }); + } + catch (OperationCanceledException) + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + + vm.Message = AiKLanguage.LanguageKey.GetValueOrDefault("DialogCancelled") ?? string.Empty; + vm.IsCompleted = true; + }); + } + catch (Exception ex) + { + await Application.Current.Dispatcher.InvokeAsync(async () => + { + vm.Message = $"{AiKLanguage.LanguageKey.GetValueOrDefault("DialogErrorPrefix") ?? string.Empty}: {ex.Message}"; + vm.IsCompleted = true; + }); + } + finally + { + stopwatch.Stop(); + timer.Stop(); + } + }, vm.CancellationToken); + + // 显示模态窗口(阻塞调用方直到关闭) + window.ShowDialog(); + } + + public async Task ShowTipsDialogAsync(string message = "", MessageTypeEnum typeEnum = MessageTypeEnum.Info, bool canCancel = true, bool isAutoClose = true, int closeTime = 3000) + { + LightTipWindow.ShowTip(Application.Current.MainWindow, AiKLanguage.LanguageKey.GetValueOrDefault("UpToDate")); + } + + public async Task ShowQRCodeDialogAsync(IServiceProvider serviceProvider, string title = "请稍候", string message = "正在处理...", MessageTypeEnum messageType = MessageTypeEnum.Info, bool canCancel = true, bool isAutoClose = true, int closeTimeSpan = 3000) + { + title = title == "请稍候" ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogPleaseWait") ?? string.Empty : title; + message = message == "正在处理..." ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogProcessing") ?? string.Empty : message; + var vm = QRCodeMessageViewModel.Create( + _serviceProvider, // 👈 传入容器 + title, + message, + messageType, + canCancel + ); + var window = new QRCodeDialogWindow + { + DataContext = vm, // 手动绑定 ViewModel + Owner = Application.Current.MainWindow + }; + // 启动后台任务(不在 UI 线程阻塞) + _ = Task.Run(async () => + { + try + { + + } + catch (OperationCanceledException) + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + vm.Message = AiKLanguage.LanguageKey.GetValueOrDefault("DialogCancelled") ?? string.Empty; + window.Close(); + }); + } + catch (Exception ex) + { + await Application.Current.Dispatcher.InvokeAsync(async () => + { + vm.Message = $"{AiKLanguage.LanguageKey.GetValueOrDefault("DialogErrorPrefix") ?? string.Empty}: {ex.Message}"; + //await Task.Delay(1500); + window.Close(); + }); + } + }, vm.CancellationToken); + + // 显示模态窗口(阻塞调用方直到关闭) + window.ShowDialog(); + + } + } + +} diff --git a/AIK.Views/UIServices/QRCodeService.cs b/AIK.Views/UIServices/QRCodeService.cs new file mode 100644 index 0000000..fc176c9 --- /dev/null +++ b/AIK.Views/UIServices/QRCodeService.cs @@ -0,0 +1,54 @@ +using AIK.Common.Interface; +using QRCoder; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace AIK.Views.UIServices +{ + public class QRCodeService : IQRCodeService + { + public byte[] GenerateQRCodeWithLogo(string text, string logoPath, int pixelsPerModule = 10) + { + using var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode(text, QRCodeGenerator.ECCLevel.H); + using var qrCode = new QRCode(qrCodeData); + + using var logo = LoadLogo(logoPath); + using var bitmap = qrCode.GetGraphic( + pixelsPerModule: pixelsPerModule, + darkColor: Color.Black, + lightColor: Color.White, + drawQuietZones: true, + icon: logo, + iconSizePercent: 15 + ); + + // 👇 关键:返回 PNG 字节数组 + using var memory = new MemoryStream(); + bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Png); + return memory.ToArray(); + } + + private static Bitmap LoadLogo(string logoPath) + { + if (logoPath.StartsWith("AIK.Assets.Images.")) + { + var assetsAssembly = Assembly.Load("AIK.Assets"); + using var stream = assetsAssembly.GetManifestResourceStream(logoPath); + return new Bitmap(stream); + } + else + { + return new Bitmap(logoPath); + } + } + } +} diff --git a/AIK.Views/UIViewModels/DialogMessageViewModel.cs b/AIK.Views/UIViewModels/DialogMessageViewModel.cs new file mode 100644 index 0000000..ee2ea79 --- /dev/null +++ b/AIK.Views/UIViewModels/DialogMessageViewModel.cs @@ -0,0 +1,59 @@ +using System.Threading; +using AIK.Common.Enum; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Views.UIViewModels +{ + public partial class DialogMessageViewModel : ObservableObject + { + private readonly CancellationTokenSource _cts = new(); + + public CancellationToken CancellationToken => _cts.Token; + [ObservableProperty] + private string _title = "请稍候"; + + [ObservableProperty] + private string _message = "处理中..."; + + [ObservableProperty] + private MessageTypeEnum _messageType; + + [ObservableProperty] + private string _iconPath = "\ue601"; + + [ObservableProperty] + private string _iconForeground = "white"; + + [ObservableProperty] + private bool _isCancelButtonVisible = true; + + public DialogMessageViewModel( + string title = "处理中", + string message = "处理中...", + MessageTypeEnum typeEnum = MessageTypeEnum.Info, + bool showCancel = true) + { + Title = title; + Message = message; + IsCancelButtonVisible = showCancel; + MessageType = typeEnum; + var (icon, color) = Common.IconString.MessageStyle.GetByType(typeEnum); + IconPath = icon; + IconForeground = color; + } + + + [RelayCommand] + private void Cancel() + { + _cts.Cancel(); + } + } +} diff --git a/AIK.Views/UIViewModels/ProgressDialogViewModel.cs b/AIK.Views/UIViewModels/ProgressDialogViewModel.cs new file mode 100644 index 0000000..4b985fa --- /dev/null +++ b/AIK.Views/UIViewModels/ProgressDialogViewModel.cs @@ -0,0 +1,78 @@ +using System.Threading; +using AIK.Common.Language; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace AIK.Views.UIViewModels +{ + /// + /// 专用于进度弹窗的 ViewModel,仅管理 UI 状态,不执行业务逻辑。 + /// + public partial class ProgressDialogViewModel : ObservableObject + { + private readonly CancellationTokenSource _cts = new(); + + public CancellationToken CancellationToken => _cts.Token; + + [ObservableProperty] + private string _title = string.Empty; + + [ObservableProperty] + private string _message = string.Empty; + + [ObservableProperty] + private double _progress = 0.0; + + [ObservableProperty] + private bool _isIndeterminate = false; + + [ObservableProperty] + private bool _isCancelButtonVisible = true; + + [ObservableProperty] + private bool _isCompleted; + + [ObservableProperty] + private double _timeSecond; + + [ObservableProperty] + private string _timeTotalText = string.Empty; + + public ProgressDialogViewModel( + string title = "正在烧录", + string message = "正在烧录...", + bool showCancel = true) + { + title = title == "正在烧录" ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogBurningTitle") ?? string.Empty : title; + message = message == "正在烧录..." ? AiKLanguage.LanguageKey.GetValueOrDefault("DialogBurningMessage") ?? string.Empty : message; + Title = title; + Message = message; + IsCancelButtonVisible = showCancel; + Progress = 0.00; + IsCompleted = false; + } + + /// + /// 由外部(如 ModalDialogService)调用,用于更新进度 + /// + public void ReportProgress(double value) + { + Progress = value; + IsIndeterminate = false; + + } + + [RelayCommand] + private void Cancel() + { + _cts.Cancel(); + } + } +} diff --git a/AIK.Views/UIViewModels/QRCodeMessageViewModel.cs b/AIK.Views/UIViewModels/QRCodeMessageViewModel.cs new file mode 100644 index 0000000..f5e380a --- /dev/null +++ b/AIK.Views/UIViewModels/QRCodeMessageViewModel.cs @@ -0,0 +1,98 @@ +using System.Threading; +using AIK.Common.Enum; +using AIK.Common.IconString; +using AIK.Common.Interface; +using AIK.Service.IService; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AIK.Views.UIViewModels +{ + public partial class QRCodeMessageViewModel : ObservableObject + { + private readonly CancellationTokenSource _cts = new(); + + public CancellationToken CancellationToken => _cts.Token; + [ObservableProperty] + private string _title = "请稍候"; + + [ObservableProperty] + private string _message = "处理中..."; + + [ObservableProperty] + private MessageTypeEnum _messageType; + + [ObservableProperty] + private string _iconPath = "\ue601"; + + [ObservableProperty] + private string _iconForeground = "white"; + + [ObservableProperty] + private bool _isCancelButtonVisible = true; + + private readonly IConfigurationService _configurationService; + private readonly IQRCodeService _qrCodeService; + [ObservableProperty] + private byte[]? _qrCodeBytes; // 👈 只暴露字节数组 + + //国内版 + private string defaultLogoPath = "AIK.Assets.Images.AIKChina.png"; + + public QRCodeMessageViewModel(IConfigurationService configurationService, IQRCodeService qRCodeService) + { + _configurationService = configurationService; + _qrCodeService = qRCodeService; + } + + public static QRCodeMessageViewModel Create( + IServiceProvider serviceProvider, + string title = "请稍候", + string message = "处理中...", + MessageTypeEnum typeEnum = MessageTypeEnum.Info, + bool showCancel = true) + { + var viewModel = ActivatorUtilities.CreateInstance(serviceProvider); + // 2. 设置业务参数 + viewModel.Title = title; + viewModel.Message = message; + viewModel.IsCancelButtonVisible = showCancel; + viewModel.MessageType = typeEnum; + + // 3. 根据类型设置图标和颜色 + var (icon, color) = Common.IconString.MessageStyle.GetByType(typeEnum); + viewModel.IconPath = icon; + viewModel.IconForeground = color; + + // 4. 根据编译指令设置 Logo 路径 +#if CN + viewModel.defaultLogoPath = "AIK.Assets.Images.AIKChina.png"; +#elif TEST + viewModel.defaultLogoPath = "AIK.Assets.Images.AIKChina.png"; +#else + viewModel.defaultLogoPath = "AIK.Assets.Images.AIKInternationl.png"; +#endif + + // 5. 加载二维码 + viewModel.LoadQrCodeImage(); + + return viewModel; + } + private void LoadQrCodeImage() + { + QrCodeBytes = _qrCodeService.GenerateQRCodeWithLogo(_configurationService.ApiSettings.QRCodeUrl, defaultLogoPath, 12); + } + + [RelayCommand] + private void Cancel() + { + _cts.Cancel(); + } + } +} diff --git a/AIK.Views/UpdatePages/UpdateWindow.xaml b/AIK.Views/UpdatePages/UpdateWindow.xaml new file mode 100644 index 0000000..846bf02 --- /dev/null +++ b/AIK.Views/UpdatePages/UpdateWindow.xaml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +