commit 295b4058be41457465984ba4472ebdab05e2f6af Author: dzc <2388224167@qq.com> Date: Sun Mar 9 18:53:36 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..faf7aba --- /dev/null +++ b/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "8495dee1fd4aacbe9de707e7581203232f591b2f" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + - platform: android + create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + - platform: ios + create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + - platform: macos + create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..01c5075 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# pj_supermarket_cloud + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..55afd91 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..31d0601 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,80 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +android { + namespace = "com.example.pj_supermarket_cloud" + compileSdk 35 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + sourceSets { + main() { + jniLibs.srcDirs = ['src/main/jniLibs'] + } + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.pj_supermarket_cloud" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + ndk { + abiFilters 'arm64-v8a' + } + } + + dataBinding { + enabled = true + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + buildToolsVersion '29.0.2' +} + +flutter { + source = "../.." +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + // rxjava + implementation 'io.reactivex.rxjava2:rxjava:2.2.9' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' + + implementation 'androidx.preference:preference:1.1.1' + + implementation 'com.google.code.gson:gson:2.10.1' + + implementation 'com.tencent.bugly:crashreport:4.1.9.3' + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.10.0' + implementation 'androidx.activity:activity:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' +} + diff --git a/android/app/libs/Msc.jar b/android/app/libs/Msc.jar new file mode 100644 index 0000000..84777a4 Binary files /dev/null and b/android/app/libs/Msc.jar differ diff --git a/android/app/libs/arcsoft_face.jar b/android/app/libs/arcsoft_face.jar new file mode 100644 index 0000000..faa72e6 Binary files /dev/null and b/android/app/libs/arcsoft_face.jar differ diff --git a/android/app/libs/arcsoft_image_util.jar b/android/app/libs/arcsoft_image_util.jar new file mode 100644 index 0000000..2260c13 Binary files /dev/null and b/android/app/libs/arcsoft_image_util.jar differ diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..f548cdb --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,27 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# -keep class com.cash.cash_register_android.** { *; } +# -keep class io.flutter.plugins.** { *; } +-keep class com.arcsoft.face.** { *; } +-keep class com.iflytek.**{*;} +-keepattributes Signature \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..eabfe36 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/database/DataBaseUtils.java b/android/app/src/main/java/io/flutter/plugins/facediscern/database/DataBaseUtils.java new file mode 100644 index 0000000..8894276 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/database/DataBaseUtils.java @@ -0,0 +1,104 @@ +package io.flutter.plugins.facediscern.database; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.flutter.Log; +import io.flutter.plugins.facediscern.tool.face.model.FaceEntity; + +public class DataBaseUtils { + + private static final String TAG = "android数据库"; + + static public List getUserFace(Context context, String queryUserId) { + List result = new ArrayList<>(); + int rowCount = 0; + try { + DatabaseHelper databaseHelper = new DatabaseHelper(context); + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + String sql = "SELECT * FROM face"; + Cursor cursor = database.rawQuery(sql, null); + if (Objects.nonNull(queryUserId) && !Objects.equals(queryUserId, "")) { + sql = sql.concat(" WHERE userId = ?"); + cursor = database.rawQuery(sql, new String[]{queryUserId}); + } + Log.i(TAG, "getUserFace,sql:" + sql + ";queryUserId:" + queryUserId + ";"); + rowCount = cursor.getCount(); + while (cursor.moveToNext()) { + int userIdIndex = cursor.getColumnIndex("userId"); + int sampleIdIndex = cursor.getColumnIndex("sampleId"); + int userNameIndex = cursor.getColumnIndex("userName"); + int pathIndex = cursor.getColumnIndex("path"); + int faceModelIndex = cursor.getColumnIndex("faceModel"); + FaceEntity item = new FaceEntity(); + if (userIdIndex != -1) { + String userId = cursor.getString(userIdIndex); + item.setUserId(userId); + } + if (sampleIdIndex != -1) { + int sampleId = cursor.getInt(sampleIdIndex); + item.setSampleId(sampleId); + item.setFaceId(sampleId); + } + if (userNameIndex != -1) { + String userName = cursor.getString(userNameIndex); + item.setUserName(userName); + } + if (pathIndex != -1) { + String faceFilePath = cursor.getString(pathIndex); + item.setFaceFilePath(faceFilePath); + } + if (faceModelIndex != -1) { + String faceModel = cursor.getString(faceModelIndex); + if (faceModel != null && !Objects.equals(faceModel, "")) { + item.setFaceModel(toByteArray(faceModel)); + } + } + result.add(item); + } + // 关闭资源 + cursor.close(); + databaseHelper.close(); + Log.i(TAG, "查询人脸数据库成功人脸总数:" + rowCount + "list长度:" + result.size() + ";"); + return result; + } catch (Exception e) { + Log.e(TAG, "查询数据库人脸错误error:" + e.getMessage(), e); + } + return result; + } + + static public Boolean updateFaceModel(Context context, FaceEntity faceEntity) { + try { + DatabaseHelper databaseHelper = new DatabaseHelper(context); + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + String sqlUpdate = "UPDATE face SET faceModel = ? WHERE sampleId = ?"; + SQLiteStatement statement = database.compileStatement(sqlUpdate); + statement.bindString(1, fromByteArray(faceEntity.getFaceModel())); + statement.bindLong(2, faceEntity.getSampleId()); + int rowsAffected = statement.executeUpdateDelete(); + Log.i(TAG, "getUserFaceupdateFaceModel,sql:" + sqlUpdate + ";sampleId:" + faceEntity.getSampleId() + ";faceModel长度:" + faceEntity.getFaceModel().length + ";res:" + rowsAffected + ";"); + return rowsAffected > 0; + } catch (Exception e) { + Log.e(TAG, "更新人脸模型失败username:" + faceEntity.getUserName() + ";id:" + faceEntity.getFaceId() + ";"); + Log.e(TAG, "更新人脸模型失败error:" + e.getMessage(), e); + } + return false; + } + + public static byte[] toByteArray(String json) { + return new Gson().fromJson(json, byte[].class); + } + + public static String fromByteArray(byte[] byteArray) { + return new Gson().toJson(byteArray); + } + +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java b/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java new file mode 100644 index 0000000..25faf11 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java @@ -0,0 +1,43 @@ +package io.flutter.plugins.facediscern.database; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Environment; +import android.util.Log; + +import java.io.File; + + +public class DatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "DatabaseHelper"; + private static final String DATABASE_NAME = "cash_register.db"; + private static final int DATABASE_VERSION = 1; + + public DatabaseHelper(Context context) { + super(context, getCustomDatabasePath(context), null, DATABASE_VERSION); + } + + private static String getCustomDatabasePath(Context context) { + File documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + File dbDir = new File(documentsDir, "pj/database"); + if (!dbDir.exists()) dbDir.mkdirs(); + return new File(dbDir, DATABASE_NAME).getAbsolutePath(); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "onCreate"); + Cursor cursor = db.query("face", null, null, null, null, null, null); + while (cursor.moveToNext()) { + int id = cursor.getColumnIndex("id"); + Log.i(TAG, "查询到人脸数据库的数据id:" + id); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onCreate(db); + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterFaceDiscernPlugin.java b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterFaceDiscernPlugin.java new file mode 100644 index 0000000..fb986c9 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterFaceDiscernPlugin.java @@ -0,0 +1,148 @@ +package io.flutter.plugins.facediscern.flutter; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.flutter.Log; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.facediscern.tool.face.constants.FaceBrodConstant; +import io.flutter.plugins.facediscern.ui.FaceDiscern; + +public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler, EventChannel.StreamHandler { + + private static final String TAG = "人脸识别插件"; + + private static final String CHANNEL_TAG = "local/face_discern"; + private static final String EVENT_TAG = "local/face_discern_event"; + + public static MethodChannel staticChannel; + + private Context context; + private MethodChannel channel; + + private EventChannel eventChannel; + + private MethodChannel.Result pendingResult; + + private FlutterFaceReceiver flutterFaceReceiver; + + private BroadcastReceiver actionsReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + List actions = intent.getIntegerArrayListExtra("actions"); + pendingResult.success(actions); + } + }; + + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + context = binding.getApplicationContext(); + channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_TAG); + staticChannel = channel; + channel.setMethodCallHandler(this); + eventChannel = new EventChannel(binding.getBinaryMessenger(), EVENT_TAG); + eventChannel.setStreamHandler(this); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + eventChannel.setStreamHandler(null); + context.unregisterReceiver(actionsReceiver); + } + + // flutter 方法的回调 + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + String method = call.method; + Log.i(TAG, "执行方法:" + method + ";参数:" + new Gson().toJson(call.arguments) + ";"); + switch (method) { + case "jumpFaceDiscern": + try { + Intent intent = new Intent(context, FaceDiscern.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + context.startActivity(intent); + result.success(true); + } catch (Exception e) { + result.success(false); + Log.i(TAG, "jumpFaceDiscern error:" + e.getMessage(), e); + } + break; + case "active": + String appId = Objects.requireNonNull(call.argument("appId")).toString(); + String activeKey = Objects.requireNonNull(call.argument("activeKey")).toString(); + String sdkKey = Objects.requireNonNull(call.argument("sdkKey")).toString(); + Boolean active = FlutterTool.active(context, appId, activeKey, sdkKey); + result.success(active); + break; + case "getConfig": + result.success(FlutterTool.getConfig()); + break; + case "registerAllStorageFace": + String filePath = Objects.requireNonNull(call.argument("filePath")).toString(); + String fileName = Objects.requireNonNull(call.argument("fileName")).toString(); + Map res = FlutterTool.registerAllStorageFace(context, filePath, fileName); + result.success(res); + break; + case "registerFaceByDatabase": + String userId = call.argument("userId"); + List errorSampleIdList = FlutterTool.registerFaceByDatabase(context, userId); + result.success(errorSampleIdList); + break; + case "getFace": + String sampleId = Objects.requireNonNull(call.argument("sampleId")).toString(); + Map face = FlutterTool.getFace(context, Integer.parseInt(sampleId)); + result.success(face); + break; + case "getFaceCount": + int faceCount = FlutterTool.getFaceCount(context); + result.success(faceCount); + break; + case "removeFace": + String removeSampleId = Objects.requireNonNull(call.argument("sampleId")).toString(); + Boolean isRemove = FlutterTool.removeFace(context, Integer.parseInt(removeSampleId)); + result.success(isRemove); + break; + } + } + + // flutter回调监听 + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + Log.e(TAG, "flutter回调监听进入"); + try { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(FaceBrodConstant.FACE_GLOBAL_INTENT); + flutterFaceReceiver = new FlutterFaceReceiver(events); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + context.registerReceiver(flutterFaceReceiver, intentFilter, Context.RECEIVER_EXPORTED); + } else { + context.registerReceiver(flutterFaceReceiver, intentFilter); + } + } catch (Exception e) { + Log.e(TAG, "flutter回调监听错误error:" + e.getMessage(), e); + } + } + + @Override + public void onCancel(Object arguments) { + context.unregisterReceiver(flutterFaceReceiver); + flutterFaceReceiver = null; + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterFaceReceiver.java b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterFaceReceiver.java new file mode 100644 index 0000000..d2404c6 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterFaceReceiver.java @@ -0,0 +1,30 @@ +package io.flutter.plugins.facediscern.flutter; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.Objects; + +import io.flutter.plugin.common.EventChannel; + +public class FlutterFaceReceiver extends BroadcastReceiver { + + private static final String TAG = "flutter回调广播"; + + private EventChannel.EventSink eventSink; + + public FlutterFaceReceiver(EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + } + + @Override + public void onReceive(Context context, Intent intent) { + String json = intent.getStringExtra("json"); + if (json == null || Objects.equals(json, "")) { + return; + } + eventSink.success(json); + } + +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterTool.java b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterTool.java new file mode 100644 index 0000000..a1e4f96 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/FlutterTool.java @@ -0,0 +1,271 @@ +package io.flutter.plugins.facediscern.flutter; + +import static com.arcsoft.face.FaceEngine.activeOnline; + +import static io.flutter.plugins.facediscern.database.DataBaseUtils.getUserFace; +import static io.flutter.plugins.facediscern.database.DataBaseUtils.updateFaceModel; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import io.flutter.Log; + +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.face.FaceFeatureInfo; +import com.arcsoft.imageutil.ArcSoftImageFormat; +import com.arcsoft.imageutil.ArcSoftImageUtil; +import com.arcsoft.imageutil.ArcSoftImageUtilError; +import com.google.gson.Gson; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import io.flutter.plugins.facediscern.flutter.config.Constants; +import io.flutter.plugins.facediscern.tool.face.model.FaceEntity; +import io.flutter.plugins.facediscern.tool.face.servcie.FaceServer; +import io.flutter.plugins.facediscern.tool.file.FileUtil; + +public class FlutterTool { + + static private final String TAG = "人脸识别工具类"; + + static private final String FACE_JSON_FILE_NAME = "face.json"; + + // 在线激活 + static public Boolean active(Context context, String appId, String activeKey, String sdkKey) { + if (Objects.isNull(appId)) { + Log.e(TAG, "请传入appId"); + return false; + } + if (Objects.isNull(activeKey)) { + Log.e(TAG, "请传入activeKey"); + return false; + } + if (Objects.isNull(sdkKey)) { + Log.e(TAG, "请传入sdkKey"); + return false; + } + Constants.APP_ID = appId; + Constants.ACTIVE_KEY = activeKey; + Constants.SDK_KEY = sdkKey; + int code = activeOnline(context, Constants.ACTIVE_KEY, Constants.APP_ID, Constants.SDK_KEY); + switch (code) { + case ErrorInfo.MOK: + Log.i(TAG, "激活成功!"); + break; + case ErrorInfo.MERR_ASF_ALREADY_ACTIVATED: + Log.i(TAG, "已激活!"); + break; + default: + Log.e(TAG, "激活失败!code:" + code); + } + boolean flag = Objects.equals(code, ErrorInfo.MOK) || Objects.equals(code, ErrorInfo.MERR_ASF_ALREADY_ACTIVATED); + Constants.IS_ACTIVE = flag; + return flag; + } + + // 获取配置 + static Map getConfig() { + HashMap config = new HashMap<>(); + config.put("appId", Constants.APP_ID); + config.put("sdkKey", Constants.SDK_KEY); + config.put("activeKey", Constants.ACTIVE_KEY); + config.put("isActive", Constants.IS_ACTIVE); + return config; + } + + + static Integer parseId(String id) { + try { + int number = Integer.parseInt(id); + return number - 10000000; + } catch (Exception e) { + return null; + } + } + + // 注册所有的本地人脸 + static public Map registerAllStorageFace(Context context, String filePath, String fileName) { + Map result = new HashMap<>(); + result.put("successCount", 0); + result.put("failedCount", 0); + result.put("registerCount", 0); + result.put("fileCount", 0); + File file = new File(filePath); + if (!file.exists()) { + return null; + } + File[] files = file.listFiles((dir1, name) -> { + String nameLowerCase = name.toLowerCase(); + return nameLowerCase.endsWith(".jpg") || nameLowerCase.endsWith(".jpeg") || nameLowerCase.endsWith(".png"); + }); + if (Objects.nonNull(fileName) && !Objects.equals(fileName, "")) { + files = Arrays.stream(files).filter(v -> Objects.equals(v.getName(), fileName)).toArray(File[]::new); + Log.i(TAG, "根据fileName查询本地人脸图片fileName:" + fileName + ";数量:" + files.length + ";"); + } + if (files == null || files.length == 0) { + return null; + } + int total = files.length; + int failed = 0; + int success = 0; + for (File v : files) { + byte[] bytes = FileUtil.fileToData(v); + if (bytes == null) { + continue; + } + String name = v.getName(); + List list = Arrays.asList(name.split("\\.")); + if (list.size() == 2) { + name = list.get(0); + } + list = Arrays.asList(name.split("_")); + if (list.size() != 3) { + continue; + } + String _userId = list.get(0); + Integer _sampleId = parseId(list.get(1)); + String _username = list.get(2); + Log.i(TAG, "注册所有的本地人脸,本地人脸解析userId:" + _userId + ";sampleId:" + _sampleId + ";username:" + _username + ";"); + if (_userId == null || _sampleId == null || _username == null || _username.isEmpty()) { + continue; + } + try { + FaceEntity entity = new FaceEntity(); + entity.setFaceId(_sampleId); + entity.setUserId(_userId); + entity.setUserName(_username); + entity.setSampleId(_sampleId); + Boolean isRegister = registerFace(context, bytes, entity); + if (isRegister) { + success++; + } else { + failed++; + } + } catch (Exception e) { + failed++; + Log.e(TAG, "注册所有的本地人脸,把人脸图片注册到内存错误图片名:" + name); + } + } + int registerCount = success + failed; + Log.i(TAG, "注册所有的本地人脸,注册成功,成功的数量:" + success + ";失败的数量:" + failed + ";尝试注册数量:" + registerCount + ";文件数量:" + total + ";"); + result.put("successCount", success); + result.put("failedCount", failed); + result.put("registerCount", registerCount); + result.put("fileCount", total); + return result; + } + + + // 注册人脸 + static public Boolean registerFace(Context context, byte[] bytes, FaceEntity entity) { + FaceServer faceServer = FaceServer.getInstance(); + faceServer.init(context); + // 类型转换 + Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + Bitmap alignedBitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true); + byte[] bgr24Data = ArcSoftImageUtil.createImageData(alignedBitmap.getWidth(), alignedBitmap.getHeight(), ArcSoftImageFormat.BGR24); + int transformCode = ArcSoftImageUtil.bitmapToImageData(alignedBitmap, bgr24Data, ArcSoftImageFormat.BGR24); + if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) { + Log.e(TAG, "注册人脸,bitmapToImageData错误code:" + transformCode); + return false; + } + byte[] faceBytes = faceServer.registerBgr24( + context, + bgr24Data, + alignedBitmap.getWidth(), + alignedBitmap.getHeight(), + String.valueOf(System.currentTimeMillis()) + ); + if (faceBytes == null) { + Log.e(TAG, "注册人脸,registerBgr24错误"); + return false; + } + entity.setFaceModel(faceBytes); + int code = faceServer.registerFaceFeatureInfoFromDb(entity); + return code == ErrorInfo.MOK; + } + + + // 获取人脸 + static public Map getFace(Context context, int sampleId) { + FaceServer faceServer = FaceServer.getInstance(); + faceServer.init(context); + FaceFeatureInfo faceFeature = faceServer.getFaceFeature(sampleId); + if (Objects.isNull(faceFeature)) return null; + Map result = new HashMap<>(); + result.put("searchId", faceFeature.getSearchId()); + result.put("faceTag", faceFeature.getFaceTag()); + result.put("featureData", faceFeature.getFeatureData()); + return result; + } + + // 获取人脸总数量 + static public int getFaceCount(Context context) { + FaceServer faceServer = FaceServer.getInstance(); + faceServer.init(context); + return faceServer.getTotalFaceCount(); + } + + // 删除人脸 -1 删除全部 + static public Boolean removeFace(Context context, int sampleId) { + FaceServer faceServer = FaceServer.getInstance(); + faceServer.init(context); + int i = faceServer.removeFace(sampleId); + return i == 0; + } + + + // 批量注册全部人脸 + static public List registerFaceByDatabase(Context context, String userId) { + List faceList = getUserFace(context, userId); + List errorSampleIdList = new ArrayList<>(); + if (faceList.isEmpty()) return errorSampleIdList; + long startTime = System.currentTimeMillis(); + // 批量查询人脸模型 + for (FaceEntity v : faceList) { + if (v.getFaceModel() != null && v.getFaceModel().length > 0) continue; + byte[] bytes = null; + try { + bytes = FileUtil.fileToData(new File(v.getFaceFilePath())); + } catch (Exception e) { + errorSampleIdList.add(v.getSampleId()); + Log.e(TAG, "registerFaceByDatabase,方法错误,file读取错误error:" + e.getMessage() + "json:" + new Gson().toJson(v), e); + continue; + } + if (bytes == null) { + errorSampleIdList.add(v.getSampleId()); + Log.e(TAG, "registerFaceByDatabase,方法错误" + v.getUserName() + "bytes is null," + "json:" + new Gson().toJson(v)); + continue; + } + Boolean isRegister = registerFace(context, bytes, v); + if (isRegister) { + updateFaceModel(context, v); + } + } + FaceServer faceServer = FaceServer.getInstance(); + faceServer.init(context); + if (errorSampleIdList.isEmpty()) { + try { + faceServer.registerFaceFeatureInfoListFromDb(faceList); + Log.d(TAG, "registerFaceByDatabase,注册全部人脸耗时:" + (System.currentTimeMillis() - startTime) + "毫秒"); + } catch (Exception e) { + faceServer.removeFace(-1); + Log.e(TAG, "registerFaceByDatabase,人脸引擎批量注册失败使用单个注册"); + } + } else { + for (FaceEntity v : faceList) { + faceServer.registerFaceFeatureInfoFromDb(v); + } + Log.d(TAG, "registerFaceByDatabase,注册单个人脸耗时:" + (System.currentTimeMillis() - startTime) + "毫秒"); + } + return errorSampleIdList; + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/config/Constants.java b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/config/Constants.java new file mode 100644 index 0000000..1c234e7 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/config/Constants.java @@ -0,0 +1,9 @@ +package io.flutter.plugins.facediscern.flutter.config; + + +public class Constants { + public static String APP_ID = "FWKhBhtgnUnVrUJXZD3SWqnGYV99AiXP4SS8aNSR6bk3"; + public static String SDK_KEY = "5SAziGjCHYPY4GdjGY8dTiBABBmuLbiTp7oWsKTcWzMb"; + public static String ACTIVE_KEY = "U571-11UF-F13U-F43A"; + public static boolean IS_ACTIVE = false; +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/DualCameraHelper.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/DualCameraHelper.java new file mode 100644 index 0000000..12cc662 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/DualCameraHelper.java @@ -0,0 +1,583 @@ +package io.flutter.plugins.facediscern.tool.camera; + +import android.graphics.ImageFormat; +import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; + +import io.flutter.plugins.facediscern.tool.camera.callback.CameraListener; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * 打开两个相机的辅助类,和{@link CameraListener}共同使用,获取nv21数据等操作 + *

+ * 由于IR摄像头和RGB摄像头的默认分辨率可能不同,为了让两者相同,该类做了以下操作: + * 1. 获取两者支持的分辨率列表到到静态变量{@link DualCameraHelper#rgbSupportedPreviewSizes}及{@link DualCameraHelper#irSupportedPreviewSizes}中, + * 2. 使用{@link DualCameraHelper#getCommonSupportedPreviewSize()}方法取分辨率的交集, + * 3. 使用{@link DualCameraHelper#getBestSupportedSize(List, Point)}取最佳分辨率使两个摄像头分辨率尽可能相同 + */ +public class DualCameraHelper implements Camera.PreviewCallback { + private static List rgbSupportedPreviewSizes; + private static List irSupportedPreviewSizes; + private static final String TAG = "CameraHelper"; + private Camera mCamera; + private int mCameraId; + private Point previewViewSize; + private View previewDisplayView; + private Camera.Size previewSize; + private Point specificPreviewSize; + private int displayOrientation = 0; + private int rotation; + private int additionalRotation; + private boolean isMirror = false; + + private Integer specificCameraId = null; + private CameraListener cameraListener; + private static final int MIN_PREVIEW_WIDTH = 720; + private static final int MIN_PREVIEW_HEIGHT = 720; + + private DualCameraHelper(Builder builder) { + previewDisplayView = builder.previewDisplayView; + specificCameraId = builder.specificCameraId; + cameraListener = builder.cameraListener; + rotation = builder.rotation; + additionalRotation = builder.additionalRotation; + previewViewSize = builder.previewViewSize; + specificPreviewSize = builder.previewSize; + if (builder.previewDisplayView instanceof TextureView) { + isMirror = builder.isMirror; + } else if (isMirror) { + throw new RuntimeException("mirror is effective only when the preview is on a textureView"); + } + } + + public void init() { + if (previewDisplayView instanceof TextureView) { + ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener); + } else if (previewDisplayView instanceof SurfaceView) { + ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback); + } + + if (isMirror) { + previewDisplayView.setScaleX(-1); + } + } + + public List getCommonSupportedPreviewSize() { + /** + * irSupportedPreviewSizes 和 rgbSupportedPreviewSizes 为null才去获取, + * 不为null就没必要获取了,而且此时有可能该camera已处于打开状态,无法打开camera + */ + if (rgbSupportedPreviewSizes == null) { + Camera rgbCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); + rgbSupportedPreviewSizes = rgbCamera.getParameters().getSupportedPreviewSizes(); + rgbCamera.release(); + } + try { + if (irSupportedPreviewSizes == null) { + Camera irCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); + irSupportedPreviewSizes = irCamera.getParameters().getSupportedPreviewSizes(); + irCamera.release(); + } + } catch (RuntimeException e) { + e.printStackTrace(); + irSupportedPreviewSizes = rgbSupportedPreviewSizes; + } + List commonPreviewSizes = new ArrayList<>(); + for (Camera.Size rgbPreviewSize : rgbSupportedPreviewSizes) { + if (rgbPreviewSize.width < MIN_PREVIEW_WIDTH || rgbPreviewSize.height < MIN_PREVIEW_HEIGHT) { + continue; + } + for (Camera.Size irPreviewSize : irSupportedPreviewSizes) { + if (irPreviewSize.width == rgbPreviewSize.width && irPreviewSize.height == rgbPreviewSize.height) { + commonPreviewSizes.add(rgbPreviewSize); + } + } + } + return commonPreviewSizes; + } + + /** + * 回传当前使用的cameraID,若当前没打开相机,回传-1 + * + * @return cameraId,失败回传-1 + */ + public int getCurrentOpenedCameraId() { + if (mCamera == null) { + return -1; + } + return mCameraId; + } + + public void start() { + synchronized (this) { + if (mCamera != null) { + return; + } + List supportedPreviewSize = getCommonSupportedPreviewSize(); + //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置 + mCameraId = Camera.getNumberOfCameras() - 1; + //若指定了相机ID且该相机存在,则打开指定的相机 + if (specificCameraId != null && specificCameraId <= mCameraId) { + mCameraId = specificCameraId; + } + //没有相机 + if (mCameraId == -1) { + if (cameraListener != null) { + cameraListener.onCameraError(new Exception("camera not found")); + } + return; + } + if (mCamera == null) { + mCamera = Camera.open(mCameraId); + } + displayOrientation = getCameraOri(rotation); + mCamera.setDisplayOrientation(displayOrientation); + try { + Camera.Parameters parameters = mCamera.getParameters(); + parameters.setPreviewFormat(ImageFormat.NV21); + //预览大小设置 + previewSize = parameters.getPreviewSize(); + if (supportedPreviewSize != null && supportedPreviewSize.size() > 0) { + previewSize = getBestSupportedSize(supportedPreviewSize, previewViewSize); + } + Log.i(TAG, "start: " + previewSize.width + "x" + previewSize.height); + parameters.setPreviewSize(previewSize.width, previewSize.height); + //对焦模式设置 + List supportedFocusModes = parameters.getSupportedFocusModes(); + if (supportedFocusModes != null && supportedFocusModes.size() > 0) { + if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); + } + } + mCamera.setParameters(parameters); + if (previewDisplayView instanceof TextureView) { + mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture()); + } else { + mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder()); + } + mCamera.setPreviewCallback(this); + mCamera.startPreview(); + if (cameraListener != null) { + cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror); + } + } catch (Exception e) { + if (cameraListener != null) { + cameraListener.onCameraError(e); + } + } + io.flutter.Log.e(TAG, "相机已打卡!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + } + } + + public void switchCameraId() { + mCameraId = 1 - mCameraId; + if (specificCameraId != null) { + specificCameraId = 1 - specificCameraId; + } + } + + private int getCameraOri(int rotation) { + int degrees = rotation * 90; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + break; + } + additionalRotation /= 90; + additionalRotation *= 90; + degrees += additionalRotation; + int result; + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(mCameraId, info); + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; + } else { + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + /** + * 停止预览 + */ + public void stop() { + synchronized (this) { + if (mCamera == null) return; + mCamera.setPreviewCallback(null); + mCamera.stopPreview(); + } + } +// public void stop() { +// synchronized (this) { +// if (mCamera == null) return; +// mCamera.setPreviewCallback(null); +// mCamera.stopPreview(); +// mCamera.release(); +// mCamera = null; +// if (cameraListener != null) { +// cameraListener.onCameraClosed(); +// } +// } +// } + + // 继续预览 + public void resumePreview() { + synchronized (this) { + if (mCamera != null) { + try { + mCamera.startPreview(); + if (cameraListener != null) { + cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror); + mCamera.setPreviewCallback(this); + } + } catch (Exception e) { + if (cameraListener != null) { + cameraListener.onCameraError(e); + } + } + } + } + } + + /** + * 释放操作 + */ + public void release() { + synchronized (this) { + stop(); + if (cameraListener != null) { + cameraListener.onCameraClosed(); + } + mCamera.release(); + mCamera = null; + previewDisplayView = null; + specificCameraId = null; + cameraListener = null; + previewViewSize = null; + specificPreviewSize = null; + previewSize = null; + } + } + + public boolean isStopped() { + synchronized (this) { + return mCamera == null; + } + } + + + /** + * 获取候选分辨率列表中最接近预览view大小的分辨率 + * + * @param sizes 支持的分辨率 + * @param previewViewSize 预览view的大小 + * @return 最接近预览view大小的分辨率 + */ + private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) { + if (sizes == null || sizes.size() == 0) { + return mCamera.getParameters().getPreviewSize(); + } + Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]); + Arrays.sort(tempSizes, new Comparator() { + @Override + public int compare(Camera.Size o1, Camera.Size o2) { + if (o1.width > o2.width) { + return -1; + } else if (o1.width == o2.width) { + return o1.height > o2.height ? -1 : 1; + } else { + return 1; + } + } + }); + sizes = Arrays.asList(tempSizes); + + Camera.Size bestSize = sizes.get(0); + float previewViewRatio; + if (previewViewSize != null) { + previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y; + } else { + previewViewRatio = (float) bestSize.width / (float) bestSize.height; + } + + if (previewViewRatio > 1) { + previewViewRatio = 1 / previewViewRatio; + } + boolean isNormalRotate = (additionalRotation % 180 == 0); + + for (Camera.Size s : sizes) { + if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) { + return s; + } + if (isNormalRotate) { + if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) { + bestSize = s; + } + } else { + if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) { + bestSize = s; + } + } + } + return bestSize; + } + + public List getSupportedPreviewSizes() { + if (mCamera == null) { + return null; + } + return mCamera.getParameters().getSupportedPreviewSizes(); + } + + public List getSupportedPictureSizes() { + if (mCamera == null) { + return null; + } + return mCamera.getParameters().getSupportedPictureSizes(); + } + + + @Override + public void onPreviewFrame(byte[] nv21, Camera camera) { + if (cameraListener != null) { + cameraListener.onPreview(nv21, camera); + } + } + + private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { +// start(); + if (mCamera != null) { + try { + mCamera.setPreviewTexture(surfaceTexture); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { + Log.i(TAG, "onSurfaceTextureSizeChanged: " + width + " " + height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + stop(); + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + + } + }; + private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { +// start(); + if (mCamera != null) { + try { + mCamera.setPreviewDisplay(holder); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + stop(); + } + }; + + public void changeDisplayOrientation(int rotation) { + if (mCamera != null) { + this.rotation = rotation; + displayOrientation = getCameraOri(rotation); + mCamera.setDisplayOrientation(displayOrientation); + if (cameraListener != null) { + cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation); + } + } + } + + public static final class Builder { + + /** + * 预览显示的view,目前仅支持surfaceView和textureView + */ + private View previewDisplayView; + + /** + * 是否镜像显示,只支持textureView + */ + private boolean isMirror; + /** + * 指定的相机ID + */ + private Integer specificCameraId; + /** + * 事件回调 + */ + private CameraListener cameraListener; + /** + * 屏幕的长宽,在选择最佳相机比例时用到 + */ + private Point previewViewSize; + /** + * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可 + */ + private int rotation; + /** + * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览 + */ + private Point previewSize; + + /** + * 额外的旋转角度(用于适配一些定制设备) + */ + private int additionalRotation; + + public Builder() { + } + + + public Builder previewOn(View val) { + if (val instanceof SurfaceView || val instanceof TextureView) { + previewDisplayView = val; + return this; + } else { + throw new RuntimeException("you must preview on a textureView or a surfaceView"); + } + } + + + public Builder isMirror(boolean val) { + isMirror = val; + return this; + } + + public Builder previewSize(Point val) { + previewSize = val; + return this; + } + + public Builder previewViewSize(Point val) { + previewViewSize = val; + return this; + } + + public Builder rotation(int val) { + rotation = val; + return this; + } + + public Builder additionalRotation(int val) { + additionalRotation = val; + return this; + } + + public Builder specificCameraId(Integer val) { + specificCameraId = val; + return this; + } + + public Builder cameraListener(CameraListener val) { + cameraListener = val; + return this; + } + + public DualCameraHelper build() { + if (previewViewSize == null) { + Log.e(TAG, "previewViewSize is null, now use default previewSize"); + } + if (cameraListener == null) { + Log.e(TAG, "cameraListener is null, callback will not be called"); + } + if (previewDisplayView == null) { + throw new RuntimeException("you must preview on a textureView or a surfaceView"); + } + return new DualCameraHelper(this); + } + } + + /** + * 根据设置的额外旋转角度旋转 + * + * @param additionalRotation 额外旋转角度 + * @return 当前显示旋转角度 + */ + public int rotateAdditional(int additionalRotation) { + this.additionalRotation = additionalRotation; + int cameraOri = getCameraOri(rotation); + if (mCamera == null) { + start(); + return cameraOri; + } + mCamera.setDisplayOrientation(cameraOri); + return cameraOri; + } + + public void setSpecificPreviewSize(Point specificPreviewSize) { + this.specificPreviewSize = specificPreviewSize; + } + + public static boolean hasDualCamera() { + return Camera.getNumberOfCameras() > 1; + } + + public static boolean canOpenDualCamera() { + Camera camera0 = null; + Camera camera1 = null; + boolean can = true; + try { + camera0 = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); + camera1 = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); + } catch (Exception e) { + can = false; + } + if (camera0 != null) { + camera0.release(); + } + if (camera1 != null) { + camera1.release(); + } + return can; + } + +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/callback/CameraListener.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/callback/CameraListener.java new file mode 100644 index 0000000..ccc2ea3 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/callback/CameraListener.java @@ -0,0 +1,40 @@ +package io.flutter.plugins.facediscern.tool.camera.callback; + +import android.hardware.Camera; + + +public interface CameraListener { + /** + * 当打开时执行 + * @param camera 相机实例 + * @param cameraId 相机ID + * @param displayOrientation 相机预览旋转角度 + * @param isMirror 是否镜像显示 + */ + void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror); + + /** + * 预览数据回调 + * @param data 预览数据 + * @param camera 相机实例 + */ + void onPreview(byte[] data, Camera camera); + + /** + * 当相机关闭时执行 + */ + void onCameraClosed(); + + /** + * 当出现异常时执行 + * @param e 相机相关异常 + */ + void onCameraError(Exception e); + + /** + * 属性变化时调用 + * @param cameraID 相机ID + * @param displayOrientation 相机旋转方向 + */ + void onCameraConfigurationChanged(int cameraID, int displayOrientation); +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/config/PreviewConfig.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/config/PreviewConfig.java new file mode 100644 index 0000000..66041c1 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/config/PreviewConfig.java @@ -0,0 +1,58 @@ +package io.flutter.plugins.facediscern.tool.camera.config; + +import android.hardware.Camera; + +public class PreviewConfig { + /** + * 默认的可见光相机ID + */ + public static final int DEFAULT_RGB_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_BACK; + /** + * 默认的红外相机ID + */ + public static final int DEFAULT_IR_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_FRONT; + + private int rgbCameraId; + private int irCameraId; + private int rgbAdditionalDisplayOrientation; + private int irAdditionalDisplayOrientation; + + public PreviewConfig(int rgbCameraId, int irCameraId, int rgbAdditionalDisplayOrientation, int irAdditionalDisplayOrientation) { + this.rgbCameraId = rgbCameraId; + this.irCameraId = irCameraId; + this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation; + this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation; + } + + public int getRgbCameraId() { + return rgbCameraId; + } + + public int getIrCameraId() { + return irCameraId; + } + + public int getRgbAdditionalDisplayOrientation() { + return rgbAdditionalDisplayOrientation; + } + + public int getIrAdditionalDisplayOrientation() { + return irAdditionalDisplayOrientation; + } + + public void setRgbCameraId(int rgbCameraId) { + this.rgbCameraId = rgbCameraId; + } + + public void setIrCameraId(int irCameraId) { + this.irCameraId = irCameraId; + } + + public void setRgbAdditionalDisplayOrientation(int rgbAdditionalDisplayOrientation) { + this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation; + } + + public void setIrAdditionalDisplayOrientation(int irAdditionalDisplayOrientation) { + this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/FaceBrodConstant.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/FaceBrodConstant.java new file mode 100644 index 0000000..92012e0 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/FaceBrodConstant.java @@ -0,0 +1,7 @@ +package io.flutter.plugins.facediscern.tool.face.constants; + +public class FaceBrodConstant { + public static String FACE_GLOBAL_INTENT = "accessibility_event"; + public static final String FACE_GLOBAL_ACTIONS = "face_global_actions"; + // public static final String FACE_GLOBAL_BROD = "face_global_brod"; +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/LivenessType.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/LivenessType.java new file mode 100644 index 0000000..90e7348 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/LivenessType.java @@ -0,0 +1,15 @@ +package io.flutter.plugins.facediscern.tool.face.constants; + +/** + * 活体检测类型 + */ +public enum LivenessType { + /** + * RGB活体检测 + */ + RGB, + /** + * 红外活体检测 + */ + IR +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RecognizeColor.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RecognizeColor.java new file mode 100644 index 0000000..b363e6d --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RecognizeColor.java @@ -0,0 +1,22 @@ +package io.flutter.plugins.facediscern.tool.face.constants; + +import android.graphics.Color; + +/** + * 识别过程中人脸框的颜色 + */ +public class RecognizeColor { + /** + * 未知情况的颜色 + */ + public static final int COLOR_UNKNOWN = Color.YELLOW; + /** + * 成功的颜色 + */ + public static final int COLOR_SUCCESS = Color.GREEN; + /** + * 失败的颜色 + */ + public static final int COLOR_FAILED = Color.YELLOW; + +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestFeatureStatus.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestFeatureStatus.java new file mode 100644 index 0000000..554061e --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestFeatureStatus.java @@ -0,0 +1,28 @@ +package io.flutter.plugins.facediscern.tool.face.constants; + +/** + * 人脸识别中可能出现的状态 + * @author + */ +public @interface RequestFeatureStatus { + /** + * 默认状态 + */ + int DEFAULT = -1; + /** + * 处理中 + */ + int SEARCHING = 0; + /** + * 识别成功 + */ + int SUCCEED = 1; + /** + * 待重试 + */ + int TO_RETRY = 2; + /** + * 识别失败 + */ + int FAILED = 3; +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestLivenessStatus.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestLivenessStatus.java new file mode 100644 index 0000000..e4aaad0 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestLivenessStatus.java @@ -0,0 +1,5 @@ +package io.flutter.plugins.facediscern.tool.face.constants; + +public class RequestLivenessStatus { + public static final int ANALYZING = 10; +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceMoveFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceMoveFilter.java new file mode 100644 index 0000000..81408ef --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceMoveFilter.java @@ -0,0 +1,95 @@ +package io.flutter.plugins.facediscern.tool.face.filter; + +import android.graphics.Rect; + + +import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter; +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * 人脸移动过滤器: + * 仅保留在{@link FaceMoveFilter#CHECK_QUEUE_SIZE}帧数内,每一帧人脸的移动大小都小于{@link FaceMoveFilter#movePixels}的人脸 + */ +public class FaceMoveFilter implements FaceRecognizeFilter { + private static final String TAG = "FaceMoveFilter"; + private Map> facePositionQueueMap = new ConcurrentHashMap<>(); + private static final int CHECK_QUEUE_SIZE = 5; + private double movePixels; + + public FaceMoveFilter(double movePixels) { + this.movePixels = movePixels; + } + + @Override + public void filter(List facePreviewInfoList) { + clearFacesNotInPreview(facePreviewInfoList); + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + LinkedBlockingDeque rectDeque = facePositionQueueMap.get(facePreviewInfo.getTrackId()); + if (rectDeque == null) { + rectDeque = new LinkedBlockingDeque<>(CHECK_QUEUE_SIZE); + facePositionQueueMap.put(facePreviewInfo.getTrackId(), rectDeque); + } + if (rectDeque.remainingCapacity() == 0) { + rectDeque.removeLast(); + } + rectDeque.push(facePreviewInfo.getFaceInfoRgb().getRect()); + + if (!facePreviewInfo.isQualityPass()) { + continue; + } + + boolean qualityPass = false; + if (rectDeque.size() == CHECK_QUEUE_SIZE) { + qualityPass = true; + Iterator iterator = rectDeque.iterator(); + Rect previous = iterator.next(); + while (iterator.hasNext()) { + Rect current = iterator.next(); + double distance = getDistance(current, previous); + previous = current; + if (distance > movePixels) { + qualityPass = false; + break; + } + } + } + facePreviewInfo.setQualityPass(qualityPass); + } + } + + private void clearFacesNotInPreview(List facePreviewInfo) { + Set trackIdSet = facePositionQueueMap.keySet(); + for (Integer trackId : trackIdSet) { + boolean contains = false; + for (FacePreviewInfo previewInfo : facePreviewInfo) { + if (previewInfo.getTrackId() == trackId) { + contains = true; + break; + } + } + if (!contains) { + facePositionQueueMap.remove(trackId); + } + } + } + + public static double getDistance(Rect first, Rect second) { + int firstX = first.centerX(); + int firstY = first.centerY(); + + int secondX = second.centerX(); + int secondY = second.centerY(); + + int distanceX = secondX - firstX; + int distanceY = secondY - firstY; + + return Math.sqrt(distanceX * distanceX + distanceY * distanceY); + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceRecognizeAreaFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceRecognizeAreaFilter.java new file mode 100644 index 0000000..62163f0 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceRecognizeAreaFilter.java @@ -0,0 +1,32 @@ +package io.flutter.plugins.facediscern.tool.face.filter; + + +import android.graphics.Rect; + +import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter; +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; + +import java.util.List; + +/** + * 人脸识别区域过滤器: + * 仅保留人脸区域在{@link FaceRecognizeAreaFilter#validArea}中的人脸。(基于View位置判断) + */ +public class FaceRecognizeAreaFilter implements FaceRecognizeFilter { + private static final String TAG = "FaceRecognizeAreaFilter"; + private Rect validArea; + + public FaceRecognizeAreaFilter(Rect validArea) { + this.validArea = validArea; + } + + @Override + public void filter(List facePreviewInfoList) { + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (!facePreviewInfo.isQualityPass()) { + continue; + } + facePreviewInfo.setQualityPass(validArea.contains(facePreviewInfo.getRgbTransformedRect())); + } + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceSizeFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceSizeFilter.java new file mode 100644 index 0000000..adceafa --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceSizeFilter.java @@ -0,0 +1,41 @@ +package io.flutter.plugins.facediscern.tool.face.filter; + +import android.graphics.Rect; + + +import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter; +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; + +import java.util.List; + +/** + * 人脸尺寸过滤器: + * 仅保留人脸宽度大于{@link FaceSizeFilter#horizontalSize},且人脸高度大于{@link FaceSizeFilter#verticalSize}的人脸。 + */ +public class FaceSizeFilter implements FaceRecognizeFilter { + private int horizontalSize; + private int verticalSize; + + private static final String TAG = "FaceSizeFilter"; + + public FaceSizeFilter(int horizontalSize, int verticalSize) { + this.horizontalSize = horizontalSize; + this.verticalSize = verticalSize; + } + + @Override + public void filter(List facePreviewInfoList) { + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (!facePreviewInfo.isQualityPass()) { + continue; + } + if (facePreviewInfo.getFaceInfoRgb() != null) { + Rect rgbRect = facePreviewInfo.getFaceInfoRgb().getRect(); + Rect irRect = facePreviewInfo.getFaceInfoIr() == null ? null : facePreviewInfo.getFaceInfoIr().getRect(); + boolean rgbRectValid = rgbRect == null || (rgbRect.width() > horizontalSize && rgbRect.height() > verticalSize); + boolean irRectValid = irRect == null || (irRect.width() > horizontalSize && irRect.height() > verticalSize); + facePreviewInfo.setQualityPass(rgbRectValid && irRectValid); + } + } + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/callback/FaceRecognizeFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/callback/FaceRecognizeFilter.java new file mode 100644 index 0000000..778e1e4 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/callback/FaceRecognizeFilter.java @@ -0,0 +1,14 @@ +package io.flutter.plugins.facediscern.tool.face.filter.callback; + + + +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; + +import java.util.List; + +/** + * 人脸识别过滤器,仅保留满足条件的人脸,(只有满足条件的人脸才进行后续的活体检测、人脸识别操作) + */ +public interface FaceRecognizeFilter { + void filter(List facePreviewInfoList); +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceHelper.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceHelper.java new file mode 100644 index 0000000..bf33e23 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceHelper.java @@ -0,0 +1,1183 @@ +package io.flutter.plugins.facediscern.tool.face.help; + +import android.graphics.Rect; +import android.hardware.Camera; +import android.util.Log; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.ImageQualitySimilar; +import com.arcsoft.face.LivenessInfo; +import com.arcsoft.face.MaskInfo; +import com.arcsoft.face.enums.ExtractType; +import com.google.gson.Gson; + +import io.flutter.plugins.facediscern.tool.face.constants.LivenessType; +import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus; +import io.flutter.plugins.facediscern.tool.face.constants.RequestLivenessStatus; +import io.flutter.plugins.facediscern.tool.face.filter.FaceMoveFilter; +import io.flutter.plugins.facediscern.tool.face.filter.FaceRecognizeAreaFilter; +import io.flutter.plugins.facediscern.tool.face.filter.FaceSizeFilter; +import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter; +import io.flutter.plugins.facediscern.tool.face.help.callback.FaceListener; +import io.flutter.plugins.facediscern.tool.face.help.callback.IDualCameraFaceInfoTransformer; +import io.flutter.plugins.facediscern.tool.face.help.callback.RecognizeCallback; +import io.flutter.plugins.facediscern.tool.face.model.CompareResult; +import io.flutter.plugins.facediscern.tool.face.model.FaceEntity; +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; +import io.flutter.plugins.facediscern.tool.face.model.RecognizeConfiguration; +import io.flutter.plugins.facediscern.tool.face.model.RecognizeInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import io.flutter.plugins.facediscern.tool.face.servcie.FaceServer; +import io.reactivex.Observable; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; + + +/** + * 人脸操作辅助类 + */ +public class FaceHelper implements FaceListener { + + private static final String TAG = "FaceHelper"; + + /** + * 识别结果的回调 + */ + private RecognizeCallback recognizeCallback; + + + /** + * 用于记录人脸识别过程信息 + */ + private ConcurrentHashMap recognizeInfoMap = new ConcurrentHashMap<>(); + + private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable(); + private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable(); + /** + * 转换方式,用于IR活体检测 + */ + private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer; + + /** + * 线程池正在处理任务 + */ + private static final int ERROR_BUSY = -1; + /** + * 特征提取引擎为空 + */ + private static final int ERROR_FR_ENGINE_IS_NULL = -2; + /** + * 活体检测引擎为空 + */ + private static final int ERROR_FL_ENGINE_IS_NULL = -3; + /** + * 人脸追踪引擎 + */ + private FaceEngine ftEngine; + /** + * 口罩检测引擎 + */ + private FaceEngine maskEngine; + /** + * 特征提取引擎 + */ + private FaceEngine frEngine; + /** + * 活体检测引擎 + */ + private FaceEngine flEngine; + + private Camera.Size previewSize; + + private List faceInfoList = new CopyOnWriteArrayList<>(); + private List maskInfoList = new CopyOnWriteArrayList<>(); + /** + * 特征提取线程池 + */ + private ExecutorService frExecutor; + /** + * 活体检测线程池 + */ + private ExecutorService flExecutor; + /** + * 特征提取线程队列 + */ + private LinkedBlockingQueue frThreadQueue; + /** + * 活体检测线程队列 + */ + private LinkedBlockingQueue flThreadQueue; + + private FaceRectTransformer rgbFaceRectTransformer; + private FaceRectTransformer irFaceRectTransformer; + /** + * 控制可识别区域(相对于View),若未设置,则是全部区域 + */ + private Rect recognizeArea = new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + + private List faceRecognizeFilterList = new ArrayList<>(); + /** + * 上次应用退出时,记录的该App检测过的人脸数了 + */ + private int trackedFaceCount = 0; + /** + * 本次打开引擎后的最大faceId + */ + private int currentMaxFaceId = 0; + + /** + * 是否只检测活体 + */ + private boolean onlyDetectLiveness; + + /** + * 识别的配置项 + */ + private RecognizeConfiguration recognizeConfiguration; + private List currentTrackIdList = new ArrayList<>(); + private List facePreviewInfoList = new ArrayList<>(); + + private Disposable timerDisposable; + + private FaceHelper(Builder builder) { + onlyDetectLiveness = builder.onlyDetectLiveness; + ftEngine = builder.ftEngine; + maskEngine = builder.maskEngine; + trackedFaceCount = builder.trackedFaceCount; + previewSize = builder.previewSize; + frEngine = builder.frEngine; + flEngine = builder.flEngine; + recognizeCallback = builder.recognizeCallback; + recognizeConfiguration = builder.recognizeConfiguration; + dualCameraFaceInfoTransformer = builder.dualCameraFaceInfoTransformer; + /* + * fr 线程队列大小 + */ + int frQueueSize = recognizeConfiguration.getMaxDetectFaces(); + if (builder.frQueueSize > 0) { + frQueueSize = builder.frQueueSize; + } else { + Log.e(TAG, "frThread num must > 0, now using default value:" + frQueueSize); + } + frThreadQueue = new LinkedBlockingQueue<>(frQueueSize); + frExecutor = new ThreadPoolExecutor(1, frQueueSize, 0, TimeUnit.MILLISECONDS, frThreadQueue, r -> { + Thread t = new Thread(r); + t.setName("frThread-" + t.getId()); + return t; + }); + + /* + * fl 线程队列大小 + */ + int flQueueSize = recognizeConfiguration.getMaxDetectFaces(); + if (builder.flQueueSize > 0) { + flQueueSize = builder.flQueueSize; + } else { + Log.e(TAG, "flThread num must > 0, now using default value:" + flQueueSize); + } + flThreadQueue = new LinkedBlockingQueue(flQueueSize); + flExecutor = new ThreadPoolExecutor(1, flQueueSize, 0, TimeUnit.MILLISECONDS, flThreadQueue, r -> { + Thread t = new Thread(r); + t.setName("flThread-" + t.getId()); + return t; + }); + if (previewSize == null) { + throw new RuntimeException("previewSize must be specified!"); + } + if (recognizeConfiguration.isEnableFaceSizeLimit()) { + // 由于目前人脸框的宽高接近一致,所以在使用时horizontalSize和verticalSize的值设置成一样 + faceRecognizeFilterList.add(new FaceSizeFilter(recognizeConfiguration.getFaceSizeLimit(), recognizeConfiguration.getFaceSizeLimit())); + } + if (recognizeConfiguration.isEnableFaceMoveLimit()) { + faceRecognizeFilterList.add(new FaceMoveFilter(recognizeConfiguration.getFaceMoveLimit())); + } + if (recognizeConfiguration.isEnableFaceAreaLimit()) { + faceRecognizeFilterList.add(new FaceRecognizeAreaFilter(recognizeArea)); + } + } + + /** + * 请求获取人脸特征数据 + * + * @param nv21 图像数据 + * @param facePreviewInfo 人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + */ + public void requestFaceFeature(byte[] nv21, FacePreviewInfo facePreviewInfo, int width, int height, int format) { + if (frEngine != null && frThreadQueue.remainingCapacity() > 0) { + frExecutor.execute(new FaceRecognizeRunnable(nv21, facePreviewInfo, width, height, format)); + } else { + onFaceFeatureInfoGet(null, facePreviewInfo.getTrackId(), ERROR_BUSY); + } + } + + /** + * 请求获取活体检测结果,需要传入活体的参数,以下参数同 + * + * @param nv21 NV21格式的图像数据 + * @param faceInfo 人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + * @param livenessType 活体检测类型 + * @param waitLock + */ + public void requestFaceLiveness(byte[] nv21, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) { + if (flEngine != null && flThreadQueue.remainingCapacity() > 0) { + flExecutor.execute(new FaceLivenessDetectRunnable(nv21, faceInfo, width, height, format, livenessType, waitLock)); + } else { + onFaceLivenessInfoGet(null, faceInfo.getTrackId(), ERROR_BUSY); + } + + } + + /** + * 释放对象 + */ + public void release() { + if (getFeatureDelayedDisposables != null) { + getFeatureDelayedDisposables.clear(); + } + if (!frExecutor.isShutdown()) { + frExecutor.shutdownNow(); + frThreadQueue.clear(); + } + if (!flExecutor.isShutdown()) { + flExecutor.shutdownNow(); + flThreadQueue.clear(); + } + if (faceInfoList != null) { + faceInfoList.clear(); + } + if (frThreadQueue != null) { + frThreadQueue.clear(); + frThreadQueue = null; + } + if (flThreadQueue != null) { + flThreadQueue.clear(); + flThreadQueue = null; + } + faceInfoList = null; + stop(); + } + + public void stop() { + if (faceInfoList != null) { + faceInfoList.clear(); + } + if (maskInfoList != null) { + maskInfoList.clear(); + } + if (recognizeInfoMap != null) { + recognizeInfoMap.clear(); + } + if (facePreviewInfoList != null) { + facePreviewInfoList.clear(); + } + } + + /** + * 处理帧数据 + * + * @param rgbNv21 可见光相机预览回传的NV21数据 + * @param irNv21 红外相机预览回传的NV21数据 + * @param doRecognize 是否进行识别 + * @return 实时人脸处理结果,封装添加了一个trackId,trackId的获取依赖于faceId,用于记录人脸序号并保存 + */ + public List onPreviewFrame(@NonNull byte[] rgbNv21, @Nullable byte[] irNv21, boolean doRecognize) { + if (ftEngine != null) { + faceInfoList.clear(); + maskInfoList.clear(); + facePreviewInfoList.clear(); + int code = ftEngine.detectFaces(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList); + if (code != ErrorInfo.MOK) { + onFail(new Exception("detectFaces failed,code is " + code)); + return facePreviewInfoList; + } + if (recognizeConfiguration.isKeepMaxFace()) { + keepMaxFace(faceInfoList); + } + refreshTrackId(faceInfoList); + if (faceInfoList.isEmpty()) { + return facePreviewInfoList; + } + if (!onlyDetectLiveness) { + code = maskEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList, + FaceEngine.ASF_MASK_DETECT); + if (code == ErrorInfo.MOK) { + code = maskEngine.getMask(maskInfoList); + if (code != ErrorInfo.MOK) { + onFail(new Exception("process getMask failed,code is " + code)); + return facePreviewInfoList; + } + } else { + onFail(new Exception("process mask failed,code is " + code)); + return facePreviewInfoList; + } + } + + for (int i = 0; i < faceInfoList.size(); i++) { + FacePreviewInfo facePreviewInfo = new FacePreviewInfo(faceInfoList.get(i), currentTrackIdList.get(i)); + if (!maskInfoList.isEmpty()) { + MaskInfo maskInfo = maskInfoList.get(i); + facePreviewInfo.setMask(maskInfo.getMask()); + } + if (rgbFaceRectTransformer != null && recognizeArea != null) { + Rect rect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getRect()); + Rect foreRect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getForeheadRect()); + facePreviewInfo.setRgbTransformedRect(rect); + facePreviewInfo.setForeRect(foreRect); + } + if (irFaceRectTransformer != null) { + FaceInfo faceInfo = faceInfoList.get(i); + if (dualCameraFaceInfoTransformer != null) { + faceInfo = dualCameraFaceInfoTransformer.transformFaceInfo(faceInfo); + } + facePreviewInfo.setFaceInfoIr(faceInfo); + facePreviewInfo.setIrTransformedRect(irFaceRectTransformer.adjustRect(faceInfo.getRect())); + } + facePreviewInfoList.add(facePreviewInfo); + } + clearLeftFace(facePreviewInfoList); + if (doRecognize) { + doRecognize(rgbNv21, irNv21, facePreviewInfoList); + } + } else { + facePreviewInfoList.clear(); + } + return facePreviewInfoList; + } + + public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) { + this.rgbFaceRectTransformer = rgbFaceRectTransformer; + } + + public void setIrFaceRectTransformer(FaceRectTransformer irFaceRectTransformer) { + this.irFaceRectTransformer = irFaceRectTransformer; + } + + /** + * 删除已经离开的人脸 + * + * @param facePreviewInfoList 人脸和trackId列表 + */ + private void clearLeftFace(List facePreviewInfoList) { + if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) { + if (getFeatureDelayedDisposables != null) { + getFeatureDelayedDisposables.clear(); + } + } + Enumeration keys = recognizeInfoMap.keys(); + while (keys.hasMoreElements()) { + int key = keys.nextElement(); + boolean contained = false; + for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) { + if (facePreviewInfo.getTrackId() == key) { + contained = true; + break; + } + } + if (!contained) { + RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key); + if (recognizeInfo != null) { + recognizeCallback.onNoticeChanged(""); + // 人脸离开时,通知特征提取线程,避免一直等待活体结果 + synchronized (recognizeInfo.getWaitLock()) { + recognizeInfo.getWaitLock().notifyAll(); + } + } + } + } + } + + private void doRecognize(byte[] rgbNv21, byte[] irNv21, List facePreviewInfoList) { + if (facePreviewInfoList != null && !facePreviewInfoList.isEmpty() && previewSize != null) { + for (FaceRecognizeFilter faceRecognizeFilter : faceRecognizeFilterList) { + faceRecognizeFilter.filter(facePreviewInfoList); + } + for (int i = 0; i < facePreviewInfoList.size(); i++) { + FacePreviewInfo facePreviewInfo = facePreviewInfoList.get(i); + if (!facePreviewInfo.isQualityPass()) { + continue; + } + //跳过mask值为MaskInfo.UNKNOWN的人脸 + if (!onlyDetectLiveness && facePreviewInfo.getMask() == MaskInfo.UNKNOWN) { + continue; + } + RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, facePreviewInfo.getTrackId()); + int status = recognizeInfo.getRecognizeStatus(); + /* + * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测 + */ + if (recognizeConfiguration.isEnableLiveness() && status != RequestFeatureStatus.SUCCEED) { + int liveness = recognizeInfo.getLiveness(); + if (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING + || status == RequestFeatureStatus.FAILED) { + changeLiveness(facePreviewInfo.getTrackId(), RequestLivenessStatus.ANALYZING); + requestFaceLiveness( + irNv21 == null ? rgbNv21 : irNv21, + facePreviewInfo, + previewSize.width, + previewSize.height, + FaceEngine.CP_PAF_NV21, + irNv21 == null ? LivenessType.RGB : LivenessType.IR, + recognizeInfo.getWaitLock() + ); + } + } + /* + * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数), + * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传 + */ + if (status == RequestFeatureStatus.TO_RETRY) { + changeRecognizeStatus(facePreviewInfo.getTrackId(), RequestFeatureStatus.SEARCHING); + requestFaceFeature( + rgbNv21, facePreviewInfo, + previewSize.width, + previewSize.height, + FaceEngine.CP_PAF_NV21 + ); + } + } + } + } + + @Override + public void onFail(Exception e) { + Log.e(TAG, "onFail:" + e.getMessage()); + } + + /** + * 获取识别信息,识别信息为空则创建一个新的 + * + * @param recognizeInfoMap 存放识别信息的map + * @param trackId 人脸唯一标识 + * @return 识别信息 + */ + public RecognizeInfo getRecognizeInfo(Map recognizeInfoMap, int trackId) { + RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId); + if (recognizeInfo == null) { + recognizeInfo = new RecognizeInfo(); + recognizeInfoMap.put(trackId, recognizeInfo); + } + return recognizeInfo; + } + + @Override + public void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode) { + //FR成功 + RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId); + if (faceFeature != null) { + // 人脸已离开,不用处理 + if (recognizeInfo == null) { + return; + } + //不做活体检测的情况,直接搜索 + if (!recognizeConfiguration.isEnableLiveness()) { + searchFace(faceFeature, trackId); + } + //活体检测通过,搜索特征 + else if (recognizeInfo.getLiveness() == LivenessInfo.ALIVE) { + searchFace(faceFeature, trackId); + } + //活体检测未出结果,或者非活体,等待 + else { + synchronized (recognizeInfo.getWaitLock()) { + try { + recognizeInfo.getWaitLock().wait(); + if (recognizeInfoMap.containsKey(trackId)) { + onFaceFeatureInfoGet(faceFeature, trackId, errorCode); + } + } catch (InterruptedException e) { + Log.e(TAG, "onFaceFeatureInfoGet: 等待活体结果时退出界面会执行,正常现象,可注释异常代码块"); + e.printStackTrace(); + } + } + } + + } + //特征提取失败时,为了及时提示做个UI反馈,将name修改为"ExtractCode:${errorCode}",再重置状态 + else { + if (recognizeInfo.increaseAndGetExtractErrorRetryCount() > recognizeConfiguration.getExtractRetryCount()) { + // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过 + recognizeInfo.setExtractErrorRetryCount(0); + retryRecognizeDelayed(trackId); + } else { + changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY); + } + } + } + + /** + * 延迟 {@link RecognizeConfiguration#getLivenessFailedRetryInterval()}后,重新进行活体检测 + * + * @param trackId 人脸ID + */ + private void retryLivenessDetectDelayed(final Integer trackId) { + Observable.timer(recognizeConfiguration.getLivenessFailedRetryInterval(), TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + disposable = d; + delayFaceTaskCompositeDisposable.add(disposable); + } + + @Override + public void onNext(Long aLong) { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 + changeLiveness(trackId, LivenessInfo.UNKNOWN); + delayFaceTaskCompositeDisposable.remove(disposable); + } + }); + } + + /** + * 延迟 {@link RecognizeConfiguration#getRecognizeFailedRetryInterval()}后,重新进行人脸识别 + * + * @param trackId 人脸ID + */ + private void retryRecognizeDelayed(final Integer trackId) { + changeRecognizeStatus(trackId, RequestFeatureStatus.FAILED); + Observable.timer(recognizeConfiguration.getRecognizeFailedRetryInterval(), TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + disposable = d; + delayFaceTaskCompositeDisposable.add(disposable); + } + + @Override + public void onNext(Long aLong) { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测 + changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY); + delayFaceTaskCompositeDisposable.remove(disposable); + } + }); + } + + @Override + public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode) { + if (livenessInfo != null) { + int liveness = livenessInfo.getLiveness(); + Log.i(TAG, "onFaceLivenessInfoGet liveness:" + liveness); + changeLiveness(trackId, liveness); + // 非活体,重试 + if (liveness != LivenessInfo.ALIVE) { + noticeCurrentStatus("活体检测未通过"); + // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测 + retryLivenessDetectDelayed(trackId); + } + } else { + RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId); + // 连续多次活体检测失败(接口调用回传值非0),将活体检测值重置为未知,会在帧回调中重新进行活体检测 + if (recognizeInfo.increaseAndGetLivenessErrorRetryCount() > recognizeConfiguration.getLivenessRetryCount()) { + recognizeInfo.setLivenessErrorRetryCount(0); + retryLivenessDetectDelayed(trackId); + } else { + changeLiveness(trackId, LivenessInfo.UNKNOWN); + } + } + } + + private void noticeCurrentStatus(String notice) { + if (recognizeCallback != null) { + recognizeCallback.onNoticeChanged(notice); + } + if (timerDisposable != null && !timerDisposable.isDisposed()) { + timerDisposable.dispose(); + } + timerDisposable = Observable.timer(1500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aLong -> { + if (recognizeCallback != null) { + recognizeCallback.onNoticeChanged(""); + } + }); + } + + private void searchFace(final FaceFeature faceFeature, final Integer trackId) { + CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature); + if (compareResult == null || compareResult.getFaceEntity() == null) { + retryRecognizeDelayed(trackId); + return; + } + compareResult.setTrackId(trackId); + boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold(); + recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass); + if (pass) { + String msg = "识别通过"; + Log.i(TAG, "用户名字:" + compareResult.getFaceEntity().getUserName() + ";人脸id:" + compareResult.getFaceEntity().getFaceId() + ";"); + if (Objects.nonNull(compareResult.getFaceEntity())) { + FaceEntity face = compareResult.getFaceEntity(); + if (Objects.nonNull(face.getUserName()) && Objects.nonNull(face.getFaceId())) { + DecimalFormat df = new DecimalFormat("0.00"); + String formattedNumber = df.format(compareResult.getSimilar() * 100); + msg = face.getUserName() + "相似度" + formattedNumber + "%"; + } + } + setName(trackId, msg); + noticeCurrentStatus(msg); + changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED); + } else { + noticeCurrentStatus("未通过:NOT_REGISTERED"); + retryRecognizeDelayed(trackId); + } + } + + /** + * 人脸特征提取线程 + */ + public class FaceRecognizeRunnable implements Runnable { + private FaceInfo faceInfo; + private int width; + private int height; + private int format; + private Integer trackId; + private byte[] nv21Data; + private int isMask; + + /** + * 异步特征提取任务的构造函数 + * + * @param nv21Data 可见光图像数据 + * @param facePreviewInfo 人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + */ + private FaceRecognizeRunnable(byte[] nv21Data, FacePreviewInfo facePreviewInfo, int width, int height, int format) { + if (nv21Data == null) { + return; + } + this.nv21Data = nv21Data; + this.faceInfo = new FaceInfo(facePreviewInfo.getFaceInfoRgb()); + this.width = width; + this.height = height; + this.format = format; + this.trackId = facePreviewInfo.getTrackId(); + this.isMask = facePreviewInfo.getMask(); + } + + @Override + public void run() { + if (nv21Data != null) { + if (frEngine != null) { + if (recognizeConfiguration.isEnableImageQuality()) { + /* + * 开启人脸质量检测 + */ + ImageQualitySimilar qualitySimilar = new ImageQualitySimilar(); + int iqCode; + long iqStartTime = System.currentTimeMillis(); + synchronized (frEngine) { + iqCode = frEngine.imageQualityDetect(nv21Data, width, height, format, faceInfo, isMask, qualitySimilar); + } + Log.i(TAG, "fr iqTime:" + (System.currentTimeMillis() - iqStartTime) + "ms" + ", mask=" + isMask); + if (iqCode == ErrorInfo.MOK) { + float quality = qualitySimilar.getScore(); + float destQuality = isMask == MaskInfo.WORN ? recognizeConfiguration.getImageQualityMaskRecognizeThreshold() : + recognizeConfiguration.getImageQualityNoMaskRecognizeThreshold(); + if (quality >= destQuality) { + extractFace(); + } else { + onFaceFail(iqCode, "fr imageQualityDetect score invalid"); + } + } else { + onFaceFail(iqCode, "fr imageQuality failed errorCode is " + iqCode); + } + } else { + extractFace(); + } + } else { + onFaceFail(ERROR_FR_ENGINE_IS_NULL, "fr failed ,frEngine is null"); + } + } + nv21Data = null; + } + + /** + * 对人脸图像进行特征提取 + */ + private void extractFace() { + long irStartTime = System.currentTimeMillis(); + FaceFeature faceFeature = new FaceFeature(); + int frCode; + synchronized (frEngine) { + /* + * 该场景为识别场景,所以参数“ExtractType”值为ExtractType.RECOGNIZE,且参数“mask”值为实际检测到的值,即isMask + */ + frCode = frEngine.extractFaceFeature(nv21Data, width, height, format, faceInfo, ExtractType.RECOGNIZE, isMask, faceFeature); + } + Log.i(TAG, "frTime:" + (System.currentTimeMillis() - irStartTime) + "ms"); + if (frCode == ErrorInfo.MOK) { + onFaceFeatureInfoGet(faceFeature, trackId, frCode); + } else { + onFaceFail(frCode, "fr failed errorCode is " + frCode); + } + } + + private void onFaceFail(int code, String errorMsg) { + onFaceFeatureInfoGet(null, trackId, code); + onFail(new Exception(errorMsg)); + } + } + + /** + * 活体检测的线程 + */ + public class FaceLivenessDetectRunnable implements Runnable { + private FaceInfo faceInfo; + private int width; + private int height; + private int format; + private Integer trackId; + private byte[] nv21Data; + private LivenessType livenessType; + private Object waitLock; + + /** + * 异步活体任务的构造函数 + * + * @param nv21Data 可见光或红外图像数据 + * @param faceInfo 可见光人脸检测得到的人脸信息 + * @param width 图像宽度 + * @param height 图像高度 + * @param format 图像格式 + * @param livenessType 活体检测类型,可以是可见光活体检测{@link LivenessType#RGB}或红外活体检测{@link LivenessType#IR} + * @param waitLock 活体检测通过后,调用该对象的notifyAll函数,通知识别线程活体已通过 + */ + private FaceLivenessDetectRunnable(byte[] nv21Data, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) { + if (nv21Data == null) { + return; + } + this.nv21Data = nv21Data; + this.faceInfo = new FaceInfo(faceInfo.getFaceInfoRgb()); + this.width = width; + this.height = height; + this.format = format; + this.trackId = faceInfo.getTrackId(); + this.livenessType = livenessType; + this.waitLock = waitLock; + } + + @Override + public void run() { + if (nv21Data != null) { + if (flEngine != null) { + processLiveness(); + } else { + onProcessFail(ERROR_FL_ENGINE_IS_NULL, "fl failed ,frEngine is null"); + } + } + nv21Data = null; + } + + /** + * 执行活体检测 + */ + private void processLiveness() { + List livenessInfoList = new ArrayList<>(); + int flCode = -1; + synchronized (flEngine) { + long flStartTime = System.currentTimeMillis(); + if (livenessType == LivenessType.RGB) { + // RGB活体检测 + flCode = flEngine.process(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_LIVENESS); + } else { + // IR活体检测 + FaceInfo rgbFaceInfo = faceInfo; + List irFaceInfoList = new ArrayList<>(); + int fdCode = flEngine.detectFaces(nv21Data, width, height, format, irFaceInfoList); + boolean isFaceOverlap = false; + if (fdCode == ErrorInfo.MOK && irFaceInfoList.size() > 0) { + keepMaxFace(irFaceInfoList); + isFaceOverlap = isFaceSame(rgbFaceInfo, irFaceInfoList.get(0), 0.3); + Log.i(TAG, "isFaceOverlap:" + isFaceOverlap); + } + if (fdCode == ErrorInfo.MOK && isFaceOverlap) { + flCode = flEngine.processIr(nv21Data, width, height, format, irFaceInfoList, FaceEngine.ASF_IR_LIVENESS); + } else { + onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ", flCode:" + flCode + ",isFaceOverlap:" + isFaceOverlap)); + } + } + Log.i(TAG, "flTime:" + (System.currentTimeMillis() - flStartTime) + "ms"); + } + if (flCode == ErrorInfo.MOK) { + if (livenessType == LivenessType.RGB) { + flCode = flEngine.getLiveness(livenessInfoList); + } else { + flCode = flEngine.getIrLiveness(livenessInfoList); + } + } + + if (flCode == ErrorInfo.MOK && !livenessInfoList.isEmpty()) { + onFaceLivenessInfoGet(livenessInfoList.get(0), trackId, flCode); + if (livenessInfoList.get(0).getLiveness() == LivenessInfo.ALIVE) { + synchronized (waitLock) { + waitLock.notifyAll(); + } + } + } else { + onProcessFail(flCode, "fl failed errorCode is " + flCode); + } + } + + private void onProcessFail(int code, String msg) { + onFaceLivenessInfoGet(null, trackId, code); + onFail(new Exception(msg)); + } + } + + /** + * 判断IR图片中的人脸FaceInfo和RGB图片中的人脸FaceInfo是否是同一个人脸 + * 此方案原理:判断两个FaceInfo是否有相交区域,如果有相交区域,则说明是同一个人脸 + * + * @param faceInfoList 人脸信息列表 + * @param faceInfo 人脸信息 + * @return 人脸信息列表中是否有人脸和传入的人脸信息相交 + */ + public static boolean isSameFace(List faceInfoList, FaceInfo faceInfo) { + if (faceInfoList == null || faceInfoList.isEmpty() || faceInfo == null) { + return false; + } + for (FaceInfo info : faceInfoList) { + if (Rect.intersects(faceInfo.getRect(), info.getRect())) { + return true; + } + } + return false; + } + + /** + * 计算两个人脸框的重叠面积,以判断它们的重叠程度 + * + * @param rect1 人脸框 + * @param rect2 人脸框 + * @return + */ + + public static Rect getOverlapArea(Rect rect1, Rect rect2) { + // 计算两个矩形的重叠区域 + int left = Math.max(rect1.left, rect2.left); + int right = Math.min(rect1.right, rect2.right); + int top = Math.max(rect1.top, rect2.top); + int bottom = Math.min(rect1.bottom, rect2.bottom); + + // 如果没有重叠区域,则返回空的矩形 + if (left < right && top < bottom) { + return new Rect(left, top, right, bottom); + } + return null; // 无重叠 + } + + /** + * 设置重叠阈值,当两个矩形的重叠面积超过这个阈值时,认为是同一张脸 + * + * @param rgbFaceInfo rgb人脸信息 + * @param irFaceInfo ir人脸信息 + * @param threshold 重叠阈值 + * @return Boolean + */ + + public static boolean isFaceSame(FaceInfo rgbFaceInfo, FaceInfo irFaceInfo, double threshold) { + Rect rect1 = rgbFaceInfo.getRect(); + Rect rect2 = irFaceInfo.getRect(); + Rect overlapRect = getOverlapArea(rect1, rect2); + if (overlapRect != null) { + int overlapArea = overlapRect.width() * overlapRect.height(); + int face1Area = rect1.width() * rect1.height(); + int face2Area = rect2.width() * rect2.height(); + // 计算重叠面积占两个矩形面积的比例 + double overlapRatio = (double) overlapArea / Math.min(face1Area, face2Area); + Log.d(TAG, "overlapRatio:" + overlapRatio + "," + threshold); + + return overlapRatio > threshold; + } + return false; + } + + /** + * 刷新trackId + * + * @param ftFaceList 传入的人脸列表 + */ + private void refreshTrackId(List ftFaceList) { + currentTrackIdList.clear(); + for (FaceInfo faceInfo : ftFaceList) { + currentTrackIdList.add(faceInfo.getFaceId() + trackedFaceCount); + } + if (!ftFaceList.isEmpty()) { + currentMaxFaceId = ftFaceList.get(ftFaceList.size() - 1).getFaceId(); + } + } + + /** + * 获取当前的最大trackID,可用于退出时保存 + * + * @return 当前trackId + */ + public int getTrackedFaceCount() { + // 引擎的人脸下标从0开始,因此需要+1 + return trackedFaceCount + currentMaxFaceId + 1; + } + + /** + * 新增搜索成功的人脸 + * + * @param trackId 指定的trackId + * @param name trackId对应的人脸 + */ + public void setName(int trackId, String name) { + RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId); + if (recognizeInfo != null) { + recognizeInfo.setName(name); + } + } + + + /** + * 设置转换方式,用于IR活体检测 + * + * @param transformer 转换方式 + */ + public void setDualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer transformer) { + this.dualCameraFaceInfoTransformer = transformer; + } + + + public String getName(int trackId) { + RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId); + return recognizeInfo == null ? null : recognizeInfo.getName(); + } + + + /** + * 设置可识别区域(相对于View) + * + * @param recognizeArea 可识别区域 + */ + public void setRecognizeArea(Rect recognizeArea) { + if (recognizeArea != null) { + this.recognizeArea.set(recognizeArea); + } + } + + @IntDef(value = { + RequestFeatureStatus.FAILED, + RequestFeatureStatus.SEARCHING, + RequestFeatureStatus.SUCCEED, + RequestFeatureStatus.TO_RETRY + }) + @Retention(RetentionPolicy.SOURCE) + @interface RequestFaceFeatureStatus { + } + + @IntDef(value = { + LivenessInfo.ALIVE, + LivenessInfo.NOT_ALIVE, + LivenessInfo.UNKNOWN, + LivenessInfo.FACE_NUM_MORE_THAN_ONE, + LivenessInfo.FACE_TOO_SMALL, + LivenessInfo.FACE_ANGLE_TOO_LARGE, + LivenessInfo.FACE_BEYOND_BOUNDARY, + RequestLivenessStatus.ANALYZING + }) + @Retention(RetentionPolicy.SOURCE) + @interface RequestFaceLivenessStatus { + } + + /** + * 修改人脸识别的状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @param newStatus 新的识别状态,详见{@link RequestFeatureStatus}中的定义 + */ + public void changeRecognizeStatus(int trackId, @RequestFaceFeatureStatus int newStatus) { + getRecognizeInfo(recognizeInfoMap, trackId).setRecognizeStatus(newStatus); + } + + /** + * 修改活体活体值或活体检测状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @param newLiveness 新的活体值或活体检测状态 + */ + public void changeLiveness(int trackId, @RequestFaceLivenessStatus int newLiveness) { + getRecognizeInfo(recognizeInfoMap, trackId).setLiveness(newLiveness); + } + + /** + * 获取活体值或活体检测状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @return 活体值或活体检测状态 + */ + public Integer getLiveness(int trackId) { + return getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(); + } + + /** + * 获取人脸识别状态 + * + * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识 + * @return 人脸识别状态 + */ + public Integer getRecognizeStatus(int trackId) { + return getRecognizeInfo(recognizeInfoMap, trackId).getRecognizeStatus(); + } + + /** + * 保留ftFaceList中最大的人脸 + * + * @param ftFaceList 人脸追踪时,一帧数据的人脸信息 + */ + private static void keepMaxFace(List ftFaceList) { + if (ftFaceList == null || ftFaceList.size() <= 1) { + return; + } + FaceInfo maxFaceInfo = ftFaceList.get(0); + for (FaceInfo faceInfo : ftFaceList) { + if (faceInfo.getRect().width() > maxFaceInfo.getRect().width()) { + maxFaceInfo = faceInfo; + } + } + ftFaceList.clear(); + ftFaceList.add(maxFaceInfo); + } + + + public static final class Builder { + private FaceEngine ftEngine; + private FaceEngine maskEngine; + private FaceEngine frEngine; + private FaceEngine flEngine; + private Camera.Size previewSize; + private boolean onlyDetectLiveness; + private RecognizeConfiguration recognizeConfiguration; + private RecognizeCallback recognizeCallback; + private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer; + private int frQueueSize; + private int flQueueSize; + private int trackedFaceCount; + + public Builder() { + } + + public Builder recognizeConfiguration(RecognizeConfiguration val) { + recognizeConfiguration = val; + return this; + } + + public Builder dualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer val) { + dualCameraFaceInfoTransformer = val; + return this; + } + + public Builder recognizeCallback(RecognizeCallback val) { + recognizeCallback = val; + return this; + } + + public Builder ftEngine(FaceEngine val) { + ftEngine = val; + return this; + } + + public Builder maskEngine(FaceEngine val) { + maskEngine = val; + return this; + } + + public Builder frEngine(FaceEngine val) { + frEngine = val; + return this; + } + + public Builder flEngine(FaceEngine val) { + flEngine = val; + return this; + } + + public Builder previewSize(Camera.Size val) { + previewSize = val; + return this; + } + + public Builder frQueueSize(int val) { + frQueueSize = val; + return this; + } + + public Builder flQueueSize(int val) { + flQueueSize = val; + return this; + } + + public Builder trackedFaceCount(int val) { + trackedFaceCount = val; + return this; + } + + public Builder onlyDetectLiveness(boolean val) { + onlyDetectLiveness = val; + return this; + } + + public FaceHelper build() { + return new FaceHelper(this); + } + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceRectTransformer.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceRectTransformer.java new file mode 100644 index 0000000..55bf32d --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceRectTransformer.java @@ -0,0 +1,222 @@ +package io.flutter.plugins.facediscern.tool.face.help; + +import android.graphics.Rect; +import android.hardware.Camera; + +/** + * 将检测回传的人脸框(基于NV21数据)转换为View绘制(基于View)所需的人脸框 + */ +public class FaceRectTransformer { + private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation, cameraId; + private boolean isMirror; + private boolean mirrorHorizontal = false, mirrorVertical = false; + + /** + * 创建一个绘制辅助类对象,并且设置绘制相关的参数 + * + * @param previewWidth 预览宽度 + * @param previewHeight 预览高度 + * @param canvasWidth 绘制控件的宽度 + * @param canvasHeight 绘制控件的高度 + * @param cameraDisplayOrientation 旋转角度 + * @param cameraId 相机ID + * @param isMirror 是否水平镜像显示(若相机是镜像显示的,设为true,用于纠正) + * @param mirrorHorizontal 为兼容部分设备使用,水平再次镜像 + * @param mirrorVertical 为兼容部分设备使用,垂直再次镜像 + */ + public FaceRectTransformer(int previewWidth, int previewHeight, int canvasWidth, + int canvasHeight, int cameraDisplayOrientation, int cameraId, + boolean isMirror, boolean mirrorHorizontal, boolean mirrorVertical) { + this.previewWidth = previewWidth; + this.previewHeight = previewHeight; + this.canvasWidth = canvasWidth; + this.canvasHeight = canvasHeight; + this.cameraDisplayOrientation = cameraDisplayOrientation; + this.cameraId = cameraId; + this.isMirror = isMirror; + this.mirrorHorizontal = mirrorHorizontal; + this.mirrorVertical = mirrorVertical; + } + + /** + * 调整人脸框用来绘制 + * + * @param ftRect FT人脸框 + * @return 调整后的需要被绘制到View上的rect + */ + public Rect adjustRect(Rect ftRect) { + int previewWidth = this.previewWidth; + int previewHeight = this.previewHeight; + int canvasWidth = this.canvasWidth; + int canvasHeight = this.canvasHeight; + int cameraDisplayOrientation = this.cameraDisplayOrientation; + int cameraId = this.cameraId; + boolean isMirror = this.isMirror; + boolean mirrorHorizontal = this.mirrorHorizontal; + boolean mirrorVertical = this.mirrorVertical; + + if (ftRect == null) { + return null; + } + + Rect rect = new Rect(ftRect); + float horizontalRatio; + float verticalRatio; + if (cameraDisplayOrientation % 180 == 0) { + horizontalRatio = (float) canvasWidth / (float) previewWidth; + verticalRatio = (float) canvasHeight / (float) previewHeight; + } else { + horizontalRatio = (float) canvasHeight / (float) previewWidth; + verticalRatio = (float) canvasWidth / (float) previewHeight; + } + rect.left *= horizontalRatio; + rect.right *= horizontalRatio; + rect.top *= verticalRatio; + rect.bottom *= verticalRatio; + + Rect newRect = new Rect(); + switch (cameraDisplayOrientation) { + case 0: + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + } else { + newRect.left = rect.left; + newRect.right = rect.right; + } + newRect.top = rect.top; + newRect.bottom = rect.bottom; + break; + case 90: + newRect.right = canvasWidth - rect.top; + newRect.left = canvasWidth - rect.bottom; + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.top = canvasHeight - rect.right; + newRect.bottom = canvasHeight - rect.left; + } else { + newRect.top = rect.left; + newRect.bottom = rect.right; + } + break; + case 180: + newRect.top = canvasHeight - rect.bottom; + newRect.bottom = canvasHeight - rect.top; + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.left = rect.left; + newRect.right = rect.right; + } else { + newRect.left = canvasWidth - rect.right; + newRect.right = canvasWidth - rect.left; + } + break; + case 270: + newRect.left = rect.top; + newRect.right = rect.bottom; + if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { + newRect.top = rect.left; + newRect.bottom = rect.right; + } else { + newRect.top = canvasHeight - rect.right; + newRect.bottom = canvasHeight - rect.left; + } + break; + default: + break; + } + + /** + * isMirror mirrorHorizontal finalIsMirrorHorizontal + * true true false + * false false false + * true false true + * false true true + * + * XOR + */ + if (isMirror ^ mirrorHorizontal) { + int left = newRect.left; + int right = newRect.right; + newRect.left = canvasWidth - right; + newRect.right = canvasWidth - left; + } + if (mirrorVertical) { + int top = newRect.top; + int bottom = newRect.bottom; + newRect.top = canvasHeight - bottom; + newRect.bottom = canvasHeight - top; + } + return newRect; + } + + public void setPreviewWidth(int previewWidth) { + this.previewWidth = previewWidth; + } + + public void setPreviewHeight(int previewHeight) { + this.previewHeight = previewHeight; + } + + public void setCanvasWidth(int canvasWidth) { + this.canvasWidth = canvasWidth; + } + + public void setCanvasHeight(int canvasHeight) { + this.canvasHeight = canvasHeight; + } + + public void setCameraDisplayOrientation(int cameraDisplayOrientation) { + this.cameraDisplayOrientation = cameraDisplayOrientation; + } + + public void setCameraId(int cameraId) { + this.cameraId = cameraId; + } + + public void setMirror(boolean mirror) { + isMirror = mirror; + } + + public int getPreviewWidth() { + return previewWidth; + } + + public int getPreviewHeight() { + return previewHeight; + } + + public int getCanvasWidth() { + return canvasWidth; + } + + public int getCanvasHeight() { + return canvasHeight; + } + + public int getCameraDisplayOrientation() { + return cameraDisplayOrientation; + } + + public int getCameraId() { + return cameraId; + } + + public boolean isMirror() { + return isMirror; + } + + public boolean isMirrorHorizontal() { + return mirrorHorizontal; + } + + public void setMirrorHorizontal(boolean mirrorHorizontal) { + this.mirrorHorizontal = mirrorHorizontal; + } + + public boolean isMirrorVertical() { + return mirrorVertical; + } + + public void setMirrorVertical(boolean mirrorVertical) { + this.mirrorVertical = mirrorVertical; + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/FaceListener.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/FaceListener.java new file mode 100644 index 0000000..56d3cd6 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/FaceListener.java @@ -0,0 +1,37 @@ +package io.flutter.plugins.facediscern.tool.face.help.callback; + +import androidx.annotation.Nullable; + +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.LivenessInfo; + +/** + * 人脸处理回调 + */ +public interface FaceListener { + /** + * 当出现异常时执行 + * + * @param e 异常信息 + */ + void onFail(Exception e); + + + /** + * 请求人脸特征后的回调 + * + * @param faceFeature 人脸特征数据 + * @param trackId 人脸Id(相当于请求码) + * @param errorCode 错误码 + */ + void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode); + + /** + * 请求活体检测后的回调 + * + * @param livenessInfo 活体检测结果 + * @param trackId 人脸Id(相当于请求码) + * @param errorCode 错误码 + */ + void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode); +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/IDualCameraFaceInfoTransformer.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/IDualCameraFaceInfoTransformer.java new file mode 100644 index 0000000..6a98cae --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/IDualCameraFaceInfoTransformer.java @@ -0,0 +1,16 @@ +package io.flutter.plugins.facediscern.tool.face.help.callback; + +import com.arcsoft.face.FaceInfo; + +/** + * 设置双目识别时,将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式 + */ +public interface IDualCameraFaceInfoTransformer { + /** + * 将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式 + * + * @param faceInfo RGB Camera帧数据检测到的人脸信息 + * @return 转换后,用于IR活体检测的FaceInfo + */ + FaceInfo transformFaceInfo(FaceInfo faceInfo); +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/RecognizeCallback.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/RecognizeCallback.java new file mode 100644 index 0000000..ebda709 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/RecognizeCallback.java @@ -0,0 +1,20 @@ +package io.flutter.plugins.facediscern.tool.face.help.callback; + + +import io.flutter.plugins.facediscern.tool.face.model.CompareResult; + +public interface RecognizeCallback { + /** + * 识别结果回调 + * + * @param compareResult 比对结果 + * @param liveness 活体值 + * @param similarPass 是否通过(依据设置的阈值) + */ + void onRecognized(CompareResult compareResult, Integer liveness, boolean similarPass); + + /** + * 提示文字变更的回调 + */ + void onNoticeChanged(String notice); +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/image/ImageUtil.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/image/ImageUtil.java new file mode 100644 index 0000000..8ac8a88 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/image/ImageUtil.java @@ -0,0 +1,229 @@ +package io.flutter.plugins.facediscern.tool.face.image; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; + +public class ImageUtil { + public static final int DEFAULT_MAX_WIDTH = 1920; + public static final int DEFAULT_MAX_HEIGHT = 1080; + + + private static final int MASK_A = 0xFF000000; + private static final int MASK_R = 0x00FF0000; + private static final int MASK_G = 0x0000FF00; + private static final int MASK_B = 0x000000FF; + + public static int rgbToY(int r, int g, int b) { + return (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16); + } + + public static int rgbToU(int r, int g, int b) { + return (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128); + } + + public static int rgbToV(int r, int g, int b) { + return (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128); + } + + public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, Rect rect) { + if (rect == null || rect.isEmpty()) { + return; + } + drawRectOnNv21(nv21, width, height, color, strokeWidth, rect.left, rect.top, rect.right, rect.bottom); + } + + public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, int left, int top, + int right, int bottom) { + if ((strokeWidth & 1) == 1) { + strokeWidth += 1; + } + // 确保边界是4的倍数 + left &= ~0b11; + top &= ~0b11; + right &= ~0b11; + bottom &= ~0b11; + // 对于溢出图像的边,不绘制 + boolean drawLeft = true, drawTop = true, drawRight = true, drawBottom = true; + if (left <= 0) { + left = 0; + drawLeft = false; + } + if (top <= 0) { + top = 0; + drawTop = false; + } + if (right >= width) { + right = width; + drawRight = false; + } + if (bottom >= height) { + bottom = height; + drawBottom = false; + } + + // 取出R G B的值,并转换为Y U V + int r = (color & MASK_R) >> 16; + int g = (color & MASK_G) >> 8; + int b = color & MASK_B; + int y = rgbToY(r, g, b); + int u = rgbToU(r, g, b); + int v = rgbToV(r, g, b); + + // 根据边框的strokeWidth确定内边界 + int innerTop = top + strokeWidth; + int innerBottom = bottom - strokeWidth; + int innerRight = right - strokeWidth; + + int horizontalPixels = right - left; + int yStartIndex; + int uvStartIndex; + boolean drawUV; + if (drawTop) { + yStartIndex = top * width + left; + uvStartIndex = width * height + ((top / 2 * width) + left); + drawUV = false; + for (int i = top; i < innerTop; i++) { + for (int j = 0; j < horizontalPixels; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < horizontalPixels; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + + if (drawLeft) { + //左边 + yStartIndex = innerTop * width + left; + uvStartIndex = width * height + (innerTop / 2 * width + left); + drawUV = false; + for (int i = innerTop; i < innerBottom; i++) { + for (int j = 0; j < strokeWidth; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < strokeWidth; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + if (drawRight) { + //右边 + yStartIndex = innerTop * width + innerRight; + uvStartIndex = width * height + (innerTop / 2 * width + innerRight); + drawUV = false; + for (int i = innerTop; i < innerBottom; i++) { + for (int j = 0; j < strokeWidth; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < strokeWidth; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + + if (drawBottom) { + //下边 + yStartIndex = innerBottom * width + left; + uvStartIndex = width * height + ((innerBottom / 2 * width) + left); + drawUV = false; + for (int i = innerBottom; i < bottom; i++) { + for (int j = 0; j < horizontalPixels; j++) { + nv21[yStartIndex + j] = (byte) y; + } + yStartIndex += width; + if (drawUV = !drawUV) { + for (int j = 0; j < horizontalPixels; j += 2) { + nv21[uvStartIndex + j] = (byte) v; + nv21[uvStartIndex + j + 1] = (byte) u; + } + uvStartIndex += width; + } + } + } + } + + /** + * 缩放图像,如果需要缩放,就顺便把宽高对齐给做了 + * + * @param bitmap 原图 + * @param maxWidth 最大目标宽度 + * @param maxHeight 最大目标高度 + * @return 缩放后的图像 + */ + public static Bitmap scaleBitmap(Bitmap bitmap, int maxWidth, int maxHeight) { + float horizontalScale = ((float) bitmap.getWidth()) / maxWidth; + float verticalScale = ((float) bitmap.getHeight()) / maxHeight; + if (horizontalScale < 1 || verticalScale < 1) { + return bitmap; + } + float maxScale = Math.max(horizontalScale, verticalScale); + // 确保为4的倍数 + int newWidth = (int) (bitmap.getWidth() / maxScale) & ~0b11; + int newHeight = (int) (bitmap.getHeight() / maxScale) & ~0b11; + + return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); + } + + /** + * 将Uri转换为Bitmap,并限制最大宽高 + */ + public static Bitmap uriToScaledBitmap(Context context, Uri uri, int maxWidth, int maxHeight) { + ContentResolver contentResolver = context.getContentResolver(); + byte[] data; + try { + InputStream input = null; + input = contentResolver.openInputStream(uri); + data = new byte[input.available()]; + input.read(data); + input.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return jpegToScaledBitmap(data, maxWidth, maxHeight); + } + + /** + * 将jpeg形式的压缩图像转换为Bitmap,并限制最大宽高 + * + * @param jpeg jpeg图像数据 + * @param maxWidth 限制的最大宽度 + * @param maxHeight 限制的最大高度 + * @return 宽高小于限制值的Bitmap对象 + */ + public static Bitmap jpegToScaledBitmap(byte[] jpeg, int maxWidth, int maxHeight) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options); + + int inSampleSize = 1; + while (options.outWidth / inSampleSize > maxWidth || options.outHeight / inSampleSize > maxHeight) { + inSampleSize++; + } + options.inSampleSize = inSampleSize; + options.inJustDecodeBounds = false; + return BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options); + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/CompareResult.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/CompareResult.java new file mode 100644 index 0000000..82df7ff --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/CompareResult.java @@ -0,0 +1,58 @@ +package io.flutter.plugins.facediscern.tool.face.model; + + + +public class CompareResult { + private FaceEntity faceEntity; + private float similar; + private int trackId; + private int compareCode; + private long cost; + + public CompareResult(FaceEntity faceEntity, float similar, int compareCode,long cost) { + this.faceEntity = faceEntity; + this.similar = similar; + this.compareCode = compareCode; + this.cost = cost; + } + + public FaceEntity getFaceEntity() { + return faceEntity; + } + + public void setFaceEntity(FaceEntity faceEntity) { + this.faceEntity = faceEntity; + } + + public float getSimilar() { + return similar; + } + + public void setSimilar(float similar) { + this.similar = similar; + } + + public int getTrackId() { + return trackId; + } + + public void setTrackId(int trackId) { + this.trackId = trackId; + } + + public int getCompareCode() { + return compareCode; + } + + public void setCompareCode(int compareCode) { + this.compareCode = compareCode; + } + + public long getCost() { + return cost; + } + + public void setCost(long cost) { + this.cost = cost; + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FaceEntity.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FaceEntity.java new file mode 100644 index 0000000..cce18d4 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FaceEntity.java @@ -0,0 +1,78 @@ +package io.flutter.plugins.facediscern.tool.face.model; + + +public class FaceEntity { + // 人脸模型id + Integer faceId; + // 用户id + String userId; + // 用户人脸样本id + Integer sampleId; + // 用户名 + String userName; + // 人脸图片文件路径 + String faceFilePath; + // 人脸模型 + byte[] faceModel; + + public FaceEntity() { + + } + + public FaceEntity(byte[] faceModel) { + this.faceModel = faceModel; + } + + public FaceEntity(Integer faceId, byte[] faceModel) { + this.faceId = faceId; + this.faceModel = faceModel; + } + + public Integer getFaceId() { + return faceId; + } + + public void setFaceId(Integer faceId) { + this.faceId = faceId; + } + + public byte[] getFaceModel() { + return faceModel; + } + + public void setFaceModel(byte[] faceModel) { + this.faceModel = faceModel; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public Integer getSampleId() { + return sampleId; + } + + public void setSampleId(Integer sampleId) { + this.sampleId = sampleId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getFaceFilePath() { + return faceFilePath; + } + + public void setFaceFilePath(String faceFilePath) { + this.faceFilePath = faceFilePath; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FacePreviewInfo.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FacePreviewInfo.java new file mode 100644 index 0000000..41e85f3 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FacePreviewInfo.java @@ -0,0 +1,156 @@ +package io.flutter.plugins.facediscern.tool.face.model; + +import android.graphics.Rect; + +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.LivenessInfo; + +/** + * 人脸追踪时的信息 + */ +public class FacePreviewInfo { + /** + * RGB人脸信息,包括人脸框和人脸角度 + */ + private FaceInfo faceInfoRgb; + /** + * IR人脸信息,包括人脸框和人脸角度 + */ + private FaceInfo faceInfoIr; + /** + * 可见光成像对应的用于FaceRectView绘制的Rect + */ + private Rect rgbTransformedRect; + /** + * 红外成像对应的用于FaceRectView绘制的Rect + */ + private Rect irTransformedRect; + private int rgbLiveness = LivenessInfo.UNKNOWN; + private int irLiveness = LivenessInfo.UNKNOWN; + private float imageQuality = 0f; + /** + * 识别区域是否合法 + */ + private boolean recognizeAreaValid; + /** + * 基于{@link FaceInfo#getFaceId()}的一个偏移值,可理解为SDK截至目前检测到的人次,唯一性同faceId + */ + private int trackId; + /** + * 整体质量是否通过,包括人脸大小、角度、移动速度等 + */ + private boolean qualityPass = true; + + /** + * 是否戴口罩 + */ + private int mask; + + private Rect foreRect; + + public Rect getForeRect() { + return foreRect; + } + + public void setForeRect(Rect foreRect) { + this.foreRect = foreRect; + } + + + public FacePreviewInfo(FaceInfo faceInfoRgb) { + this.faceInfoRgb = faceInfoRgb; + } + public FacePreviewInfo(FaceInfo faceInfoRgb, int trackId) { + this.faceInfoRgb = faceInfoRgb; + this.trackId = trackId; + } + + public FaceInfo getFaceInfoRgb() { + return faceInfoRgb; + } + + public void setFaceInfoRgb(FaceInfo faceInfoRgb) { + this.faceInfoRgb = faceInfoRgb; + } + + + public int getTrackId() { + return trackId; + } + + public void setTrackId(int trackId) { + this.trackId = trackId; + } + + public void setRgbTransformedRect(Rect rgbTransformedRect) { + this.rgbTransformedRect = rgbTransformedRect; + } + + public void setIrTransformedRect(Rect irTransformedRect) { + this.irTransformedRect = irTransformedRect; + } + + public Rect getRgbTransformedRect() { + return rgbTransformedRect; + } + + public Rect getIrTransformedRect() { + return irTransformedRect; + } + + public boolean isRecognizeAreaValid() { + return recognizeAreaValid; + } + + public void setRecognizeAreaValid(boolean recognizeAreaValid) { + this.recognizeAreaValid = recognizeAreaValid; + } + + public void setFaceInfoIr(FaceInfo faceInfoIr) { + this.faceInfoIr = faceInfoIr; + } + + public FaceInfo getFaceInfoIr() { + return faceInfoIr; + } + + public int getRgbLiveness() { + return rgbLiveness; + } + + public void setRgbLiveness(int rgbLiveness) { + this.rgbLiveness = rgbLiveness; + } + + public int getIrLiveness() { + return irLiveness; + } + + public void setIrLiveness(int irLiveness) { + this.irLiveness = irLiveness; + } + + public void setImageQuality(float imageQuality) { + this.imageQuality = imageQuality; + } + + public float getImageQuality() { + return imageQuality; + } + + public boolean isQualityPass() { + return qualityPass; + } + + public void setQualityPass(boolean qualityPass) { + this.qualityPass = qualityPass; + } + + public int getMask() { + return mask; + } + + public void setMask(int mask) { + this.mask = mask; + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeConfiguration.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeConfiguration.java new file mode 100644 index 0000000..e67f62e --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeConfiguration.java @@ -0,0 +1,300 @@ +package io.flutter.plugins.facediscern.tool.face.model; + +import com.arcsoft.face.LivenessParam; + +/** + * 识别相关的配置项 + */ +public class RecognizeConfiguration { + /** + * 产生特征提取失败示语的特征提取次数(小于该值不提示) + */ + private int extractRetryCount; + /** + * 产生活体检测失败示语的活体检测次数(小于该值不提示) + */ + private int livenessRetryCount; + /** + * 最大人脸检测数量 + */ + private int maxDetectFaces; + /** + * 识别阈值 + */ + private float similarThreshold; + /** + * 图像质量检测阈值:适用于不戴口罩且人脸识别场景 + */ + private float imageQualityNoMaskRecognizeThreshold; + /** + * 图像质量检测阈值:适用于戴口罩且人脸识别场景 + */ + private float imageQualityMaskRecognizeThreshold; + /** + * 识别失败重试间隔 + */ + private int recognizeFailedRetryInterval; + /** + * 活体检测未通过重试间隔 + */ + private int livenessFailedRetryInterval; + /** + * 启用活体 + */ + private boolean enableLiveness; + /** + * 启用图像质量检测 + */ + private boolean enableImageQuality; + /** + * 识别区域限制 + */ + private boolean enableFaceAreaLimit; + /** + * 仅识别最大人脸 + */ + private boolean keepMaxFace; + /** + * 活体阈值设置 + */ + private LivenessParam livenessParam; + + + /** + * 启用人脸边长限制 + */ + private boolean enableFaceSizeLimit = false; + /** + * 启用人脸移动限制 + */ + private boolean enableFaceMoveLimit = false; + /** + * 人脸边长限制值 + */ + private int faceSizeLimit = 0; + /** + * 人脸上下针移动限制值 + */ + private int faceMoveLimit = 0; + + + public RecognizeConfiguration(Builder builder) { + this.extractRetryCount = builder.extractRetryCount; + this.livenessRetryCount = builder.livenessRetryCount; + this.livenessFailedRetryInterval = builder.livenessFailedRetryInterval; + this.maxDetectFaces = builder.maxDetectFaces; + this.similarThreshold = builder.similarThreshold; + this.imageQualityNoMaskRecognizeThreshold = builder.imageQualityNoMaskRecognizeThreshold; + this.imageQualityMaskRecognizeThreshold = builder.imageQualityMaskRecognizeThreshold; + this.enableLiveness = builder.enableLiveness; + this.enableImageQuality = builder.enableImageQuality; + this.enableFaceAreaLimit = builder.enableFaceAreaLimit; + this.keepMaxFace = builder.keepMaxFace; + this.recognizeFailedRetryInterval = builder.recognizeFailedRetryInterval; + this.livenessParam = builder.livenessParam; + this.enableFaceSizeLimit = builder.enableFaceSizeLimit; + this.enableFaceMoveLimit = builder.enableFaceMoveLimit; + this.faceSizeLimit = builder.faceSizeLimit; + this.faceMoveLimit = builder.faceMoveLimit; + } + + //TODO: demo不实现所有配置,若以下项也需要进行自定义配置,可参考其他配置项实现 + public static class Builder { + private int extractRetryCount = 3; + private int livenessRetryCount = 3; + private int maxDetectFaces = 3; + private int recognizeFailedRetryInterval = 0; + private int livenessFailedRetryInterval = 0; + private float similarThreshold = 0.8f; + private float imageQualityNoMaskRecognizeThreshold = 0.49f; + private float imageQualityMaskRecognizeThreshold = 0.29f; + private boolean enableLiveness = false; + private boolean enableFaceAreaLimit = false; + private boolean enableImageQuality = false; + private boolean enableFaceSizeLimit = false; + private boolean enableFaceMoveLimit = false; + private int faceSizeLimit = 0; + private int faceMoveLimit = 0; + private boolean keepMaxFace = false; + private LivenessParam livenessParam; + + public Builder recognizeFailedRetryInterval(int val) { + this.recognizeFailedRetryInterval = val; + return this; + } + + public Builder livenessFailedRetryInterval(int val) { + this.livenessFailedRetryInterval = val; + return this; + } + + public Builder extractRetryCount(int val) { + this.extractRetryCount = val; + return this; + } + + public Builder livenessRetryCount(int val) { + this.livenessRetryCount = val; + return this; + } + + public Builder maxDetectFaces(int val) { + this.maxDetectFaces = val; + return this; + } + + public Builder similarThreshold(float val) { + this.similarThreshold = val; + return this; + } + + public Builder imageQualityNoMaskRecognizeThreshold(float val) { + this.imageQualityNoMaskRecognizeThreshold = val; + return this; + } + + public Builder imageQualityMaskRecognizeThreshold(float val) { + this.imageQualityMaskRecognizeThreshold = val; + return this; + } + + public Builder enableLiveness(boolean val) { + this.enableLiveness = val; + return this; + } + + + public Builder enableImageQuality(boolean val) { + this.enableImageQuality = val; + return this; + } + public Builder enableFaceAreaLimit(boolean val) { + this.enableFaceAreaLimit = val; + return this; + } + + public Builder enableFaceSizeLimit(boolean val) { + this.enableFaceSizeLimit = val; + return this; + } + + public Builder enableFaceMoveLimit(boolean val) { + this.enableFaceMoveLimit = val; + return this; + } + + public Builder faceSizeLimit(int val) { + this.faceSizeLimit = val; + return this; + } + + public Builder faceMoveLimit(int val) { + this.faceMoveLimit = val; + return this; + } + + public Builder keepMaxFace(boolean val) { + this.keepMaxFace = val; + return this; + } + + public Builder livenessParam(LivenessParam val) { + this.livenessParam = val; + return this; + } + + + public RecognizeConfiguration build() { + return new RecognizeConfiguration(this); + } + } + + public float getImageQualityNoMaskRecognizeThreshold() { + return imageQualityNoMaskRecognizeThreshold; + } + + public float getImageQualityMaskRecognizeThreshold() { + return imageQualityMaskRecognizeThreshold; + } + + public boolean isEnableImageQuality() { + return enableImageQuality; + } + + public boolean isEnableFaceAreaLimit() { + return enableFaceAreaLimit; + } + + public LivenessParam getLivenessParam() { + return livenessParam; + } + + public int getExtractRetryCount() { + return extractRetryCount; + } + + public int getLivenessRetryCount() { + return livenessRetryCount; + } + + public int getMaxDetectFaces() { + return maxDetectFaces; + } + + public float getSimilarThreshold() { + return similarThreshold; + } + + public boolean isEnableLiveness() { + return enableLiveness; + } + + + public int getRecognizeFailedRetryInterval() { + return recognizeFailedRetryInterval; + } + + public int getLivenessFailedRetryInterval() { + return livenessFailedRetryInterval; + } + + public boolean isKeepMaxFace() { + return keepMaxFace; + } + + public boolean isEnableFaceSizeLimit() { + return enableFaceSizeLimit; + } + + public boolean isEnableFaceMoveLimit() { + return enableFaceMoveLimit; + } + + public int getFaceSizeLimit() { + return faceSizeLimit; + } + + public int getFaceMoveLimit() { + return faceMoveLimit; + } + + @Override + public String toString() { + return + "extractRetryCount: " + extractRetryCount + "\r\n" + + "similarThreshold: " + similarThreshold + "\r\n" + + "recognizeFailedRetryInterval: " + recognizeFailedRetryInterval + "\r\n" + + + "keepMaxFace: " + keepMaxFace + "\r\n" + + "maxDetectFaces: " + maxDetectFaces + "\r\n" + + + "enableImageQuality: " + enableImageQuality + "\r\n" + + "imageQualityNoMaskRecognizeThreshold: " + imageQualityNoMaskRecognizeThreshold + "\r\n" + + "imageQualityMaskRecognizeThreshold: " + imageQualityMaskRecognizeThreshold + "\r\n" + + + "enableLiveness: " + enableLiveness + "\r\n" + + "livenessRetryCount: " + livenessRetryCount + "\r\n" + + "livenessParams: " + (livenessParam == null ? null : (livenessParam.getRgbThreshold() + "," + livenessParam.getIrThreshold())); + + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeInfo.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeInfo.java new file mode 100644 index 0000000..9350b37 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeInfo.java @@ -0,0 +1,86 @@ +package io.flutter.plugins.facediscern.tool.face.model; + +import com.arcsoft.face.LivenessInfo; +import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus; + +/** + * 单个人脸(faceId)识别过程中的信息 + */ +public class RecognizeInfo { + /** + * 用于记录人脸识别相关状态 + */ + private int recognizeStatus = RequestFeatureStatus.TO_RETRY; + /** + * 用于记录人脸特征提取出错重试次数 + */ + private int extractErrorRetryCount; + /** + * 用于存储活体值 + */ + private int liveness = LivenessInfo.UNKNOWN; + /** + * 用于存储活体检测出错重试次数 + */ + private int livenessErrorRetryCount; + /** + * 用户姓名,用于显示 + */ + private String name; + /** + * 特征等活体的lock + */ + private Object waitLock = new Object(); + + public int getRecognizeStatus() { + return recognizeStatus; + } + + public void setRecognizeStatus(int recognizeStatus) { + this.recognizeStatus = recognizeStatus; + } + + public void setLiveness(int liveness) { + this.liveness = liveness; + } + + public int increaseAndGetExtractErrorRetryCount() { + return ++extractErrorRetryCount; + } + + public int getLiveness() { + return liveness; + } + + public int increaseAndGetLivenessErrorRetryCount() { + return ++livenessErrorRetryCount; + } + + public void setExtractErrorRetryCount(int extractErrorRetryCount) { + this.extractErrorRetryCount = extractErrorRetryCount; + } + + public void setLivenessErrorRetryCount(int livenessErrorRetryCount) { + this.livenessErrorRetryCount = livenessErrorRetryCount; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Object getWaitLock() { + return waitLock; + } + + public int getExtractErrorRetryCount() { + return extractErrorRetryCount; + } + + public int getLivenessErrorRetryCount() { + return livenessErrorRetryCount; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/FaceServer.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/FaceServer.java new file mode 100644 index 0000000..04b092d --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/FaceServer.java @@ -0,0 +1,359 @@ +package io.flutter.plugins.facediscern.tool.face.servcie; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.os.Environment; +import android.util.Log; + +import com.arcsoft.face.ErrorInfo; +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceFeature; +import com.arcsoft.face.FaceFeatureInfo; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.MaskInfo; +import com.arcsoft.face.SearchResult; +import com.arcsoft.face.enums.DetectFaceOrientPriority; +import com.arcsoft.face.enums.DetectMode; +import com.arcsoft.face.enums.ExtractType; +import com.arcsoft.imageutil.ArcSoftImageFormat; +import com.arcsoft.imageutil.ArcSoftImageUtil; +import com.arcsoft.imageutil.ArcSoftImageUtilError; +import com.arcsoft.imageutil.ArcSoftRotateDegree; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.flutter.plugins.facediscern.tool.face.model.CompareResult; +import io.flutter.plugins.facediscern.tool.face.model.FaceEntity; + +/** + * 人脸库操作类,包含注册和搜索 + */ +public class FaceServer { + private static final String TAG = "FaceServer"; + private static FaceEngine faceEngine = null; + private static volatile FaceServer faceServer = null; + private List faceRegisterInfoList = new ArrayList<>(); + private String imageRootPath; + /** + * 最大注册人脸数 + */ + private static final int MAX_REGISTER_FACE_COUNT = 30000; + + public synchronized void init(Context context) { + if (faceEngine == null && context != null) { + faceEngine = new FaceEngine(); + int engineCode = faceEngine.init( + context, + DetectMode.ASF_DETECT_MODE_IMAGE, + DetectFaceOrientPriority.ASF_OP_ALL_OUT, + 1, + FaceEngine.ASF_FACE_RECOGNITION | FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT + ); + if (engineCode != ErrorInfo.MOK) { + Log.e(TAG, "人脸引擎初始化失败code" + engineCode); + } + } + } + + public static FaceServer getInstance() { + if (faceServer == null) { + synchronized (FaceServer.class) { + if (faceServer == null) { + faceServer = new FaceServer(); + } + } + } + return faceServer; + } + + /** + * 销毁 + */ + public synchronized void release() { + if (faceRegisterInfoList != null) { + faceRegisterInfoList.clear(); + faceRegisterInfoList = null; + } + if (faceEngine != null) { + synchronized (faceEngine) { + faceEngine.unInit(); + } + faceEngine = null; + } + faceServer = null; + } + + public int registerFaceFeatureInfoListFromDb(List faceEntityList) { + if (faceEngine == null) return -99; + List faceFeatureInfoList = new ArrayList<>(); + for (FaceEntity faceEntity : faceEntityList) { + FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFaceModel()); + faceFeatureInfoList.add(faceFeatureInfo); + } + //首先清除FaceEngine中所有人脸数据,再添加新的人脸数据 + faceEngine.removeFaceFeature(-1); + int code = faceEngine.registerFaceFeature(faceFeatureInfoList); + if (code == ErrorInfo.MOK) { + faceRegisterInfoList.addAll(faceEntityList); + } + return code; + } + + // 注册单个人脸数据 + public int registerFaceFeatureInfoFromDb(FaceEntity faceEntity) { + if (faceEntity != null && faceEngine != null) { + FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo(faceEntity.getFaceId(), faceEntity.getFaceModel()); + int code = faceEngine.registerFaceFeature(faceFeatureInfo); + if (code == ErrorInfo.MOK) { + faceEntity.setFaceModel(faceFeatureInfo.getFeatureData()); + // 查询一下确保入库成功 + FaceFeatureInfo afterDetail = getFaceFeature(faceEntity.getFaceId()); + if (Objects.nonNull(afterDetail)) { + long count = faceRegisterInfoList.stream().filter(v -> Objects.equals(v.getFaceId(), faceEntity.getFaceId())).count(); + if (count == 0) { + faceRegisterInfoList.add(faceEntity); + } + return code; + } + } + } + return -99; + } + + // 从人脸数据库中搜索人脸 + public FaceFeatureInfo getFaceFeature(int searchId) { + FaceFeatureInfo faceFeatureInfo = null; + try { + faceFeatureInfo = faceEngine.getFaceFeature(searchId); + } catch (IllegalArgumentException e) { + Log.e(TAG, "从人脸数据库中搜索人脸error:" + e.getMessage(), e); + } + return faceFeatureInfo; + } + + // 获取人脸数量 + public int getTotalFaceCount() { + if (faceEngine == null) return -99; + return faceEngine.getFaceCount(); + } + + // 删除人脸-1删除全部 + public int removeFace(int searchId) { + if (faceEngine == null) return -99; + return faceEngine.removeFaceFeature(searchId); + } + + // 在特征库中搜索 + public CompareResult searchFaceFeature(FaceFeature faceFeature) { + if (faceFeature == null) return null; + if (faceEngine == null) return null; + SearchResult searchResult = null; + try { + long searchStart = System.currentTimeMillis(); + searchResult = faceEngine.searchFaceFeature(faceFeature); + long searchCost = System.currentTimeMillis() - searchStart; + if (searchResult != null) { + FaceFeatureInfo faceFeatureInfo = searchResult.getFaceFeatureInfo(); + FaceEntity entity = faceRegisterInfoList.stream().filter(v -> Objects.equals(v.getFaceId(), faceFeatureInfo.getSearchId())).findFirst().get(); + if (Objects.isNull(entity)) { + entity = new FaceEntity(faceFeatureInfo.getSearchId(), faceFeatureInfo.getFeatureData()); + } + Log.i(TAG, "查询人脸总耗时:" + (System.currentTimeMillis() - searchStart) + "ms;username:" + entity.getUserName() + ";faceId:" + entity.getFaceId() + ";"); + return new CompareResult(entity, searchResult.getMaxSimilar(), ErrorInfo.MOK, searchCost); + } + } catch (IllegalArgumentException exception) { + Log.i(TAG, "exception:" + exception.getMessage()); + } + return null; + } + + + public byte[] registerBgr24(Context context, byte[] bgr24, int width, int height, String name) { + if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) { + Log.e(TAG, "registerBgr24: invalid params"); + return null; + } + //人脸检测 + List faceInfoList = new ArrayList<>(); + int code; + synchronized (faceEngine) { + code = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList); + } + if (code == ErrorInfo.MOK && !faceInfoList.isEmpty()) { + code = faceEngine.process(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList, + FaceEngine.ASF_MASK_DETECT); + if (code == ErrorInfo.MOK) { + List maskInfoList = new ArrayList<>(); + faceEngine.getMask(maskInfoList); + if (!maskInfoList.isEmpty()) { + int isMask = maskInfoList.get(0).getMask(); + if (isMask == MaskInfo.WORN) { + /* + * 注册照要求不戴口罩 + */ + Log.e(TAG, "registerBgr24: maskInfo is worn"); + return null; + } + if (isMask == MaskInfo.UNKNOWN) { + Log.e(TAG, "registerBgr24: maskInfo is unknown"); + return null; + } + } + } else { + Log.e(TAG, "registerBgr24 mask process fail res=" + code); + return null; + } + FaceFeature faceFeature = new FaceFeature(); + /* + * 特征提取,注册人脸时参数extractType值为ExtractType.REGISTER,参数mask的值为MaskInfo.NOT_WORN + */ + synchronized (faceEngine) { + code = faceEngine.extractFaceFeature(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList.get(0), + ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature); + } + String userName = name == null ? String.valueOf(System.currentTimeMillis()) : name; + //保存注册结果(注册图、特征数据) + if (code == ErrorInfo.MOK) { + //为了美观,扩大rect截取注册图 + Rect cropRect = getBestRect(width, height, faceInfoList.get(0).getRect()); + if (cropRect == null) { + Log.e(TAG, "registerBgr24: cropRect is null"); + return null; + } + cropRect.left &= ~3; + cropRect.top &= ~3; + cropRect.right &= ~3; + cropRect.bottom &= ~3; + String imgPath = getImagePath(userName, context); + // 创建一个头像的Bitmap,存放旋转结果图 + Bitmap headBmp = getHeadImage(bgr24, width, height, faceInfoList.get(0).getOrient(), cropRect, ArcSoftImageFormat.BGR24); + try { + FileOutputStream fos = new FileOutputStream(imgPath); + headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return faceFeature.getFeatureData(); + } else { + Log.e(TAG, "registerBgr24: extract face feature failed, code is " + code); + return null; + } + } else { + Log.e(TAG, "registerBgr24: no face detected, code is " + code); + return null; + } + } + + private String getImagePath(String name, Context context) { + if (imageRootPath == null) { + imageRootPath = getImageDir(context); + File dir = new File(imageRootPath); + if (!dir.exists() && !dir.mkdirs()) { + return null; + } + } + return imageRootPath + File.separator + name + "_" + System.currentTimeMillis() + ".jpg"; + } + + private String getImageDir(Context context) { + String path = context + .getApplicationContext() + .getExternalFilesDir(Environment.DIRECTORY_PICTURES) + + File.separator + "faceDB" + File.separator + "registerFaces"; + Log.i(TAG, "存储图片的地址为path:" + path); + return path; + } + + /** + * 将图像中需要截取的Rect向外扩张一倍,若扩张一倍会溢出,则扩张到边界,若Rect已溢出,则收缩到边界 + * + * @param width 图像宽度 + * @param height 图像高度 + * @param srcRect 原Rect + * @return 调整后的Rect + */ + private static Rect getBestRect(int width, int height, Rect srcRect) { + if (srcRect == null) { + return null; + } + Rect rect = new Rect(srcRect); + + // 原rect边界已溢出宽高的情况 + int maxOverFlow = Math.max(-rect.left, Math.max(-rect.top, Math.max(rect.right - width, rect.bottom - height))); + if (maxOverFlow >= 0) { + rect.inset(maxOverFlow, maxOverFlow); + return rect; + } + + // 原rect边界未溢出宽高的情况 + int padding = rect.height() / 2; + + // 若以此padding扩张rect会溢出,取最大padding为四个边距的最小值 + if (!(rect.left - padding > 0 && rect.right + padding < width && rect.top - padding > 0 && rect.bottom + padding < height)) { + padding = Math.min(Math.min(Math.min(rect.left, width - rect.right), height - rect.bottom), rect.top); + } + rect.inset(-padding, -padding); + return rect; + } + + private Bitmap getHeadImage(byte[] originImageData, int width, int height, int orient, Rect cropRect, ArcSoftImageFormat imageFormat) { + byte[] headImageData = ArcSoftImageUtil.createImageData(cropRect.width(), cropRect.height(), imageFormat); + int cropCode = ArcSoftImageUtil.cropImage(originImageData, headImageData, width, height, cropRect, imageFormat); + if (cropCode != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("crop image failed, code is " + cropCode); + } + + //判断人脸旋转角度,若不为0度则旋转注册图 + byte[] rotateHeadImageData = null; + int cropImageWidth; + int cropImageHeight; + // 90度或270度的情况,需要宽高互换 + if (orient == FaceEngine.ASF_OC_90 || orient == FaceEngine.ASF_OC_270) { + cropImageWidth = cropRect.height(); + cropImageHeight = cropRect.width(); + } else { + cropImageWidth = cropRect.width(); + cropImageHeight = cropRect.height(); + } + ArcSoftRotateDegree rotateDegree = null; + switch (orient) { + case FaceEngine.ASF_OC_90: + rotateDegree = ArcSoftRotateDegree.DEGREE_270; + break; + case FaceEngine.ASF_OC_180: + rotateDegree = ArcSoftRotateDegree.DEGREE_180; + break; + case FaceEngine.ASF_OC_270: + rotateDegree = ArcSoftRotateDegree.DEGREE_90; + break; + case FaceEngine.ASF_OC_0: + default: + rotateHeadImageData = headImageData; + break; + } + // 非0度的情况,旋转图像 + if (rotateDegree != null) { + rotateHeadImageData = new byte[headImageData.length]; + int rotateCode = ArcSoftImageUtil.rotateImage(headImageData, rotateHeadImageData, cropRect.width(), cropRect.height(), rotateDegree, imageFormat); + if (rotateCode != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("rotate image failed, code is : " + rotateCode + ", code description is : "); + } + } + // 将创建一个Bitmap,并将图像数据存放到Bitmap中 + Bitmap headBmp = Bitmap.createBitmap(cropImageWidth, cropImageHeight, Bitmap.Config.RGB_565); + int imageDataToBitmapCode = ArcSoftImageUtil.imageDataToBitmap(rotateHeadImageData, headBmp, imageFormat); + if (imageDataToBitmapCode != ArcSoftImageUtilError.CODE_SUCCESS) { + throw new RuntimeException("failed to transform image data to bitmap, code is : " + imageDataToBitmapCode + + ", code description is : "); + } + return headBmp; + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/RegisterFailedException.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/RegisterFailedException.java new file mode 100644 index 0000000..7d2d7c8 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/RegisterFailedException.java @@ -0,0 +1,7 @@ +package io.flutter.plugins.facediscern.tool.face.servcie; + +public class RegisterFailedException extends Exception { + public RegisterFailedException(String message) { + super(message); + } +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/widget/FaceRectView.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/widget/FaceRectView.java new file mode 100644 index 0000000..d438d88 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/widget/FaceRectView.java @@ -0,0 +1,295 @@ +package io.flutter.plugins.facediscern.tool.face.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.arcsoft.face.AgeInfo; +import com.arcsoft.face.FaceAttributeInfo; +import com.arcsoft.face.GenderInfo; +import com.arcsoft.face.LivenessInfo; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 用于显示人脸信息的控件 + */ +public class FaceRectView extends View { + private CopyOnWriteArrayList drawInfoList = new CopyOnWriteArrayList<>(); + + // 画笔,复用 + private Paint paint; + + // 默认人脸框厚度 + private static final int DEFAULT_FACE_RECT_THICKNESS = 6; + + public FaceRectView(Context context) { + this(context, null); + } + + public FaceRectView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + paint = new Paint(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (drawInfoList != null && drawInfoList.size() > 0) { + for (int i = 0; i < drawInfoList.size(); i++) { + drawFaceRect(canvas, drawInfoList.get(i), DEFAULT_FACE_RECT_THICKNESS, paint); + } + } + } + + public void clearFaceInfo() { + drawInfoList.clear(); + postInvalidate(); + } + + public void addFaceInfo(DrawInfo faceInfo) { + drawInfoList.add(faceInfo); + postInvalidate(); + } + + public void addFaceInfo(List faceInfoList) { + drawInfoList.addAll(faceInfoList); + postInvalidate(); + } + + public void drawRealtimeFaceInfo(List drawInfoList) { + clearFaceInfo(); + if (drawInfoList == null || drawInfoList.size() == 0) { + return; + } + addFaceInfo(drawInfoList); + } + + public static class DrawInfo { + private Rect rect; + private int sex; + private int age; + private int liveness; + private int color; + private int isWithinBoundary; + private String name = null; + private boolean drawRectInfo; + private Rect foreheadRect; + private FaceAttributeInfo faceAttributeInfo; + private boolean rgbRect; + + public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name) { + this.rect = rect; + this.sex = sex; + this.age = age; + this.liveness = liveness; + this.color = color; + this.name = name; + } + + public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name, int isWithinBoundary, Rect foreheadRect, + FaceAttributeInfo faceAttributeInfo, boolean drawRectInfo, boolean rgbRect) { + this.rect = rect; + this.sex = sex; + this.age = age; + this.liveness = liveness; + this.color = color; + this.name = name; + this.isWithinBoundary = isWithinBoundary; + this.drawRectInfo = drawRectInfo; + this.foreheadRect = foreheadRect; + this.faceAttributeInfo = faceAttributeInfo; + this.rgbRect = rgbRect; + } + + public DrawInfo(DrawInfo drawInfo) { + if (drawInfo == null) { + return; + } + this.rect = drawInfo.rect; + this.sex = drawInfo.sex; + this.age = drawInfo.age; + this.liveness = drawInfo.liveness; + this.color = drawInfo.color; + this.name = drawInfo.name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Rect getRect() { + return rect; + } + + public void setRect(Rect rect) { + this.rect = rect; + } + + public int getSex() { + return sex; + } + + public void setSex(int sex) { + this.sex = sex; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public int getLiveness() { + return liveness; + } + + public void setLiveness(int liveness) { + this.liveness = liveness; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public boolean isDrawRectInfo() { + return drawRectInfo; + } + + public void setDrawRectInfo(boolean drawRectInfo) { + this.drawRectInfo = drawRectInfo; + } + + public Rect getForeheadRect() { + return foreheadRect; + } + + public void setForeheadRect(Rect foreheadRect) { + this.foreheadRect = foreheadRect; + } + + public FaceAttributeInfo getFaceAttributeInfo() { + return faceAttributeInfo; + } + + public void setFaceAttributeInfo(FaceAttributeInfo faceAttributeInfo) { + this.faceAttributeInfo = faceAttributeInfo; + } + + public int getIsWithinBoundary() { + return isWithinBoundary; + } + + public void setIsWithinBoundary(int isWithinBoundary) { + this.isWithinBoundary = isWithinBoundary; + } + } + + + /** + * 绘制数据信息到view上,若 {@link DrawInfo#getName()} 不为null则绘制 {@link DrawInfo#getName()} + * + * @param canvas 需要被绘制的view的canvas + * @param drawInfo 绘制信息 + * @param faceRectThickness 人脸框厚度 + * @param paint 画笔 + */ + private static void drawFaceRect(Canvas canvas, DrawInfo drawInfo, int faceRectThickness, Paint paint) { + if (canvas == null || drawInfo == null) { + return; + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(faceRectThickness); + paint.setColor(drawInfo.getColor()); + paint.setAntiAlias(true); + + Path mPath = new Path(); + // 左上 + Rect rect = drawInfo.getRect(); + mPath.moveTo(rect.left, rect.top + rect.height() / 4); + mPath.lineTo(rect.left, rect.top); + mPath.lineTo(rect.left + rect.width() / 4, rect.top); + // 右上 + mPath.moveTo(rect.right - rect.width() / 4, rect.top); + mPath.lineTo(rect.right, rect.top); + mPath.lineTo(rect.right, rect.top + rect.height() / 4); + // 右下 + mPath.moveTo(rect.right, rect.bottom - rect.height() / 4); + mPath.lineTo(rect.right, rect.bottom); + mPath.lineTo(rect.right - rect.width() / 4, rect.bottom); + // 左下 + mPath.moveTo(rect.left + rect.width() / 4, rect.bottom); + mPath.lineTo(rect.left, rect.bottom); + mPath.lineTo(rect.left, rect.bottom - rect.height() / 4); + canvas.drawPath(mPath, paint); + + // 绘制文字,用最细的即可,避免在某些低像素设备上文字模糊 + paint.setStrokeWidth(1); + + if (drawInfo.getName() == null) { + paint.setStyle(Paint.Style.FILL_AND_STROKE); + paint.setTextSize(rect.width() / 12); + String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN")) + + "," + + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : drawInfo.getAge()) + + "," + + (drawInfo.getLiveness() == LivenessInfo.ALIVE ? "ALIVE" : (drawInfo.getLiveness() == LivenessInfo.NOT_ALIVE ? "NOT_ALIVE" : "UNKNOWN")); + canvas.drawText(str, rect.left, rect.top - 10, paint); + } else { + paint.setStyle(Paint.Style.FILL_AND_STROKE); + paint.setTextSize(rect.width() / 12); + canvas.drawText(drawInfo.getName(), rect.left, rect.top - 10, paint); + } + + if (drawInfo.drawRectInfo && drawInfo.rgbRect) { + Rect foreRect = drawInfo.foreheadRect; + if (foreRect != null) { + Path forePath = new Path(); + forePath.moveTo(foreRect.left, foreRect.top); + forePath.lineTo(foreRect.right, foreRect.top); + forePath.lineTo(foreRect.right, foreRect.bottom); + forePath.lineTo(foreRect.left, foreRect.bottom); + forePath.lineTo(foreRect.left, foreRect.top); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + canvas.drawPath(forePath, paint); + } + + FaceAttributeInfo attributeInfo = drawInfo.getFaceAttributeInfo(); + if (attributeInfo != null) { + paint.setStyle(Paint.Style.FILL_AND_STROKE); + int textSize = rect.width() / 8; + paint.setStrokeWidth(1); + paint.setTextSize(textSize); + int defX = rect.left; + int defY = rect.bottom + rect.width() / 8; + + String isStrInfo0 = drawInfo.getIsWithinBoundary() == 1 ? "是" : "否"; + String strInfo0 = "是否在相机范围内: " + isStrInfo0; + canvas.drawText(strInfo0, defX, defY, paint); + + String isStrInfo1 = attributeInfo.getWearGlasses() == 1 ? "是" : "否"; + String strInfo1 = "是否戴眼镜: " + isStrInfo1; + canvas.drawText(strInfo1, defX, defY + textSize, paint); + } + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/file/FileUtil.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/file/FileUtil.java new file mode 100644 index 0000000..4f94960 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/file/FileUtil.java @@ -0,0 +1,71 @@ +package io.flutter.plugins.facediscern.tool.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +public class FileUtil { + /** + * 读取文件中的数据内容 + * + * @param file 文件 + * @return 二进制数据内容 + */ + public static byte[] fileToData(File file) { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + byte[] data = new byte[fis.available()]; + fis.read(data); + fis.close(); + return data; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public static boolean saveDataToFile(byte[] data, File file, boolean append) { + if (data == null){ + return false; + } + File parentFile = file.getParentFile(); + if (parentFile == null) { + return false; + } + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + return false; + } + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file, append); + int bufferSize = 1024; + int index = 0; + while (index < data.length) { + if (data.length - index < bufferSize) { + bufferSize = data.length - index; + } + fos.write(data, index, bufferSize); + index += bufferSize; + } + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + try { + if (fos != null) { + fos.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public static boolean saveDataToFile(byte[] data, File file) { + return saveDataToFile(data, file, false); + } + +} diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/ui/FaceDiscern.java b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/FaceDiscern.java new file mode 100644 index 0000000..0548c62 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/FaceDiscern.java @@ -0,0 +1,323 @@ +package io.flutter.plugins.facediscern.ui; + +import android.Manifest; +import android.content.Intent; +import android.graphics.Point; +import android.hardware.Camera; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import androidx.activity.EdgeToEdge; +import androidx.core.app.ActivityCompat; +import androidx.databinding.DataBindingUtil; +import androidx.lifecycle.ViewModelProvider; + +import com.example.pj_supermarket_cloud.R; +import com.example.pj_supermarket_cloud.databinding.ActivityFaceDiscernBinding; + +import io.flutter.Log; +import io.flutter.plugins.facediscern.flutter.FlutterFaceDiscernPlugin; +import io.flutter.plugins.facediscern.tool.camera.DualCameraHelper; +import io.flutter.plugins.facediscern.tool.camera.callback.CameraListener; +import io.flutter.plugins.facediscern.tool.face.constants.LivenessType; +import io.flutter.plugins.facediscern.tool.face.help.FaceRectTransformer; +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; +import io.flutter.plugins.facediscern.tool.face.widget.FaceRectView; +import io.flutter.plugins.facediscern.ui.base.BaseActivity; +import io.flutter.plugins.facediscern.ui.viewmodel.FaceDiscernViewModel; +import io.flutter.plugin.common.MethodChannel; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class FaceDiscern extends BaseActivity implements ViewTreeObserver.OnGlobalLayoutListener { + + private static final String TAG = "人脸识别页面"; + + private static final String[] NEEDED_PERMISSIONS = new String[]{ + Manifest.permission.CAMERA, + Manifest.permission.READ_PHONE_STATE + }; + + private FaceRectTransformer rgbFaceRectTransformer; + + // 彩色摄像头 + private DualCameraHelper rgbCameraHelper; + + private FaceDiscernViewModel viewModel; + + private ActivityFaceDiscernBinding binding; + + private Boolean previewLock = false; + + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + EdgeToEdge.enable(this); + binding = DataBindingUtil.setContentView(this, R.layout.activity_face_discern); + runOnUiThread(() -> { + this.initViewModel(); + initView(); + MethodChannel channel = FlutterFaceDiscernPlugin.staticChannel; + if (Objects.nonNull(channel)) channel.invokeMethod("faceDiscernPageInit", null); + }); + Log.e(TAG, "相机触发onCreate"); + } + + @Override + public void onGlobalLayout() { + binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(FaceDiscern.this); + if (!checkPermissions(NEEDED_PERMISSIONS)) { + ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, 0x001); + return; + } + initRgbCamera(); + Log.e(TAG, "相机onGlobalLayout"); + } + + // 请求权限的回调 + @Override + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + super.afterRequestPermission(requestCode, isAllGranted); + if (!checkPermissions(NEEDED_PERMISSIONS)) { + return; + } + initRgbCamera(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Log.e(TAG, "相机触发onNewIntent"); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (rgbCameraHelper != null) { + rgbCameraHelper.release(); + rgbCameraHelper = null; + } + if (binding != null) binding = null; + viewModel.destroy(); + Log.e(TAG, "相机触发onDestroy"); + } + + @Override + protected void onPause() { + super.onPause(); + if (rgbCameraHelper != null) { + rgbCameraHelper.stop(); + } + if (viewModel != null) viewModel.stop(); + binding.dualCameraFaceRectView.clearFaceInfo(); + previewLock = true; + Log.i(TAG, "相机onPause"); + } + + @Override + protected void onResume() { + super.onResume(); + MethodChannel channel = FlutterFaceDiscernPlugin.staticChannel; + if (Objects.nonNull(channel)) { + channel.invokeMethod("faceDiscernPageInit", null); + } + if (rgbCameraHelper != null) { + rgbCameraHelper.resumePreview(); + } + scheduler.schedule(() -> { + try { + if (viewModel != null) { + viewModel.stop(); + previewLock = false; + Log.i(TAG, "相机开始识别"); + } + } catch (Exception e) { + Log.e(TAG, "相机开始识别错误", e); + } + }, 500, TimeUnit.MILLISECONDS); + Log.i(TAG, "相机onResume"); + } + + @Override + protected void onStop() { + super.onStop(); + if (rgbCameraHelper != null && !isFinishing()) { + rgbCameraHelper.stop(); + } + } + + // 手机返回按钮 + @Override + public void onBackPressed() { + Log.e(TAG, "相机onBackPressed"); + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.setClassName(this.getPackageName(), this.getPackageName() + ".MainActivity"); + this.startActivity(intent); + MethodChannel channel = FlutterFaceDiscernPlugin.staticChannel; + if (Objects.isNull(channel)) return; + channel.invokeMethod("faceDiscernBack", null); + } + + + /* ***************** start 初始化 start ***************** */ + private void initViewModel() { + viewModel = new ViewModelProvider(this).get(FaceDiscernViewModel.class); + viewModel.init(FaceDiscern.this); + + } + + private void initView() { + binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(FaceDiscern.this); + } + + private void initRgbCamera() { + CameraListener cameraListener = new CameraListener() { + @Override + public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) { + runOnUiThread(() -> { + Camera.Size previewSizeRgb = camera.getParameters().getPreviewSize(); + ViewGroup.LayoutParams layoutParams = adjustPreviewViewSize( + binding.dualCameraTexturePreviewRgb, + binding.dualCameraTexturePreviewRgb, + binding.dualCameraFaceRectView, + previewSizeRgb, + displayOrientation, + 1.0f + ); + rgbFaceRectTransformer = new FaceRectTransformer( + previewSizeRgb.width, + previewSizeRgb.height, + layoutParams.width, + layoutParams.height, + displayOrientation, + cameraId, + isMirror, + true, + true + ); + viewModel.onRgbCameraOpened(camera); + viewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer); + Log.e(TAG, "相机打开,宽:" + previewSizeRgb.width + ",高:" + previewSizeRgb.height + ";layout宽:" + layoutParams.width + ",layout高:" + layoutParams.height + ";"); + }); + } + + @Override + public void onPreview(byte[] data, Camera camera) { + if (previewLock) return; + binding.dualCameraFaceRectView.clearFaceInfo(); + List facePreviewInfoList = viewModel.onPreviewFrame(data, true); + if (facePreviewInfoList != null && rgbFaceRectTransformer != null) { + drawPreviewInfo(facePreviewInfoList); + } + } + + @Override + public void onCameraClosed() { + Log.e(TAG, "相机关闭"); + } + + @Override + public void onCameraError(Exception e) { + Log.e(TAG, "相机错误error", e); + } + + @Override + public void onCameraConfigurationChanged(int cameraID, int displayOrientation) { + + } + }; + // 使用前置相机 + int mCameraId = 0; + int numberOfCameras = Camera.getNumberOfCameras(); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + for (int i = 0; i < numberOfCameras; i++) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + mCameraId = i; + break; + } + } + Point previewSize = new Point(binding.dualCameraTexturePreviewRgb.getMeasuredWidth(), binding.dualCameraTexturePreviewRgb.getMeasuredHeight()); + rgbCameraHelper = new DualCameraHelper.Builder() + .previewViewSize(previewSize) + .previewSize(previewSize) + .rotation(getWindowManager().getDefaultDisplay().getRotation()) + .additionalRotation(0) + .specificCameraId(mCameraId) + .isMirror(false) + .previewOn(binding.dualCameraTexturePreviewRgb) + .cameraListener(cameraListener) + .build(); + rgbCameraHelper.init(); + rgbCameraHelper.start(); + viewModel.setRgbCameraHelper(rgbCameraHelper); + } + /* ***************** end end end ***************** */ + + + private ViewGroup.LayoutParams adjustPreviewViewSize(View rgbPreview, View previewView, FaceRectView faceRectView, Camera.Size previewSize, int displayOrientation, float scale) { + ViewGroup.LayoutParams layoutParams = previewView.getLayoutParams(); + int measuredWidth = previewView.getMeasuredWidth(); + int measuredHeight = previewView.getMeasuredHeight(); + float ratio = ((float) previewSize.height) / (float) previewSize.width; + if (ratio > 1) { + ratio = 1 / ratio; + } + if (displayOrientation % 180 == 0) { + layoutParams.width = measuredWidth; + layoutParams.height = (int) (measuredWidth * ratio); + } else { + layoutParams.height = measuredHeight; + layoutParams.width = (int) (measuredHeight * ratio); + } + if (scale < 1f) { + ViewGroup.LayoutParams rgbParam = rgbPreview.getLayoutParams(); + layoutParams.width = (int) (rgbParam.width * scale); + layoutParams.height = (int) (rgbParam.height * scale); + } else { + layoutParams.width *= scale; + layoutParams.height *= scale; + } + + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + + if (layoutParams.width >= metrics.widthPixels) { + float viewRatio = layoutParams.width / ((float) metrics.widthPixels); + layoutParams.width /= viewRatio; + layoutParams.height /= viewRatio; + } + if (layoutParams.height >= metrics.heightPixels) { + float viewRatio = layoutParams.height / ((float) metrics.heightPixels); + layoutParams.width /= viewRatio; + layoutParams.height /= viewRatio; + } + + previewView.setLayoutParams(layoutParams); + faceRectView.setLayoutParams(layoutParams); + return layoutParams; + } + + /** + * 绘制RGB、IR画面的实时人脸信息 + * + * @param facePreviewInfoList RGB画面的实时人脸信息 + */ + private void drawPreviewInfo(List facePreviewInfoList) { + if (rgbFaceRectTransformer != null) { + List rgbDrawInfoList = viewModel.getDrawInfo(facePreviewInfoList, LivenessType.RGB, false); + binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/ui/base/BaseActivity.java b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/base/BaseActivity.java new file mode 100644 index 0000000..ad124f6 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/base/BaseActivity.java @@ -0,0 +1,82 @@ +package io.flutter.plugins.facediscern.ui.base; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class BaseActivity extends AppCompatActivity { + private static ExecutorService executor; + private static List activityList; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (activityList == null) { + activityList = new ArrayList<>(); + } + if (executor == null) { + executor = new ThreadPoolExecutor( + 1, + 1, + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + r -> { + Thread t = new Thread(r); + t.setName("activity-sub-thread-" + t.getId()); + return t; + }); + } + activityList.add(this); + } + + @Override + protected void onDestroy() { + activityList.remove(this); + if (activityList.isEmpty()) { + activityList = null; + executor.shutdown(); + executor = null; + } + super.onDestroy(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + boolean isAllGranted = true; + for (int grantResult : grantResults) { + isAllGranted &= (grantResult == PackageManager.PERMISSION_GRANTED); + } + afterRequestPermission(requestCode, isAllGranted); + } + + // 请求权限的回调 + protected void afterRequestPermission(int requestCode, boolean isAllGranted) { + + } + + // 检查权限 + protected boolean checkPermissions(String[] neededPermissions) { + if (neededPermissions == null || neededPermissions.length == 0) { + return true; + } + boolean allGranted = true; + for (String neededPermission : neededPermissions) { + allGranted &= ContextCompat.checkSelfPermission(this, neededPermission) == PackageManager.PERMISSION_GRANTED; + } + return allGranted; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/ui/viewmodel/FaceDiscernViewModel.java b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/viewmodel/FaceDiscernViewModel.java new file mode 100644 index 0000000..dbe491c --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/viewmodel/FaceDiscernViewModel.java @@ -0,0 +1,309 @@ +package io.flutter.plugins.facediscern.ui.viewmodel; + + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.Camera; +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.arcsoft.face.AgeInfo; +import com.arcsoft.face.FaceEngine; +import com.arcsoft.face.FaceInfo; +import com.arcsoft.face.GenderInfo; +import com.arcsoft.face.LivenessInfo; +import com.arcsoft.face.LivenessParam; +import com.arcsoft.face.enums.DetectFaceOrientPriority; +import com.arcsoft.face.enums.DetectMode; +import com.google.gson.Gson; + +import io.flutter.plugins.facediscern.tool.camera.DualCameraHelper; +import io.flutter.plugins.facediscern.tool.camera.config.PreviewConfig; +import io.flutter.plugins.facediscern.tool.face.constants.FaceBrodConstant; +import io.flutter.plugins.facediscern.tool.face.constants.LivenessType; +import io.flutter.plugins.facediscern.tool.face.constants.RecognizeColor; +import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus; +import io.flutter.plugins.facediscern.tool.face.help.FaceHelper; +import io.flutter.plugins.facediscern.tool.face.help.FaceRectTransformer; +import io.flutter.plugins.facediscern.tool.face.help.callback.RecognizeCallback; +import io.flutter.plugins.facediscern.tool.face.model.CompareResult; +import io.flutter.plugins.facediscern.tool.face.model.FaceEntity; +import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo; +import io.flutter.plugins.facediscern.tool.face.model.RecognizeConfiguration; +import io.flutter.plugins.facediscern.tool.face.widget.FaceRectView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + +public class FaceDiscernViewModel extends ViewModel implements RecognizeCallback { + + private final String TAG = "FaceDiscernViewModel"; + + // 相机预览的分辨率 + private Camera.Size previewSize; + + /// ****************** start 引擎 start ****************** + + // 用于特征提取的引擎 + private FaceEngine frEngine; + // VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测 + private FaceEngine ftEngine; + // VIDEO模式人脸口罩检测引擎,用于预览帧人脸口罩检测 + private FaceEngine maskEngine; + // IMAGE模式活体检测引擎,用于预览帧人脸活体检测 + private FaceEngine flEngine; + // 各个引擎初始化的错误码 + private MutableLiveData ftInitCode = new MutableLiveData<>(); + private MutableLiveData frInitCode = new MutableLiveData<>(); + private MutableLiveData flInitCode = new MutableLiveData<>(); + + /// ****************** end 引擎 end ****************** + + private Rect recognizeArea = new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + + // 当前活体检测的检测类型 + private LivenessType livenessType; + + private FaceRectTransformer rgbFaceRectTransformer; + + private PreviewConfig previewConfig; + + // 人脸操作辅助类,推帧即可,内部会进行特征提取、识别 + private FaceHelper faceHelper; + + // 人脸库数据加载完成 + private boolean loadFaceList = true; + + // IR活体数据 + private byte[] irNV21 = null; + + public static final int REGISTER_STATUS_READY = 0; + // 注册人脸状态码,注册中 + public static final int REGISTER_STATUS_PROCESSING = 1; + // 册人脸状态码,注册结束(无论成功失败) + public static final int REGISTER_STATUS_DONE = 2; + + private int registerStatus = REGISTER_STATUS_DONE; + + private Intent flutterFaceCallbackIntent; + + private Context context; + + private DualCameraHelper rgbCameraHelper; + + private HashMap faceDiscern = null; + + private MutableLiveData recognizeConfiguration = new MutableLiveData<>(); + + public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) { + faceHelper.setRgbFaceRectTransformer(rgbFaceRectTransformer); + } + + public void setRgbCameraHelper(DualCameraHelper rgbCameraHelper) { + this.rgbCameraHelper = rgbCameraHelper; + } + + public void setContext(Context context) { + this.context = context; + } + + private void updateRegisterStatus(int status) { + registerStatus = status; + } + + // 初始化引擎 + public void init(Context context) { + setContext(context); + boolean switchCamera = false; + previewConfig = new PreviewConfig(switchCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK, switchCamera ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT, 0, 0); + // 填入在设置界面设置好的配置信息 + boolean enableLive = false; + boolean enableFaceQualityDetect = true; + boolean enableFaceMoveLimit = false; + boolean enableFaceSizeLimit = false; + int faceSizeLimit = 160; + int faceMoveLimit = 20; + int maxDetectFaces = 1; + boolean keepMaxFace = true; + float similarThreshold = 0.8F; + float imageQualityNoMaskRecognizeThreshold = 0.49F; + float imageQualityMaskRecognizeThreshold = 0.29F; + RecognizeConfiguration configuration = new RecognizeConfiguration.Builder().enableFaceMoveLimit(enableFaceMoveLimit).enableFaceSizeLimit(enableFaceSizeLimit).faceSizeLimit(faceSizeLimit).faceMoveLimit(faceMoveLimit).enableLiveness(enableLive).enableImageQuality(enableFaceQualityDetect).maxDetectFaces(maxDetectFaces).keepMaxFace(keepMaxFace).similarThreshold(similarThreshold).imageQualityNoMaskRecognizeThreshold(imageQualityNoMaskRecognizeThreshold).imageQualityMaskRecognizeThreshold(imageQualityMaskRecognizeThreshold).livenessParam(new LivenessParam(0.5F, 0.5F)).build(); + + ftEngine = new FaceEngine(); + ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_ALL_OUT, 1, FaceEngine.ASF_FACE_DETECT)); + maskEngine = new FaceEngine(); + ftInitCode.postValue(maskEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, 1, FaceEngine.ASF_MASK_DETECT)); + + frEngine = new FaceEngine(); + int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION; + if (enableFaceQualityDetect) { + frEngineMask |= FaceEngine.ASF_IMAGEQUALITY; + } + frInitCode.postValue(frEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, 10, frEngineMask)); + //启用活体检测时,才初始化活体引擎 + if (enableLive) { + flEngine = new FaceEngine(); + int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT)); + flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask)); + LivenessParam livenessParam = new LivenessParam(0.5F, 0.5F); + flEngine.setLivenessParam(livenessParam); + } + recognizeConfiguration.setValue(configuration); + flutterFaceCallbackIntent = new Intent(FaceBrodConstant.FACE_GLOBAL_INTENT); + } + + private void initFaceHelper(Camera.Size lastPreviewSize) { + if (faceHelper == null || lastPreviewSize == null || lastPreviewSize.width != previewSize.width || lastPreviewSize.height != previewSize.height) { + Integer trackedFaceCount = null; + // 记录切换时的人脸序号 + if (faceHelper != null) { + trackedFaceCount = faceHelper.getTrackedFaceCount(); + faceHelper.release(); + } + int horizontalOffset = 0; + int verticalOffset = 0; + int maxDetectFaceNum = 1; + faceHelper = new FaceHelper.Builder().ftEngine(ftEngine).maskEngine(maskEngine).frEngine(frEngine).flEngine(flEngine).frQueueSize(maxDetectFaceNum).flQueueSize(maxDetectFaceNum).previewSize(previewSize).recognizeCallback(this).recognizeConfiguration(recognizeConfiguration.getValue()).trackedFaceCount(trackedFaceCount == null ? 0 : trackedFaceCount).dualCameraFaceInfoTransformer(faceInfo -> { + FaceInfo irFaceInfo = new FaceInfo(faceInfo); + irFaceInfo.getRect().offset(horizontalOffset, verticalOffset); + return irFaceInfo; + }).build(); + } + } + + + // 当相机打开时由activity调用,进行一些初始化操作 + public void onRgbCameraOpened(Camera camera) { + Camera.Size lastPreviewSize = previewSize; + previewSize = camera.getParameters().getPreviewSize(); + // 切换相机的时候可能会导致预览尺寸发生变化 + initFaceHelper(lastPreviewSize); + } + + public Point loadPreviewSize() { + String[] size = new String[]{"1280", "720"}; + return new Point(Integer.parseInt(size[0]), Integer.parseInt(size[1])); + } + + /** + * 传入可见光相机预览数据 + * + * @param nv21 可见光相机预览数据 + * @param doRecognize 是否进行识别 + * @return 当前帧的检测结果信息 + */ + public List onPreviewFrame(byte[] nv21, boolean doRecognize) { + if (faceHelper != null) { + if (!loadFaceList) { + return null; + } + if (livenessType == LivenessType.IR && irNV21 == null) { + return null; + } + return faceHelper.onPreviewFrame(nv21, irNV21, doRecognize); + } + return null; + } + + + /** + * 根据预览信息生成绘制信息 + * + * @param facePreviewInfoList 预览信息 + * @return 绘制信息 + */ + public List getDrawInfo(List facePreviewInfoList, LivenessType livenessType, boolean drawRectInfo) { + List drawInfoList = new ArrayList<>(); + for (int i = 0; i < facePreviewInfoList.size(); i++) { + int trackId = facePreviewInfoList.get(i).getTrackId(); + String name = faceHelper.getName(trackId); + Integer liveness = faceHelper.getLiveness(trackId); + Integer recognizeStatus = faceHelper.getRecognizeStatus(trackId); + + // 根据识别结果和活体结果设置颜色 + int color = RecognizeColor.COLOR_UNKNOWN; + if (recognizeStatus != null) { + if (recognizeStatus == RequestFeatureStatus.FAILED) { + color = RecognizeColor.COLOR_FAILED; + } + if (recognizeStatus == RequestFeatureStatus.SUCCEED) { + color = RecognizeColor.COLOR_SUCCESS; + } + } + if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) { + color = RecognizeColor.COLOR_FAILED; + } + + drawInfoList.add(new FaceRectView.DrawInfo(livenessType == LivenessType.RGB ? facePreviewInfoList.get(i).getRgbTransformedRect() : facePreviewInfoList.get(i).getIrTransformedRect(), GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness == null ? LivenessInfo.UNKNOWN : liveness, color, name == null ? "" : name, facePreviewInfoList.get(i).getFaceInfoRgb().getIsWithinBoundary(), facePreviewInfoList.get(i).getForeRect(), facePreviewInfoList.get(i).getFaceInfoRgb().getFaceAttributeInfo(), drawRectInfo, livenessType == LivenessType.RGB)); + } + return drawInfoList; + } + + public void destroy() { + if (faceHelper != null) { + faceHelper.release(); + } + // FaceServer.getInstance().release(); + } + + public void stop() { + if (faceHelper != null) { + faceHelper.stop(); + } + } + + // 识别结果回调 + @Override + public void onRecognized(CompareResult compareResult, Integer liveness, boolean similarPass) { + try { + if (!similarPass) return; + HashMap json = new HashMap<>(); + json.put("similar", compareResult.getSimilar()); + json.put("cost", compareResult.getCost()); + if (Objects.nonNull(compareResult.getFaceEntity())) { + FaceEntity face = compareResult.getFaceEntity(); + json.put("faceId", face.getFaceId()); + json.put("sampleId", face.getSampleId()); + json.put("userId", face.getUserId()); + json.put("userName", face.getUserName()); + } + faceDiscern = json; + } catch (Exception e) { + Log.e(TAG, "识别结果回调,错误error:" + e.getMessage(), e); + } + } + + // 提示文字变更的回调 + @Override + public void onNoticeChanged(String notice) { + try { + if (!Objects.equals(notice, "")) return; + if (Objects.isNull(faceDiscern)) return; + if (flutterFaceCallbackIntent == null) { + flutterFaceCallbackIntent = new Intent(FaceBrodConstant.FACE_GLOBAL_INTENT); + } + Log.i(TAG, "人脸识别成功json:" + new Gson().toJson(faceDiscern)); + flutterFaceCallbackIntent.putExtra("json", new Gson().toJson(faceDiscern)); + context.sendBroadcast(flutterFaceCallbackIntent); + faceDiscern = null; + if (context instanceof Activity) { + Thread.sleep(500); + String packageName = context.getPackageName(); + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.setClassName(packageName, packageName + ".MainActivity"); + context.startActivity(intent); + } + } catch (Exception e) { + Log.e(TAG, "提示文字变更的回调,错误error:" + e.getMessage(), e); + } + } +} diff --git a/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_face.so b/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_face.so new file mode 100644 index 0000000..d473515 Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_face.so differ diff --git a/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_face_engine.so b/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_face_engine.so new file mode 100644 index 0000000..62fe98f Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_face_engine.so differ diff --git a/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_image_util.so b/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_image_util.so new file mode 100644 index 0000000..490a2e9 Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/libarcsoft_image_util.so differ diff --git a/android/app/src/main/jniLibs/arm64-v8a/libmsc.so b/android/app/src/main/jniLibs/arm64-v8a/libmsc.so new file mode 100644 index 0000000..a09c4b5 Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/libmsc.so differ diff --git a/android/app/src/main/jniLibs/arm64-v8a/librknnrt.so b/android/app/src/main/jniLibs/arm64-v8a/librknnrt.so new file mode 100644 index 0000000..dadb2a9 Binary files /dev/null and b/android/app/src/main/jniLibs/arm64-v8a/librknnrt.so differ diff --git a/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt b/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt new file mode 100644 index 0000000..7e05329 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.pj_supermarket_cloud + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_face_discern.xml b/android/app/src/main/res/layout/activity_face_discern.xml new file mode 100644 index 0000000..da8201a --- /dev/null +++ b/android/app/src/main/res/layout/activity_face_discern.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/flutter_presentation_view.xml b/android/app/src/main/res/layout/flutter_presentation_view.xml new file mode 100644 index 0000000..272680d --- /dev/null +++ b/android/app/src/main/res/layout/flutter_presentation_view.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..2597170 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0ce3064 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +#distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..5c454f5 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,28 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + maven { url "https://maven.aliyun.com/repository/google" } + maven { url "https://maven.aliyun.com/repository/central" } + maven { url "https://maven.aliyun.com/repository/gradle-plugin" } + maven { url "https://jitpack.io" } + google() + mavenCentral() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" diff --git a/assets/crt/emqxsl-ca.crt b/assets/crt/emqxsl-ca.crt new file mode 100644 index 0000000..8f3aea9 --- /dev/null +++ b/assets/crt/emqxsl-ca.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/assets/images/default_avatar.png b/assets/images/default_avatar.png new file mode 100644 index 0000000..98c75ba Binary files /dev/null and b/assets/images/default_avatar.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..ed792d5 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/icons_launcher.yaml b/icons_launcher.yaml new file mode 100644 index 0000000..258cfff --- /dev/null +++ b/icons_launcher.yaml @@ -0,0 +1,5 @@ +icons_launcher: + image_path: "assets/images/logo.png" + platforms: + android: + enable: true \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..2d477fa --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return ScreenUtilInit( + designSize: const Size(1920, 1080), + minTextAdapt: true, + splitScreenMode: true, + builder: (_, child) { + final appTheme = Get.put(AppThemeControl()); + final fTheme = appTheme.theme.value; + final sizeScalar = appTheme.sizeScalar.value; + final themeData = FThemeData.inherit( + colorScheme: fTheme.colorScheme.copyWith(), + typography: fTheme.typography.scale(sizeScalar: sizeScalar), + style: fTheme.style.copyWith( + borderRadius: BorderRadius.all( + Radius.circular(12.w), + ), + borderWidth: 1, + iconStyle: fTheme.style.iconStyle.copyWith( + size: fTheme.typography.xl2.fontSize, + ), + ), + ); + return GetMaterialApp( + title: '收银机', + initialRoute: RouteNames.load, + getPages: RoutePages.list, + enableLog: true, + debugShowCheckedModeBanner: false, + builder: EasyLoading.init( + builder: (context, child) => FTheme( + data: themeData, + child: child!, + ), + ), + ); + }); + } +} + +class SubApp extends StatelessWidget { + const SubApp({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); + } +} diff --git a/lib/common/api/hard_api.dart b/lib/common/api/hard_api.dart new file mode 100644 index 0000000..7e32374 --- /dev/null +++ b/lib/common/api/hard_api.dart @@ -0,0 +1,141 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class HardApi { + /// 获取硬件配置信息 + static Future bind(dynamic pa) async { + final res = await HttpManage().dio.post( + "/hard/hard-api/ad/bind", + data: pa, + ); + if (res.data == null) return null; + return ServiceHardware.fromJson(res.data); + } + + /// 获取硬件配置信息 + static Future> get(dynamic pa) async { + final res = await HttpManage().dio.get>( + "/hard/hard-api/ad/get", + queryParameters: pa, + ); + if (res.data?.data == null) { + return res.data!; + } + final map = res.data!.data as Map; + final typeData = ServiceHardware.fromJson(map); + res.data?.data = typeData; + return res.data!; + } + + /// 获取用户信息 + static Future> getUserInfoById( + dynamic formData, + ) async { + final res = await HttpManage().dio.get>( + "/hard/hard-api/ad/get-user", + queryParameters: formData, + ); + if (res.data?.data == null) { + return res.data!; + } + final map = res.data!.data as Map; + final typeData = ServiceUser.fromJson(map); + res.data?.data = typeData; + return res.data!; + } + + /// 获取用户信息 + static Future> getUserByCard( + dynamic formData, + ) async { + final res = await HttpManage().dio.get>( + "/hard/hard-api/ad/get-user-by-card", + queryParameters: formData, + ); + if (res.data?.data == null) { + return res.data!; + } + final map = res.data!.data as Map; + final typeData = ServiceUser.fromJson(map); + res.data?.data = typeData; + return res.data!; + } + + /// 创建硬件订单 + static Future createHardwareOrder(dynamic formData) async { + final res = await HttpManage().dio.post( + "/hard/hard-api/ad/create-order", + data: formData, + ); + return res.data as ServiceBase; + } + + /// 获取订单详情 + static Future getOrder(dynamic formData) async { + final res = await HttpManage().dio.get( + "/hard/hard-api/ad/get-order", + queryParameters: formData, + ); + return res.data as ServiceBase; + } + + /// 获取订单购物车 + static Future> getCarByOrderId( + dynamic formData, + ) async { + final res = await HttpManage().dio.get( + "/hard/hard-api/ad/get-by-order-id", + queryParameters: formData, + ); + if (res.data?.data == null) { + return res.data!; + } + final typeData = + res.data?.data?.map((e) => ServiceCar.fromJson(e)).toList(); + res.data?.data = typeData; + return res.data!; + } + + /// 核销订单 + static Future cancelQrCode(dynamic formData) async { + final res = await HttpManage().dio.post( + "/hard/hard-api/ad/cancel-code", + data: formData, + ); + return res.data!; + } + + /// 生成发票 + static Future> generateInvoice( + dynamic formData, + ) async { + final res = await HttpManage().dio.get>( + "/hard/hard-api/ad/generate-invoice", + queryParameters: formData, + ); + if (res.data?.data == null) { + return res.data!; + } + final map = res.data!.data as Map; + final typeData = ServiceGenerateInvoice.fromJson(map); + res.data?.data = typeData; + return res.data!; + } + + /// 打印成功 + static Future printInvoice(dynamic formData) async { + final res = await HttpManage().dio.get( + "/hard/hard-api/ad/print-invoice", + queryParameters: formData, + ); + return res.data as ServiceBase; + } + + /// 离线同步 + static Future syncOfflineOrder(dynamic formData) async { + final res = await HttpManage().dio.post( + "/hard/hard-api/ad/sync-offline-order", + data: formData, + ); + return res.data as ServiceBase; + } +} diff --git a/lib/common/api/index.dart b/lib/common/api/index.dart new file mode 100644 index 0000000..8b01c9b --- /dev/null +++ b/lib/common/api/index.dart @@ -0,0 +1,6 @@ +library; + +export 'product_api.dart'; +export './hard_api.dart'; +export './user_api.dart'; +export './wallet_api.dart'; diff --git a/lib/common/api/product_api.dart b/lib/common/api/product_api.dart new file mode 100644 index 0000000..0c61901 --- /dev/null +++ b/lib/common/api/product_api.dart @@ -0,0 +1,57 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class ProductApi { + /// 菜品分类 + static Future?> cookbookClassify( + dynamic pa, + ) async { + final res = await HttpManage().dio.get( + "/canteen/canteenCategory/getAllSimple", + queryParameters: pa, + ); + if (res.data == null) return null; + return res.data + ?.map((e) => ServiceClassify.fromJson(e)) + .toList(); + } + + /// 菜品商品 + static Future?> canteenProduct( + dynamic pa, + ) async { + final res = await HttpManage().dio.get( + "/canteen/canteenDailyMenu/ad/getCanteenList", + queryParameters: pa, + ); + if (res.data == null) return null; + return res.data + ?.map((e) => ServiceProduct.fromJson(e)) + .toList(); + } + + /// 超市分类 + static Future?> supermarketClassify( + dynamic pa, + ) async { + final res = await HttpManage().dio.get( + "/supermarket-classify", + queryParameters: pa, + ); + if (res.data == null) return null; + return res.data + ?.map((e) => ServiceClassify.fromJson(e)) + .toList(); + } + + /// 超市商品 + static Future?> supermarketProduct(dynamic pa) async { + final res = await HttpManage().dio.get( + "/supermarket-product", + queryParameters: pa, + ); + if (res.data == null) return null; + return res.data + ?.map((e) => ServiceProduct.fromJson(e)) + .toList(); + } +} diff --git a/lib/common/api/user_api.dart b/lib/common/api/user_api.dart new file mode 100644 index 0000000..2b2af8d --- /dev/null +++ b/lib/common/api/user_api.dart @@ -0,0 +1,63 @@ +import 'package:dio/dio.dart' as dio; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class UserApi { + /// 硬件管理登录 + static Future login(dynamic pa) async { + final res = await HttpManage().dio.post( + "/auth/oauth2/token", + queryParameters: { + "username": pa["username"], + "randomStr": "", + "code": "", + "grant_type": pa["grant_type"], + "scope": pa["scope"], + }, + data: { + "password": pa["password"], + }, + options: dio.Options( + headers: { + "Authorization": EncryptionTool.getLoginAuthorization(), + "skipToken": true, + "content-type": "application/x-www-form-urlencoded", + }, + ), + ); + return res.data; + } + + /// 刷新token + static Future> refreshToken(dynamic pa) async { + final res = await HttpManage().dio.post( + "/refresh-token", + queryParameters: pa, + ); + if (res.data?.data == null) { + return res.data; + } + final map = res.data!.data as Map; + final typeData = ServiceAuth.fromJson(map); + res.data?.data = typeData; + return res.data; + } + + /// 获取人脸 + static Future> getFaceList( + dynamic formData, + ) async { + final res = await HttpManage().dio.get( + "/admin/user-face/ad/find-face", + queryParameters: formData, + ); + if (res.data?.data == null) { + return res.data; + } + final typeData = res.data?.data + ?.map((e) => ServiceAsyncFace.fromJson(e)) + .toList(); + res.data?.data = typeData; + return res.data; + } +} diff --git a/lib/common/api/wallet_api.dart b/lib/common/api/wallet_api.dart new file mode 100644 index 0000000..3c56499 --- /dev/null +++ b/lib/common/api/wallet_api.dart @@ -0,0 +1,42 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class WalletApi { + /// 支付配置 + static Future getPaySetting() async { + final res = await HttpManage().dio.get( + "/wallet/wallet-setting/ad/get-setting", + ); + if (res.data == null) return null; + return ServicePaySetting.fromJson(res.data); + } + + /// 开卡列表 + static Future openCardList(dynamic pa) async { + final res = await HttpManage().dio.get( + "/wallet/wallet-active/ad/open-card-list", + queryParameters: pa, + ); + if (res.data == null) return null; + return res.data as ServiceBase; + } + + /// 开卡成功 + static Future openCardSuccess(dynamic pa) async { + final res = await HttpManage().dio.get( + "/wallet/wallet-active/ad/open-card-success", + queryParameters: pa, + ); + if (res.data == null) return false; + return true; + } + + /// 重新激活 + static Future refreshAvatar(dynamic pa) async { + final res = await HttpManage().dio.get( + "/wallet/wallet-active/ad/refresh-avatar", + queryParameters: pa, + ); + if (res.data == null) return false; + return true; + } +} diff --git a/lib/common/components/index.dart b/lib/common/components/index.dart new file mode 100644 index 0000000..fa8d106 --- /dev/null +++ b/lib/common/components/index.dart @@ -0,0 +1,3 @@ +library; + +// export './xxxx.dart'; diff --git a/lib/common/extension/index.dart b/lib/common/extension/index.dart new file mode 100644 index 0000000..fa8d106 --- /dev/null +++ b/lib/common/extension/index.dart @@ -0,0 +1,3 @@ +library; + +// export './xxxx.dart'; diff --git a/lib/common/i18n/index.dart b/lib/common/i18n/index.dart new file mode 100644 index 0000000..fa8d106 --- /dev/null +++ b/lib/common/i18n/index.dart @@ -0,0 +1,3 @@ +library; + +// export './xxxx.dart'; diff --git a/lib/common/index.dart b/lib/common/index.dart new file mode 100644 index 0000000..0e997ad --- /dev/null +++ b/lib/common/index.dart @@ -0,0 +1,13 @@ +library; + +export 'api/index.dart'; +export 'components/index.dart'; +export 'extension/index.dart'; +export 'i18n/index.dart'; +export 'models/index.dart'; +export 'routers/index.dart'; +export 'services/index.dart'; +export 'style/index.dart'; +export 'utils/index.dart'; +export 'values/index.dart'; +export 'widgets/index.dart'; diff --git a/lib/common/models/api/base_api_models.dart b/lib/common/models/api/base_api_models.dart new file mode 100644 index 0000000..373ca54 --- /dev/null +++ b/lib/common/models/api/base_api_models.dart @@ -0,0 +1,40 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'base_api_models.g.dart'; + +@JsonSerializable() +class ServiceBase { + String? msg; + int? code; + dynamic data; + + ServiceBase({ + this.msg, + this.code, + this.data, + }); + + factory ServiceBase.fromJson(Map json) => + _$ServiceBaseFromJson(json); + + Map toJson() => _$ServiceBaseToJson(this); +} + +@JsonSerializable() +class ServiceListBase { + String? msg; + int? code; + @JsonKey(includeFromJson: false, includeToJson: false) + List? data; + + ServiceListBase({ + this.msg, + this.code, + this.data, + }); + + factory ServiceListBase.fromJson(Map json) => + _$ServiceListBaseFromJson(json); + + Map toJson() => _$ServiceListBaseToJson(this); +} diff --git a/lib/common/models/api/base_api_models.g.dart b/lib/common/models/api/base_api_models.g.dart new file mode 100644 index 0000000..7625156 --- /dev/null +++ b/lib/common/models/api/base_api_models.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'base_api_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ServiceBase _$ServiceBaseFromJson(Map json) => + ServiceBase( + msg: json['msg'] as String?, + code: (json['code'] as num?)?.toInt(), + data: json['data'], + ); + +Map _$ServiceBaseToJson(ServiceBase instance) => + { + 'msg': instance.msg, + 'code': instance.code, + 'data': instance.data, + }; + +ServiceListBase _$ServiceListBaseFromJson(Map json) => + ServiceListBase( + msg: json['msg'] as String?, + code: (json['code'] as num?)?.toInt(), + ); + +Map _$ServiceListBaseToJson(ServiceListBase instance) => + { + 'msg': instance.msg, + 'code': instance.code, + }; diff --git a/lib/common/models/api/hard_api_models.dart b/lib/common/models/api/hard_api_models.dart new file mode 100644 index 0000000..a1bcab4 --- /dev/null +++ b/lib/common/models/api/hard_api_models.dart @@ -0,0 +1,203 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'hard_api_models.g.dart'; + +@JsonSerializable() +class ServiceHardware extends ServiceBase { + /// 编号 + final String? id; + + /// 设备名 + final String? name; + + /// 设备类型 + final String? type; + + /// 设备类型enum + final String? typeEnum; + + /// 商户id + final String? merchantId; + + /// 商户部门id + final String? merchantDeptId; + + /// 硬件分组id + final String? hardGroupId; + + /// 商户名 + final String? merchantStr; + + /// mq连接id + final String? mqClientId; + + /// android唯一id + final String? uniqueCode; + + /// 当前设备的topic + final String? mainTopic; + + /// mq地址 + final String? mqSocketAddress; + + /// mq端口 + final String? mqSocketPort; + + /// mq用户名 + final String? mqUsername; + + /// mq密码 + final String? mqPassword; + + /// 同步次数版本 + final int? mqSyncCount; + + /// 人脸appId + final String? faceAppId; + + /// 人脸sdkKey + final String? faceSdkKey; + + /// 人脸activeKey + final String? faceActiveKey; + + ServiceHardware({ + this.id, + this.name, + this.type, + this.typeEnum, + this.merchantId, + this.merchantDeptId, + this.hardGroupId, + this.merchantStr, + this.mqClientId, + this.uniqueCode, + this.mainTopic, + this.mqSocketAddress, + this.mqSocketPort, + this.mqUsername, + this.mqPassword, + this.mqSyncCount, + this.faceAppId, + this.faceSdkKey, + this.faceActiveKey, + }); + + factory ServiceHardware.fromJson(Map json) => + _$ServiceHardwareFromJson(json); + + @override + Map toJson() => _$ServiceHardwareToJson(this); +} + +@JsonSerializable() +class ServiceCar { + /// 编号 + final String? id; + + /// 父购物车id + final String? parentCarId; + + /// 商品名 + final String? productName; + + /// 商品价格 + final int? productPrice; + + /// 商品id + final String? productId; + + /// 数量 + final int? productCount; + + /// 总价(分) + @JsonKey(fromJson: JsonTool.stringToInt, toJson: JsonTool.intToString) + final int? totalPrice; + + /// 商品图片 + final String? productPic; + + ServiceCar({ + this.id, + this.parentCarId, + this.productName, + this.productPrice, + this.productId, + this.productCount, + this.totalPrice, + this.productPic, + }); + + factory ServiceCar.fromJson(Map json) => + _$ServiceCarFromJson(json); + + Map toJson() => _$ServiceCarToJson(this); +} + +@JsonSerializable() +class ServiceGenerateInvoice { + /// 图片 + final String? pic; + + /// 记录id + final String? historyId; + + ServiceGenerateInvoice({ + this.pic, + this.historyId, + }); + + factory ServiceGenerateInvoice.fromJson(Map json) => + _$ServiceGenerateInvoiceFromJson(json); + + Map toJson() => _$ServiceGenerateInvoiceToJson(this); +} + +@JsonSerializable() +class ServiceQRCode { + /// 总id + @JsonKey(fromJson: JsonTool.intToString, toJson: JsonTool.stringToInt) + final String? id; + + /// 子id + @JsonKey(fromJson: JsonTool.intToString, toJson: JsonTool.stringToInt) + final String? itemId; + + /// 类型id 比如钱包类型 + @JsonKey(fromJson: JsonTool.intToString, toJson: JsonTool.stringToInt) + final String? typeId; + + /// 用户id + @JsonKey(fromJson: JsonTool.intToString, toJson: JsonTool.stringToInt) + final String? userId; + + /// 订单类型 + final String? orderType; + + /// 支付类型 + final String? payType; + + /// 过期时间 -1 代表没有限制 + final int? exceedTime; + + /// 额外的用户信息 + final String? extra; + + ServiceQRCode({ + this.id, + this.itemId, + this.typeId, + this.userId, + this.orderType, + this.payType, + this.exceedTime, + this.extra, + }); + + factory ServiceQRCode.fromJson(Map json) => + _$ServiceQRCodeFromJson(json); + + Map toJson() => _$ServiceQRCodeToJson(this); +} diff --git a/lib/common/models/api/hard_api_models.g.dart b/lib/common/models/api/hard_api_models.g.dart new file mode 100644 index 0000000..66b28b2 --- /dev/null +++ b/lib/common/models/api/hard_api_models.g.dart @@ -0,0 +1,120 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'hard_api_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ServiceHardware _$ServiceHardwareFromJson(Map json) => + ServiceHardware( + id: json['id'] as String?, + name: json['name'] as String?, + type: json['type'] as String?, + typeEnum: json['typeEnum'] as String?, + merchantId: json['merchantId'] as String?, + merchantDeptId: json['merchantDeptId'] as String?, + hardGroupId: json['hardGroupId'] as String?, + merchantStr: json['merchantStr'] as String?, + mqClientId: json['mqClientId'] as String?, + uniqueCode: json['uniqueCode'] as String?, + mainTopic: json['mainTopic'] as String?, + mqSocketAddress: json['mqSocketAddress'] as String?, + mqSocketPort: json['mqSocketPort'] as String?, + mqUsername: json['mqUsername'] as String?, + mqPassword: json['mqPassword'] as String?, + mqSyncCount: (json['mqSyncCount'] as num?)?.toInt(), + faceAppId: json['faceAppId'] as String?, + faceSdkKey: json['faceSdkKey'] as String?, + faceActiveKey: json['faceActiveKey'] as String?, + ) + ..msg = json['msg'] as String? + ..code = (json['code'] as num?)?.toInt() + ..data = json['data']; + +Map _$ServiceHardwareToJson(ServiceHardware instance) => + { + 'msg': instance.msg, + 'code': instance.code, + 'data': instance.data, + 'id': instance.id, + 'name': instance.name, + 'type': instance.type, + 'typeEnum': instance.typeEnum, + 'merchantId': instance.merchantId, + 'merchantDeptId': instance.merchantDeptId, + 'hardGroupId': instance.hardGroupId, + 'merchantStr': instance.merchantStr, + 'mqClientId': instance.mqClientId, + 'uniqueCode': instance.uniqueCode, + 'mainTopic': instance.mainTopic, + 'mqSocketAddress': instance.mqSocketAddress, + 'mqSocketPort': instance.mqSocketPort, + 'mqUsername': instance.mqUsername, + 'mqPassword': instance.mqPassword, + 'mqSyncCount': instance.mqSyncCount, + 'faceAppId': instance.faceAppId, + 'faceSdkKey': instance.faceSdkKey, + 'faceActiveKey': instance.faceActiveKey, + }; + +ServiceCar _$ServiceCarFromJson(Map json) => ServiceCar( + id: json['id'] as String?, + parentCarId: json['parentCarId'] as String?, + productName: json['productName'] as String?, + productPrice: (json['productPrice'] as num?)?.toInt(), + productId: json['productId'] as String?, + productCount: (json['productCount'] as num?)?.toInt(), + totalPrice: JsonTool.stringToInt(json['totalPrice'] as String?), + productPic: json['productPic'] as String?, + ); + +Map _$ServiceCarToJson(ServiceCar instance) => + { + 'id': instance.id, + 'parentCarId': instance.parentCarId, + 'productName': instance.productName, + 'productPrice': instance.productPrice, + 'productId': instance.productId, + 'productCount': instance.productCount, + 'totalPrice': JsonTool.intToString(instance.totalPrice), + 'productPic': instance.productPic, + }; + +ServiceGenerateInvoice _$ServiceGenerateInvoiceFromJson( + Map json) => + ServiceGenerateInvoice( + pic: json['pic'] as String?, + historyId: json['historyId'] as String?, + ); + +Map _$ServiceGenerateInvoiceToJson( + ServiceGenerateInvoice instance) => + { + 'pic': instance.pic, + 'historyId': instance.historyId, + }; + +ServiceQRCode _$ServiceQRCodeFromJson(Map json) => + ServiceQRCode( + id: JsonTool.intToString((json['id'] as num?)?.toInt()), + itemId: JsonTool.intToString((json['itemId'] as num?)?.toInt()), + typeId: JsonTool.intToString((json['typeId'] as num?)?.toInt()), + userId: JsonTool.intToString((json['userId'] as num?)?.toInt()), + orderType: json['orderType'] as String?, + payType: json['payType'] as String?, + exceedTime: (json['exceedTime'] as num?)?.toInt(), + extra: json['extra'] as String?, + ); + +Map _$ServiceQRCodeToJson(ServiceQRCode instance) => + { + 'id': JsonTool.stringToInt(instance.id), + 'itemId': JsonTool.stringToInt(instance.itemId), + 'typeId': JsonTool.stringToInt(instance.typeId), + 'userId': JsonTool.stringToInt(instance.userId), + 'orderType': instance.orderType, + 'payType': instance.payType, + 'exceedTime': instance.exceedTime, + 'extra': instance.extra, + }; diff --git a/lib/common/models/api/product_api_models.dart b/lib/common/models/api/product_api_models.dart new file mode 100644 index 0000000..97ba575 --- /dev/null +++ b/lib/common/models/api/product_api_models.dart @@ -0,0 +1,90 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'product_api_models.g.dart'; + +@JsonSerializable() +class ServiceClassify { + /// 分类编号 + String? id; + + /// 分类名称 + String? name; + + /// 移动端分类图 + String? picUrl; + + /// 分类排序 + int? sort; + + /// 开启状态 + int? status; + + /// 系统类型 + int? systemType; + + /// 用餐类型,早餐、中餐、晚餐 + String? categoryType; + + ServiceClassify({ + this.id, + this.name, + this.picUrl, + this.sort, + this.status, + this.systemType, + this.categoryType, + }); + + factory ServiceClassify.fromJson(Map json) => + _$ServiceClassifyFromJson(json); + + Map toJson() => _$ServiceClassifyToJson(this); +} + +@JsonSerializable() +class ServiceProduct { + /// id + String? id; + + /// 菜品id + String? spuId; + + /// 时间类型 (食堂特有) + @JsonKey(fromJson: JsonTool.anyToString) + String? hourType; + + /// 菜品分类编号 + String? categoryId; + + /// 商品名 + String? name; + + /// 商品图片 + String? picUrl; + + /// 价格分 + int? price; + + /// 排序 + int? sort; + + /// 用餐类型,早餐、中餐、晚餐 + String? categoryType; + + ServiceProduct({ + this.id, + this.spuId, + this.categoryId, + this.name, + this.picUrl, + this.hourType, + this.sort, + this.categoryType, + }); + + factory ServiceProduct.fromJson(Map json) => + _$ServiceProductFromJson(json); + + Map toJson() => _$ServiceProductToJson(this); +} diff --git a/lib/common/models/api/product_api_models.g.dart b/lib/common/models/api/product_api_models.g.dart new file mode 100644 index 0000000..6e04015 --- /dev/null +++ b/lib/common/models/api/product_api_models.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_api_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ServiceClassify _$ServiceClassifyFromJson(Map json) => + ServiceClassify( + id: json['id'] as String?, + name: json['name'] as String?, + picUrl: json['picUrl'] as String?, + sort: (json['sort'] as num?)?.toInt(), + status: (json['status'] as num?)?.toInt(), + systemType: (json['systemType'] as num?)?.toInt(), + categoryType: json['categoryType'] as String?, + ); + +Map _$ServiceClassifyToJson(ServiceClassify instance) => + { + 'id': instance.id, + 'name': instance.name, + 'picUrl': instance.picUrl, + 'sort': instance.sort, + 'status': instance.status, + 'systemType': instance.systemType, + 'categoryType': instance.categoryType, + }; + +ServiceProduct _$ServiceProductFromJson(Map json) => + ServiceProduct( + id: json['id'] as String?, + spuId: json['spuId'] as String?, + categoryId: json['categoryId'] as String?, + name: json['name'] as String?, + picUrl: json['picUrl'] as String?, + hourType: JsonTool.anyToString(json['hourType']), + sort: (json['sort'] as num?)?.toInt(), + categoryType: json['categoryType'] as String?, + )..price = (json['price'] as num?)?.toInt(); + +Map _$ServiceProductToJson(ServiceProduct instance) => + { + 'id': instance.id, + 'spuId': instance.spuId, + 'hourType': instance.hourType, + 'categoryId': instance.categoryId, + 'name': instance.name, + 'picUrl': instance.picUrl, + 'price': instance.price, + 'sort': instance.sort, + 'categoryType': instance.categoryType, + }; diff --git a/lib/common/models/api/user_api_models.dart b/lib/common/models/api/user_api_models.dart new file mode 100644 index 0000000..c044cf9 --- /dev/null +++ b/lib/common/models/api/user_api_models.dart @@ -0,0 +1,128 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'user_api_models.g.dart'; + +@JsonSerializable() +class ServiceUser { + /// 用户ID + String? userId; + + /// 用户昵称 + String? username; + + /// 部门ID + String? deptId; + + /// 手机号码 + String? userMobile; + + /// 用户头像 + String? userAvatar; + + /// 钱包id + String? walletId; + + /// 钱包类型id + String? walletTypeId; + + /// 钱包余额 + int? walletBalance; + + /// 是否冻结饭卡 + @JsonKey(fromJson: JsonTool.stringToBoolean, toJson: JsonTool.booleanToString) + bool? walletIsFreeze; + + /// 卡号 + String? walletCardNumber; + + /// 构造函数 + ServiceUser({ + this.userId, + this.username, + this.deptId, + this.userMobile, + this.userAvatar, + this.walletId, + this.walletTypeId, + this.walletBalance, + this.walletIsFreeze, + this.walletCardNumber, + }); + + factory ServiceUser.fromJson(Map json) => + _$ServiceUserFromJson(json); + + Map toJson() => _$ServiceUserToJson(this); +} + +@JsonSerializable() +class ServiceAuth { + /// 用户编号 + final int? userId; + + /// 访问令牌 + final String? accessToken; + + /// 刷新令牌 + final String? refreshToken; + + /// 过期时间 + final int? expiresTime; + + ServiceAuth({ + required this.userId, + required this.accessToken, + required this.refreshToken, + required this.expiresTime, + }); + + factory ServiceAuth.fromJson(Map json) => + _$ServiceAuthFromJson(json); + + Map toJson() => _$ServiceAuthToJson(this); +} + +@JsonSerializable() +class ServiceAsyncFace { + /// 编号 + final String? id; + + /// 人脸编号 + final String? autoCode; + + /// 人脸图片 + final String? faceImg; + + /// 用户id + final String? userId; + + /// 用户名 + final String? username; + + /// 用户头像 + final String? avatar; + + /// 部门id + final String? deptId; + + /// 部门名 + final String? deptName; + + ServiceAsyncFace({ + this.id, + this.autoCode, + this.faceImg, + this.userId, + this.username, + this.avatar, + this.deptId, + this.deptName, + }); + + factory ServiceAsyncFace.fromJson(Map json) => + _$ServiceAsyncFaceFromJson(json); + + Map toJson() => _$ServiceAsyncFaceToJson(this); +} diff --git a/lib/common/models/api/user_api_models.g.dart b/lib/common/models/api/user_api_models.g.dart new file mode 100644 index 0000000..7241d4f --- /dev/null +++ b/lib/common/models/api/user_api_models.g.dart @@ -0,0 +1,74 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_api_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ServiceUser _$ServiceUserFromJson(Map json) => ServiceUser( + userId: json['userId'] as String?, + username: json['username'] as String?, + deptId: json['deptId'] as String?, + userMobile: json['userMobile'] as String?, + userAvatar: json['userAvatar'] as String?, + walletId: json['walletId'] as String?, + walletTypeId: json['walletTypeId'] as String?, + walletBalance: (json['walletBalance'] as num?)?.toInt(), + walletIsFreeze: + JsonTool.stringToBoolean(json['walletIsFreeze'] as String), + walletCardNumber: json['walletCardNumber'] as String?, + ); + +Map _$ServiceUserToJson(ServiceUser instance) => + { + 'userId': instance.userId, + 'username': instance.username, + 'deptId': instance.deptId, + 'userMobile': instance.userMobile, + 'userAvatar': instance.userAvatar, + 'walletId': instance.walletId, + 'walletTypeId': instance.walletTypeId, + 'walletBalance': instance.walletBalance, + 'walletIsFreeze': JsonTool.booleanToString(instance.walletIsFreeze), + 'walletCardNumber': instance.walletCardNumber, + }; + +ServiceAuth _$ServiceAuthFromJson(Map json) => ServiceAuth( + userId: (json['userId'] as num?)?.toInt(), + accessToken: json['accessToken'] as String?, + refreshToken: json['refreshToken'] as String?, + expiresTime: (json['expiresTime'] as num?)?.toInt(), + ); + +Map _$ServiceAuthToJson(ServiceAuth instance) => + { + 'userId': instance.userId, + 'accessToken': instance.accessToken, + 'refreshToken': instance.refreshToken, + 'expiresTime': instance.expiresTime, + }; + +ServiceAsyncFace _$ServiceAsyncFaceFromJson(Map json) => + ServiceAsyncFace( + id: json['id'] as String?, + autoCode: json['autoCode'] as String?, + faceImg: json['faceImg'] as String?, + userId: json['userId'] as String?, + username: json['username'] as String?, + avatar: json['avatar'] as String?, + deptId: json['deptId'] as String?, + deptName: json['deptName'] as String?, + ); + +Map _$ServiceAsyncFaceToJson(ServiceAsyncFace instance) => + { + 'id': instance.id, + 'autoCode': instance.autoCode, + 'faceImg': instance.faceImg, + 'userId': instance.userId, + 'username': instance.username, + 'avatar': instance.avatar, + 'deptId': instance.deptId, + 'deptName': instance.deptName, + }; diff --git a/lib/common/models/api/wallet_api_models.dart b/lib/common/models/api/wallet_api_models.dart new file mode 100644 index 0000000..5244663 --- /dev/null +++ b/lib/common/models/api/wallet_api_models.dart @@ -0,0 +1,54 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'wallet_api_models.g.dart'; + +@JsonSerializable() +class ServicePaySetting { + /// 唯一标识符 + final String? id; + + /// 是否开启每一餐的额外缴费 + @JsonKey(fromJson: JsonTool.stringToBoolean, toJson: JsonTool.booleanToString) + final bool? payAmountEnable; + + /// 每一餐的上限(分) + final int? payAmountMealLimit; + + /// 每一餐的倍数 + final double? payAmountMealTimes; + + /// 售后的退款理由 + final String? afterSaleRefundReasons; + + /// 售后的退货理由 + final String? afterSaleReturnReasons; + + /// 是否开启钱包二维码刷新 + @JsonKey(fromJson: JsonTool.stringToBoolean, toJson: JsonTool.booleanToString) + final bool? refreshQrCodeEnable; + + /// 二维码刷新频率 + final int? refreshQrCodeFre; + + /// 是否开启离线订单审批 + @JsonKey(fromJson: JsonTool.stringToBoolean, toJson: JsonTool.booleanToString) + final bool? offLineOrderApplyEnable; + + ServicePaySetting({ + this.id, + this.payAmountEnable, + this.payAmountMealLimit, + this.payAmountMealTimes, + this.afterSaleRefundReasons, + this.afterSaleReturnReasons, + this.refreshQrCodeEnable, + this.refreshQrCodeFre, + this.offLineOrderApplyEnable, + }); + + factory ServicePaySetting.fromJson(Map json) => + _$ServicePaySettingFromJson(json); + + Map toJson() => _$ServicePaySettingToJson(this); +} diff --git a/lib/common/models/api/wallet_api_models.g.dart b/lib/common/models/api/wallet_api_models.g.dart new file mode 100644 index 0000000..c571f6c --- /dev/null +++ b/lib/common/models/api/wallet_api_models.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'wallet_api_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ServicePaySetting _$ServicePaySettingFromJson(Map json) => + ServicePaySetting( + id: json['id'] as String?, + payAmountEnable: + JsonTool.stringToBoolean(json['payAmountEnable'] as String), + payAmountMealLimit: (json['payAmountMealLimit'] as num?)?.toInt(), + payAmountMealTimes: (json['payAmountMealTimes'] as num?)?.toDouble(), + afterSaleRefundReasons: json['afterSaleRefundReasons'] as String?, + afterSaleReturnReasons: json['afterSaleReturnReasons'] as String?, + refreshQrCodeEnable: + JsonTool.stringToBoolean(json['refreshQrCodeEnable'] as String), + refreshQrCodeFre: (json['refreshQrCodeFre'] as num?)?.toInt(), + offLineOrderApplyEnable: + JsonTool.stringToBoolean(json['offLineOrderApplyEnable'] as String), + ); + +Map _$ServicePaySettingToJson(ServicePaySetting instance) => + { + 'id': instance.id, + 'payAmountEnable': JsonTool.booleanToString(instance.payAmountEnable), + 'payAmountMealLimit': instance.payAmountMealLimit, + 'payAmountMealTimes': instance.payAmountMealTimes, + 'afterSaleRefundReasons': instance.afterSaleRefundReasons, + 'afterSaleReturnReasons': instance.afterSaleReturnReasons, + 'refreshQrCodeEnable': + JsonTool.booleanToString(instance.refreshQrCodeEnable), + 'refreshQrCodeFre': instance.refreshQrCodeFre, + 'offLineOrderApplyEnable': + JsonTool.booleanToString(instance.offLineOrderApplyEnable), + }; diff --git a/lib/common/models/base/base_models.dart b/lib/common/models/base/base_models.dart new file mode 100644 index 0000000..203f4bf --- /dev/null +++ b/lib/common/models/base/base_models.dart @@ -0,0 +1,14 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; + +abstract interface class BaseEnum { + String get code; + String get title; +} + +extension BaseEnumExtension on List { + T? fromCode(String? code) { + if (ObjectUtil.isEmpty(code)) return null; + return firstWhereOrNull((v) => v.code == code); + } +} diff --git a/lib/common/models/database/database_enum_models.dart b/lib/common/models/database/database_enum_models.dart new file mode 100644 index 0000000..eaccc3b --- /dev/null +++ b/lib/common/models/database/database_enum_models.dart @@ -0,0 +1,102 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +/// 设备类型 +enum SysType implements BaseEnum { + supermarket("1", "超市", "智慧超市", "supermarket"), + canteen("2", "食堂", "智慧食堂", "canteen"), + ; + + @override + final String code; + @override + final String title; + final String cnStr; + final String usStr; + + const SysType(this.code, this.title, this.cnStr, this.usStr); +} + +/// 订单类型 +enum OrderType implements BaseEnum { + below("1", "线下购买"), + reserve("2", "线上订餐"), + foreignTicket("3", "外来人员"), + strideDept("4", "跨单位"), + ; + + @override + final String code; + @override + final String title; + + const OrderType(this.code, this.title); +} + +/// 付款方式 +enum OrderPayType implements BaseEnum { + walletDefault("1", "普通钱包支付"), + walletQrCode("2", "钱包二维码支付"), + walletCard("3", "钱包饭卡支付"), + face("4", "人脸识别支付"), + strideDeptQrCode("5", "跨单位二维码支付"), + foreignTicketQrCode("6", "外来人员二维码支付"), + reserveQrCode("7", "线上订餐二维码支付"), + ; + + @override + final String code; + @override + final String title; + + const OrderPayType(this.code, this.title); +} + +/// 订单状态 +enum OrderStatus implements BaseEnum { + // 查询使用 + all("-1", "未付款"), + + nonePay("1", "未付款"), + pay("5", "已付款"), + offlinePay("6", "本地支付"), + refund("10", "已退订"), + ; + + @override + final String code; + @override + final String title; + + const OrderStatus(this.code, this.title); +} + +// 打印状态 +enum PrintStatus implements BaseEnum { + none("10", "未打印"), + success("20", "打印成功"), + sync("30", "同步成功"), + error("40", "打印失败"), + ; + + @override + final String code; + @override + final String title; + + const PrintStatus(this.code, this.title); +} + +enum ProductHourType implements BaseEnum { + all("-1", "查询全部"), + morning("1", "早餐"), + noon("2", "中餐"), + night("3", "晚餐"), + ; + + @override + final String code; + @override + final String title; + + const ProductHourType(this.code, this.title); +} diff --git a/lib/common/models/database/database_models.dart b/lib/common/models/database/database_models.dart new file mode 100644 index 0000000..ae0505e --- /dev/null +++ b/lib/common/models/database/database_models.dart @@ -0,0 +1,221 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'database_models.g.dart'; + +@JsonSerializable() +class SysSetting extends Equatable { + /// 设备类型 + SysType sysType; + + /// mac地址 + String? androidId; + + /// 服务端ID + String? serviceId; + + /// 服务单设备名 + String? serviceName; + + /// 商户id + String? serviceMerchantId; + + /// 商户部门id + String? merchantDeptId; + + /// 硬件分组id + String? hardGroupId; + + /// mq地址 + String? mqSocketAddress; + + /// mq端口 + String? mqSocketPort; + + /// mq用户名 + String? mqUsername; + + /// mq密码 + String? mqPassword; + + /// 当前设备的topic + String? mainTopic; + + /// mq连接id + String? mqClientId; + + /// mq状态 + String? mqStatus; + + /// 外部存储地址 + String? externalPath; + + /// 数据库地址 + String? sqlAddress; + + /// 日志地址 + String? logAddress; + + /// 人脸 appId + String? faceAppId; + + /// 人脸 sdkKey + String? faceSdkKey; + + /// 人脸 activeKey + String? faceActiveKey; + + /// 同步时间 + DateTime? syncTime; + + /// 菜谱分类同步时间 + DateTime? canteenClassifySyncTime; + + /// 菜谱商品同步时间 + DateTime? canteenProductSyncTime; + + /// 超市分类同步时间 + DateTime? supermarketClassifySyncTime; + + /// 超市商品同步时间 + DateTime? supermarketProductSyncTime; + + /// 离线订单同步时间 + DateTime? offlineOrderSyncTime; + + /// 长链接重连 + DateTime? mqSyncTime; + + SysSetting({ + required this.sysType, + this.androidId, + this.serviceId, + this.mqSocketAddress, + this.mqStatus, + this.sqlAddress, + this.logAddress, + this.hardGroupId, + }); + + factory SysSetting.fromJson(Map json) => + _$SysSettingFromJson(json); + + Map toJson() => _$SysSettingToJson(this); + + @override + List get props => [ + sysType, + androidId, + serviceId, + serviceName, + serviceMerchantId, + merchantDeptId, + hardGroupId, + mqSocketAddress, + mqSocketPort, + mqUsername, + mqPassword, + mainTopic, + mqClientId, + mqStatus, + externalPath, + sqlAddress, + logAddress, + faceAppId, + faceSdkKey, + faceActiveKey, + syncTime?.millisecondsSinceEpoch, + canteenClassifySyncTime?.millisecondsSinceEpoch, + canteenProductSyncTime?.millisecondsSinceEpoch, + supermarketClassifySyncTime?.millisecondsSinceEpoch, + supermarketProductSyncTime?.millisecondsSinceEpoch, + offlineOrderSyncTime?.millisecondsSinceEpoch, + mqSyncTime?.millisecondsSinceEpoch, + ]; +} + +/// 用户信息 +@JsonSerializable() +class UserSetting extends Equatable { + /// 账号 + String? username; + + /// 密码 + String? password; + + /// start 服务端信息 start + + /// 设备类型 + SysType? sysType; + + /// 用户编号 + int? userId; + + /// 访问令牌 + String? accessToken; + + /// 刷新令牌 + String? refreshToken; + + /// 过期时间 + int? expiresTime; + + /// end 服务端信息 end + + /// 商品时间段类型 + ProductHourType? hourType; + + UserSetting({ + this.username, + this.password, + }); + + factory UserSetting.fromJson(Map json) => + _$UserSettingFromJson(json); + + Map toJson() => _$UserSettingToJson(this); + + @override + List get props => [ + username, + password, + sysType, + userId, + accessToken, + refreshToken, + expiresTime, + hourType, + ]; +} + +@JsonSerializable() +class CanteenExtraPriceSetting extends Equatable { + /// 是否开启额外金额 + bool payAmountEnable; + + /// 每一次上限 + int payAmountMealLimit; + + /// 超过的倍数 + double payAmountMealTimes; + + CanteenExtraPriceSetting({ + required this.payAmountEnable, + required this.payAmountMealLimit, + required this.payAmountMealTimes, + }); + + @override + List get props => [ + payAmountEnable, + payAmountMealLimit, + payAmountMealTimes, + ]; + + factory CanteenExtraPriceSetting.fromJson(Map json) => + _$CanteenExtraPriceSettingFromJson(json); + + Map toJson() => _$CanteenExtraPriceSettingToJson(this); +} diff --git a/lib/common/models/database/database_models.g.dart b/lib/common/models/database/database_models.g.dart new file mode 100644 index 0000000..d21bf99 --- /dev/null +++ b/lib/common/models/database/database_models.g.dart @@ -0,0 +1,139 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SysSetting _$SysSettingFromJson(Map json) => SysSetting( + sysType: $enumDecode(_$SysTypeEnumMap, json['sysType']), + androidId: json['androidId'] as String?, + serviceId: json['serviceId'] as String?, + mqSocketAddress: json['mqSocketAddress'] as String?, + mqStatus: json['mqStatus'] as String?, + sqlAddress: json['sqlAddress'] as String?, + logAddress: json['logAddress'] as String?, + hardGroupId: json['hardGroupId'] as String?, + ) + ..serviceName = json['serviceName'] as String? + ..serviceMerchantId = json['serviceMerchantId'] as String? + ..merchantDeptId = json['merchantDeptId'] as String? + ..mqSocketPort = json['mqSocketPort'] as String? + ..mqUsername = json['mqUsername'] as String? + ..mqPassword = json['mqPassword'] as String? + ..mainTopic = json['mainTopic'] as String? + ..mqClientId = json['mqClientId'] as String? + ..externalPath = json['externalPath'] as String? + ..faceAppId = json['faceAppId'] as String? + ..faceSdkKey = json['faceSdkKey'] as String? + ..faceActiveKey = json['faceActiveKey'] as String? + ..syncTime = json['syncTime'] == null + ? null + : DateTime.parse(json['syncTime'] as String) + ..canteenClassifySyncTime = json['canteenClassifySyncTime'] == null + ? null + : DateTime.parse(json['canteenClassifySyncTime'] as String) + ..canteenProductSyncTime = json['canteenProductSyncTime'] == null + ? null + : DateTime.parse(json['canteenProductSyncTime'] as String) + ..supermarketClassifySyncTime = + json['supermarketClassifySyncTime'] == null + ? null + : DateTime.parse(json['supermarketClassifySyncTime'] as String) + ..supermarketProductSyncTime = json['supermarketProductSyncTime'] == null + ? null + : DateTime.parse(json['supermarketProductSyncTime'] as String) + ..offlineOrderSyncTime = json['offlineOrderSyncTime'] == null + ? null + : DateTime.parse(json['offlineOrderSyncTime'] as String) + ..mqSyncTime = json['mqSyncTime'] == null + ? null + : DateTime.parse(json['mqSyncTime'] as String); + +Map _$SysSettingToJson(SysSetting instance) => + { + 'sysType': _$SysTypeEnumMap[instance.sysType]!, + 'androidId': instance.androidId, + 'serviceId': instance.serviceId, + 'serviceName': instance.serviceName, + 'serviceMerchantId': instance.serviceMerchantId, + 'merchantDeptId': instance.merchantDeptId, + 'hardGroupId': instance.hardGroupId, + 'mqSocketAddress': instance.mqSocketAddress, + 'mqSocketPort': instance.mqSocketPort, + 'mqUsername': instance.mqUsername, + 'mqPassword': instance.mqPassword, + 'mainTopic': instance.mainTopic, + 'mqClientId': instance.mqClientId, + 'mqStatus': instance.mqStatus, + 'externalPath': instance.externalPath, + 'sqlAddress': instance.sqlAddress, + 'logAddress': instance.logAddress, + 'faceAppId': instance.faceAppId, + 'faceSdkKey': instance.faceSdkKey, + 'faceActiveKey': instance.faceActiveKey, + 'syncTime': instance.syncTime?.toIso8601String(), + 'canteenClassifySyncTime': + instance.canteenClassifySyncTime?.toIso8601String(), + 'canteenProductSyncTime': + instance.canteenProductSyncTime?.toIso8601String(), + 'supermarketClassifySyncTime': + instance.supermarketClassifySyncTime?.toIso8601String(), + 'supermarketProductSyncTime': + instance.supermarketProductSyncTime?.toIso8601String(), + 'offlineOrderSyncTime': instance.offlineOrderSyncTime?.toIso8601String(), + 'mqSyncTime': instance.mqSyncTime?.toIso8601String(), + }; + +const _$SysTypeEnumMap = { + SysType.supermarket: 'supermarket', + SysType.canteen: 'canteen', +}; + +UserSetting _$UserSettingFromJson(Map json) => UserSetting( + username: json['username'] as String?, + password: json['password'] as String?, + ) + ..sysType = $enumDecodeNullable(_$SysTypeEnumMap, json['sysType']) + ..userId = (json['userId'] as num?)?.toInt() + ..accessToken = json['accessToken'] as String? + ..refreshToken = json['refreshToken'] as String? + ..expiresTime = (json['expiresTime'] as num?)?.toInt() + ..hourType = + $enumDecodeNullable(_$ProductHourTypeEnumMap, json['hourType']); + +Map _$UserSettingToJson(UserSetting instance) => + { + 'username': instance.username, + 'password': instance.password, + 'sysType': _$SysTypeEnumMap[instance.sysType], + 'userId': instance.userId, + 'accessToken': instance.accessToken, + 'refreshToken': instance.refreshToken, + 'expiresTime': instance.expiresTime, + 'hourType': _$ProductHourTypeEnumMap[instance.hourType], + }; + +const _$ProductHourTypeEnumMap = { + ProductHourType.all: 'all', + ProductHourType.morning: 'morning', + ProductHourType.noon: 'noon', + ProductHourType.night: 'night', +}; + +CanteenExtraPriceSetting _$CanteenExtraPriceSettingFromJson( + Map json) => + CanteenExtraPriceSetting( + payAmountEnable: json['payAmountEnable'] as bool, + payAmountMealLimit: (json['payAmountMealLimit'] as num).toInt(), + payAmountMealTimes: (json['payAmountMealTimes'] as num).toDouble(), + ); + +Map _$CanteenExtraPriceSettingToJson( + CanteenExtraPriceSetting instance) => + { + 'payAmountEnable': instance.payAmountEnable, + 'payAmountMealLimit': instance.payAmountMealLimit, + 'payAmountMealTimes': instance.payAmountMealTimes, + }; diff --git a/lib/common/models/encryption/encryption_tool.dart b/lib/common/models/encryption/encryption_tool.dart new file mode 100644 index 0000000..29a2d2e --- /dev/null +++ b/lib/common/models/encryption/encryption_tool.dart @@ -0,0 +1,10 @@ +import 'package:common_utils/common_utils.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class EncryptionTool { + // 获取base64加密后的 oauth2 APP模式客户端信息 + static String getLoginAuthorization() { + return "Basic ${EncryptUtil.encodeBase64(Constants.oauth2AppClient)}"; + } +} diff --git a/lib/common/models/index.dart b/lib/common/models/index.dart new file mode 100644 index 0000000..bdf02e7 --- /dev/null +++ b/lib/common/models/index.dart @@ -0,0 +1,31 @@ +library; + +export './api/base_api_models.dart'; +export 'api/product_api_models.dart'; +export './api/hard_api_models.dart'; +export './api/user_api_models.dart'; +export './api/wallet_api_models.dart'; + +export './base/base_models.dart'; + +export './page/order_page_enum_models.dart'; +export './page/order_page_models.dart'; +export './page/panel_page_models.dart'; +export './page/setting_page_models.dart'; + +export './mq/mq_enum_models.dart'; +export './mq/mq_models.dart'; + +export './manage/usb_facility_enum_manage.dart'; +export './manage/usb_facility_manage.dart'; + +export './encryption/encryption_tool.dart'; + +export './database/database_enum_models.dart'; +export './database/database_models.dart'; + +export './plugs/face_discern_models.dart'; +export './plugs/serial_card_enum_models.dart'; +export './plugs/serial_card_models.dart'; +export './plugs/sub_screen_enum_models.dart'; +export './plugs/sub_screen_models.dart'; diff --git a/lib/common/models/manage/usb_facility_enum_manage.dart b/lib/common/models/manage/usb_facility_enum_manage.dart new file mode 100644 index 0000000..df1192b --- /dev/null +++ b/lib/common/models/manage/usb_facility_enum_manage.dart @@ -0,0 +1,16 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +enum UsbFacilityNames implements BaseEnum { + camera("1", "摄像头", "/dev/bus/usb/002/002"), + scanCode("1", "二维码读头", "/dev/bus/usb/001/003"), + print("1", "打印机", ""), + ; + + @override + final String code; + @override + final String title; + final String usbName; + + const UsbFacilityNames(this.code, this.title, this.usbName); +} diff --git a/lib/common/models/manage/usb_facility_manage.dart b/lib/common/models/manage/usb_facility_manage.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/common/models/manage/usb_facility_manage.dart @@ -0,0 +1 @@ + diff --git a/lib/common/models/mq/mq_enum_models.dart b/lib/common/models/mq/mq_enum_models.dart new file mode 100644 index 0000000..6a3be9e --- /dev/null +++ b/lib/common/models/mq/mq_enum_models.dart @@ -0,0 +1,19 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +enum MqtActionEnum implements BaseEnum { + config("10", "设置变化"), + canteenProduct("20", "食堂商品变化"), + canteenClassify("30", "食堂分类变化"), + supermarketProduct("40", "超市商品变化"), + supermarketClassify("50", "超市分类变化"), + print("60", "打印变化"), + face("70", "人脸变化"), + ; + + @override + final String code; + @override + final String title; + + const MqtActionEnum(this.code, this.title); +} diff --git a/lib/common/models/mq/mq_models.dart b/lib/common/models/mq/mq_models.dart new file mode 100644 index 0000000..6b98dce --- /dev/null +++ b/lib/common/models/mq/mq_models.dart @@ -0,0 +1,63 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'mq_models.g.dart'; + +@JsonSerializable() +class MqBaseMsg { + // 动作 + MqtActionEnum? action; + + MqBaseMsg({ + this.action, + }); + + factory MqBaseMsg.fromJson(Map json) => + _$MqBaseMsgFromJson(json); + + Map toJson() => _$MqBaseMsgToJson(this); +} + +@JsonSerializable() +class MqttInvoiceMsg { + // 动作 + MqtActionEnum? action; + + // 记录id + @JsonKey(fromJson: JsonTool.anyToString, toJson: JsonTool.stringToInt) + final String? historyId; + + // 用户id + @JsonKey(fromJson: JsonTool.anyToString, toJson: JsonTool.stringToInt) + final String? userId; + + // 订单id + @JsonKey(fromJson: JsonTool.anyToString, toJson: JsonTool.stringToInt) + final String? orderId; + + // 安卓订单id + @JsonKey(fromJson: JsonTool.anyToString, toJson: JsonTool.stringToInt) + final String? androidOrderCode; + + // 图片地址url + final String? pic; + + // 是否设置为全局人脸 + final bool? isGlobalFace; + + MqttInvoiceMsg({ + this.action, + this.historyId, + this.userId, + this.orderId, + this.androidOrderCode, + this.pic, + this.isGlobalFace, + }); + + factory MqttInvoiceMsg.fromJson(Map json) => + _$MqttInvoiceMsgFromJson(json); + + Map toJson() => _$MqttInvoiceMsgToJson(this); +} diff --git a/lib/common/models/mq/mq_models.g.dart b/lib/common/models/mq/mq_models.g.dart new file mode 100644 index 0000000..7782818 --- /dev/null +++ b/lib/common/models/mq/mq_models.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mq_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MqBaseMsg _$MqBaseMsgFromJson(Map json) => MqBaseMsg( + action: $enumDecodeNullable(_$MqtActionEnumEnumMap, json['action']), + ); + +Map _$MqBaseMsgToJson(MqBaseMsg instance) => { + 'action': _$MqtActionEnumEnumMap[instance.action], + }; + +const _$MqtActionEnumEnumMap = { + MqtActionEnum.config: 'config', + MqtActionEnum.canteenProduct: 'canteenProduct', + MqtActionEnum.canteenClassify: 'canteenClassify', + MqtActionEnum.supermarketProduct: 'supermarketProduct', + MqtActionEnum.supermarketClassify: 'supermarketClassify', + MqtActionEnum.print: 'print', + MqtActionEnum.face: 'face', +}; + +MqttInvoiceMsg _$MqttInvoiceMsgFromJson(Map json) => + MqttInvoiceMsg( + action: $enumDecodeNullable(_$MqtActionEnumEnumMap, json['action']), + historyId: JsonTool.anyToString(json['historyId']), + userId: JsonTool.anyToString(json['userId']), + orderId: JsonTool.anyToString(json['orderId']), + androidOrderCode: JsonTool.anyToString(json['androidOrderCode']), + pic: json['pic'] as String?, + isGlobalFace: json['isGlobalFace'] as bool?, + ); + +Map _$MqttInvoiceMsgToJson(MqttInvoiceMsg instance) => + { + 'action': _$MqtActionEnumEnumMap[instance.action], + 'historyId': JsonTool.stringToInt(instance.historyId), + 'userId': JsonTool.stringToInt(instance.userId), + 'orderId': JsonTool.stringToInt(instance.orderId), + 'androidOrderCode': JsonTool.stringToInt(instance.androidOrderCode), + 'pic': instance.pic, + 'isGlobalFace': instance.isGlobalFace, + }; diff --git a/lib/common/models/page/order_page_enum_models.dart b/lib/common/models/page/order_page_enum_models.dart new file mode 100644 index 0000000..cc9a84c --- /dev/null +++ b/lib/common/models/page/order_page_enum_models.dart @@ -0,0 +1,15 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +enum OrderPayDisplayType implements BaseEnum { + qrCode("1", "二维码支付"), + walletCard("2", "饭卡支付"), + face("3", "人脸支付"), + ; + + @override + final String code; + @override + final String title; + + const OrderPayDisplayType(this.code, this.title); +} diff --git a/lib/common/models/page/order_page_models.dart b/lib/common/models/page/order_page_models.dart new file mode 100644 index 0000000..9001274 --- /dev/null +++ b/lib/common/models/page/order_page_models.dart @@ -0,0 +1,11 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +class OrderHistoryTableItem { + OrderStatus status; + int total; + + OrderHistoryTableItem({ + required this.status, + this.total = 0, + }); +} diff --git a/lib/common/models/page/panel_page_models.dart b/lib/common/models/page/panel_page_models.dart new file mode 100644 index 0000000..9a4f993 --- /dev/null +++ b/lib/common/models/page/panel_page_models.dart @@ -0,0 +1,31 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +class PanelItem { + String title; + bool visible; + PanelType type; + PanelItem({ + required this.title, + required this.visible, + required this.type, + }); +} + +enum PanelType implements BaseEnum { + supermarketOrder("10", "超市购买"), + canteenOrder("20", "点餐"), + takeFood("30", "取餐"), + orderHistory("40", "交易查询"), + setting("50", "设置"), + cardActive("60", "开卡管理"), + printer("70", "打印测试"), + logout("80", "退出登录"), + ; + + @override + final String code; + @override + final String title; + + const PanelType(this.code, this.title); +} diff --git a/lib/common/models/page/setting_page_models.dart b/lib/common/models/page/setting_page_models.dart new file mode 100644 index 0000000..abc267d --- /dev/null +++ b/lib/common/models/page/setting_page_models.dart @@ -0,0 +1,32 @@ +import 'package:forui/forui.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +enum SettingMenuType implements BaseEnum { + baseInfo("10", "基本信息"), + syncSetting("20", "同步设置"), + paySetting("30", "支付设置"), + backup("40", "资源备份"), + faceSetting("50", "支付设置"), + productSetting("60", "资源备份"), + ; + + @override + final String code; + @override + final String title; + + const SettingMenuType(this.code, this.title); +} + +class SettingMenu { + SvgAsset prefixIcon; + String title; + SettingMenuType type; + + SettingMenu({ + required this.prefixIcon, + required this.title, + required this.type, + }); +} diff --git a/lib/common/models/plugs/face_discern_models.dart b/lib/common/models/plugs/face_discern_models.dart new file mode 100644 index 0000000..6030d4a --- /dev/null +++ b/lib/common/models/plugs/face_discern_models.dart @@ -0,0 +1,109 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'face_discern_models.g.dart'; + +@JsonSerializable() +class FaceStream { + // 相似度 + double similar; + + // 识别时间 + int cost; + + // 人脸id + int? faceId; + + // 人脸样本id + int? sampleId; + + // 样本id + String? userId; + + // 用户名 + String? userName; + + FaceStream({ + required this.similar, + required this.cost, + this.faceId, + this.sampleId, + this.userId, + this.userName, + }); + + factory FaceStream.fromJson(Map json) => + _$FaceStreamFromJson(json); + + Map toJson() => _$FaceStreamToJson(this); +} + +@JsonSerializable() +class FaceConfig { + String? appId; + + String? sdkKey; + + String? activeKey; + + // 是否激活 + bool isActive = false; + + FaceConfig({ + this.appId, + this.sdkKey, + this.activeKey, + this.isActive = false, + }); + + factory FaceConfig.fromJson(Map json) => + _$FaceConfigFromJson(json); + + Map toJson() => _$FaceConfigToJson(this); +} + +@JsonSerializable() +class RegisterAllStorageFaceRes { + // 注册成功数量 + int successCount; + + // 注册失败数量 + int failedCount; + + // 注册数量 + int registerCount; + + // 本地文件数量 + int fileCount; + + RegisterAllStorageFaceRes({ + required this.successCount, + required this.failedCount, + required this.registerCount, + required this.fileCount, + }); + + factory RegisterAllStorageFaceRes.fromJson(Map json) => + _$RegisterAllStorageFaceResFromJson(json); + + Map toJson() => _$RegisterAllStorageFaceResToJson(this); +} + +@JsonSerializable() +class FaceRes { + int searchId; + + List featureData; + + String faceTag; + + FaceRes({ + required this.searchId, + required this.featureData, + required this.faceTag, + }); + + factory FaceRes.fromJson(Map json) => + _$FaceResFromJson(json); + + Map toJson() => _$FaceResToJson(this); +} diff --git a/lib/common/models/plugs/face_discern_models.g.dart b/lib/common/models/plugs/face_discern_models.g.dart new file mode 100644 index 0000000..e5f5880 --- /dev/null +++ b/lib/common/models/plugs/face_discern_models.g.dart @@ -0,0 +1,73 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'face_discern_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FaceStream _$FaceStreamFromJson(Map json) => FaceStream( + similar: (json['similar'] as num).toDouble(), + cost: (json['cost'] as num).toInt(), + faceId: (json['faceId'] as num?)?.toInt(), + sampleId: (json['sampleId'] as num?)?.toInt(), + userId: json['userId'] as String?, + userName: json['userName'] as String?, + ); + +Map _$FaceStreamToJson(FaceStream instance) => + { + 'similar': instance.similar, + 'cost': instance.cost, + 'faceId': instance.faceId, + 'sampleId': instance.sampleId, + 'userId': instance.userId, + 'userName': instance.userName, + }; + +FaceConfig _$FaceConfigFromJson(Map json) => FaceConfig( + appId: json['appId'] as String?, + sdkKey: json['sdkKey'] as String?, + activeKey: json['activeKey'] as String?, + isActive: json['isActive'] as bool? ?? false, + ); + +Map _$FaceConfigToJson(FaceConfig instance) => + { + 'appId': instance.appId, + 'sdkKey': instance.sdkKey, + 'activeKey': instance.activeKey, + 'isActive': instance.isActive, + }; + +RegisterAllStorageFaceRes _$RegisterAllStorageFaceResFromJson( + Map json) => + RegisterAllStorageFaceRes( + successCount: (json['successCount'] as num).toInt(), + failedCount: (json['failedCount'] as num).toInt(), + registerCount: (json['registerCount'] as num).toInt(), + fileCount: (json['fileCount'] as num).toInt(), + ); + +Map _$RegisterAllStorageFaceResToJson( + RegisterAllStorageFaceRes instance) => + { + 'successCount': instance.successCount, + 'failedCount': instance.failedCount, + 'registerCount': instance.registerCount, + 'fileCount': instance.fileCount, + }; + +FaceRes _$FaceResFromJson(Map json) => FaceRes( + searchId: (json['searchId'] as num).toInt(), + featureData: (json['featureData'] as List) + .map((e) => (e as num).toInt()) + .toList(), + faceTag: json['faceTag'] as String, + ); + +Map _$FaceResToJson(FaceRes instance) => { + 'searchId': instance.searchId, + 'featureData': instance.featureData, + 'faceTag': instance.faceTag, + }; diff --git a/lib/common/models/plugs/serial_card_enum_models.dart b/lib/common/models/plugs/serial_card_enum_models.dart new file mode 100644 index 0000000..0c60a7c --- /dev/null +++ b/lib/common/models/plugs/serial_card_enum_models.dart @@ -0,0 +1,10 @@ +enum SerialCardCmdEnum { + write, + read; +} + +enum SerialCardResStatus { + success, + error, + warning, +} diff --git a/lib/common/models/plugs/serial_card_models.dart b/lib/common/models/plugs/serial_card_models.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/common/models/plugs/sub_screen_enum_models.dart b/lib/common/models/plugs/sub_screen_enum_models.dart new file mode 100644 index 0000000..f90ace9 --- /dev/null +++ b/lib/common/models/plugs/sub_screen_enum_models.dart @@ -0,0 +1,17 @@ +// 父屏 +import 'package:pj_supermarket_cloud/common/index.dart'; + +enum SubScreenAction implements BaseEnum { + updateCard("", "订单更新"), + jumpSubPage("", "跳转到订单选择页面"), + jumpSubBack("", "跳转到上一个页面"), + videoImg("", "视频图片"), + ; + + @override + final String code; + @override + final String title; + + const SubScreenAction(this.code, this.title); +} diff --git a/lib/common/models/plugs/sub_screen_models.dart b/lib/common/models/plugs/sub_screen_models.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/common/models/plugs/sub_screen_models.dart @@ -0,0 +1 @@ + diff --git a/lib/common/routers/index.dart b/lib/common/routers/index.dart new file mode 100644 index 0000000..f345140 --- /dev/null +++ b/lib/common/routers/index.dart @@ -0,0 +1,4 @@ +library; + +export 'names.dart'; +export 'pages.dart'; diff --git a/lib/common/routers/names.dart b/lib/common/routers/names.dart new file mode 100644 index 0000000..8635dec --- /dev/null +++ b/lib/common/routers/names.dart @@ -0,0 +1,34 @@ +class RouteNames { + /// 加载页面 + static const load = '/load'; + + /// 登录 + static const login = '/login'; + + /// 面板 + static const panel = '/panel'; + + /// 超市购买 + static const supermarketProduct = '/supermarket_product'; + + /// 点餐 + static const canteenProduct = '/canteen_product'; + + /// 取餐 + static const takeFood = '/take_food'; + + /// 交易查询 + static const orderHistory = '/order_history'; + + // 交易记录详情 + static const orderHistoryDetail = '/order_history_detail'; + + /// 设置 + static const setting = '/setting'; + + /// 饭卡 + static const cardActive = '/card_active'; + + /// 人脸 + static const faceDiscern = '/face_discern'; +} diff --git a/lib/common/routers/pages.dart b/lib/common/routers/pages.dart new file mode 100644 index 0000000..195699a --- /dev/null +++ b/lib/common/routers/pages.dart @@ -0,0 +1,83 @@ +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; +import 'package:pj_supermarket_cloud/pages/index.dart'; + +class RoutePages { + // 列表 + static List list = [ + GetPage( + name: RouteNames.load, + page: () => const LoadPage(), + ), + GetPage( + name: RouteNames.login, + page: () => const LoginPage(), + ), + GetPage( + name: RouteNames.panel, + page: () => const PanelPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => PanelController()); + }), + ), + GetPage( + name: RouteNames.supermarketProduct, + page: () => const SupermarketOrderPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => SelectPayTypeDialogController()); + Get.lazyPut(() => PanelController()); + Get.lazyPut(() => SupermarketOrderController()); + }), + ), + GetPage( + name: RouteNames.canteenProduct, + page: () => const CanteenProductPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => SelectPayTypeDialogController()); + Get.lazyPut(() => PanelController()); + Get.lazyPut(() => CanteenProductController()); + }), + ), + GetPage( + name: RouteNames.takeFood, + page: () => const TakeFoodPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => PanelController()); + Get.lazyPut(() => TakeFoodController()); + }), + ), + GetPage( + name: RouteNames.orderHistory, + page: () => const OrderHistoryPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => PanelController()); + Get.lazyPut(() => OrderHistoryController()); + }), + ), + GetPage( + name: RouteNames.orderHistoryDetail, + page: () => const OrderHistoryDetailPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => OrderHistoryController()); + Get.lazyPut(() => OrderHistoryDetailController()); + }), + ), + GetPage( + name: RouteNames.setting, + page: () => const SettingPage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => PanelController()); + Get.lazyPut(() => SettingController()); + }), + ), + GetPage( + name: RouteNames.cardActive, + page: () => const CardActivePage(), + binding: BindingsBuilder(() { + Get.lazyPut(() => SettingController()); + Get.lazyPut(() => CardActiveController()); + }), + ), + ]; +} diff --git a/lib/common/services/database/dao/classify_dao.dart b/lib/common/services/database/dao/classify_dao.dart new file mode 100644 index 0000000..2e9be23 --- /dev/null +++ b/lib/common/services/database/dao/classify_dao.dart @@ -0,0 +1,52 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:floor/floor.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +@dao +abstract class ClassifyDao { + @insert + Future save(ClassifyEntity entity); + + @insert + Future batchSave(List entitys); + + Future delByIds(List codes) async { + if (ObjectUtil.isEmptyList(codes)) return -1; + final app = Get.find(); + final database = app.dataBase.database; + final placeholders = List.generate(codes.length, (index) => '?').join(', '); + final sql = ''' + UPDATE classify SET delFlag = 1 WHERE serviceUniqueCode IN ($placeholders) + '''; + return await database.rawUpdate(sql, [...codes]); + } + + @update + Future edit(ClassifyEntity entity); + + @update + Future batchEdit(List entitys); + + @Query('SELECT DISTINCT COUNT(0) FROM classify WHERE delFlag = 0') + Future getCount(); + + @Query(""" + SELECT * FROM classify WHERE delFlag = 0 AND serviceUniqueCode = :code + """) + Future getByCode(String code); + + @Query(''' + SELECT classify.* FROM classify + WHERE + delFlag = 0 AND + (:sysType = '-1' OR sysType = :sysType) AND + (:categoryType = '-1' OR INSTR(',' || categoryType || ',', ',' || :categoryType || ',') > 0) + ORDER BY classify.sort + ''') + Future> getAll( + String sysType, + String categoryType, + ); +} diff --git a/lib/common/services/database/dao/face_dao.dart b/lib/common/services/database/dao/face_dao.dart new file mode 100644 index 0000000..a0223fe --- /dev/null +++ b/lib/common/services/database/dao/face_dao.dart @@ -0,0 +1,77 @@ +import 'package:floor/floor.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +// face +@dao +abstract class FaceDao { + @insert + Future save(FaceEntity entity); + + @insert + Future batchSave(List entitys); + + @delete + Future del(FaceEntity entity); + + @Query(""" DELETE FROM face """) + Future delAll(); + + @Query(''' + DELETE FROM face WHERE id = :id + ''') + Future delById(int id); + + @Query(''' + DELETE FROM face WHERE userId = :userId + ''') + Future delByUserId(String userId); + + @delete + Future batchDel(List entitys); + + @update + Future edit(FaceEntity entity); + + @update + Future batchEdit(List entitys); + + @Query(""" + SELECT DISTINCT COUNT(0) FROM face + WHERE + (:userName = '-1' OR userName LIKE '%' || :userName || '%') + """) + Future findCount(String userName); + + @Query(''' + SELECT * FROM face WHERE id = :id + ''') + Future getById(int id); + + @Query(''' + SELECT * FROM face WHERE sampleId = :sampleId + ''') + Future findBySampleId(String sampleId); + + @Query(''' + SELECT * FROM face WHERE userId = :userId + ''') + Future?> findByUserId(String userId); + + @Query(''' + SELECT * FROM face + WHERE + (:userName = '-1' OR userName LIKE '%' || :userName || '%') + LIMIT :pageSize OFFSET :offset + ''') + Future> findAll( + String userName, + int pageSize, + int offset, + ); + + @Query(''' + SELECT DISTINCT a.deptId FROM face a WHERE a.deptId IS NOT NULL + ''') + Future> findAllUserIds(); +} diff --git a/lib/common/services/database/dao/order_dao.dart b/lib/common/services/database/dao/order_dao.dart new file mode 100644 index 0000000..98962f5 --- /dev/null +++ b/lib/common/services/database/dao/order_dao.dart @@ -0,0 +1,85 @@ +import 'package:floor/floor.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +@dao +abstract class OrderDao { + @insert + Future save(OrderEntity entity); + + @insert + Future batchSave(List entitys); + + @delete + Future del(OrderEntity entity); + + @delete + Future batchDel(List entitys); + + @update + Future edit(OrderEntity entity); + + @update + Future batchEdit(List entitys); + + @Query(''' + UPDATE order_record SET status = 'PAY' WHERE uniqueCode IN :codes + ''') + Future editOfflineStatus(String codes); + + @Query(''' + SELECT * FROM order_record + WHERE + (:status == "-1" OR status = :status) AND + (:sysType = '-1' OR sysType = :sysType) AND + (:hourType = '-1' OR hourType = :hourType) AND + createTime >= :startCreateOrderTime AND createTime <= :endCreateOrderTime + ORDER BY createTime DESC + LIMIT :pageSize OFFSET :offset + ''') + Future> findAll( + String status, + DateTime startCreateOrderTime, + DateTime endCreateOrderTime, + int pageSize, + int offset, + String sysType, + String hourType, + ); + + @Query(''' + SELECT COUNT(0) FROM order_record + WHERE + (:status == "-1" OR status = :status) AND + (:sysType = '-1' OR sysType = :sysType) AND + (:hourType = '-1' OR hourType = :hourType) AND + createTime >= :startCreateOrderTime AND createTime <= :endCreateOrderTime + ''') + Future count( + String status, + DateTime startCreateOrderTime, + DateTime endCreateOrderTime, + String sysType, + String hourType, + ); + + @Query(''' + SELECT * FROM order_record + WHERE + status = 'OFFLINE_PAY' AND + (:sysType = '-1' OR sysType = :sysType) AND + createTime >= :startCreateOrderTime AND createTime <= :endCreateOrderTime + ''') + Future> findOffline( + String sysType, + DateTime startCreateOrderTime, + DateTime endCreateOrderTime, + ); + + @Query(''' + SELECT * FROM order_record + WHERE + id = :id + ''') + Future findById(int id); +} diff --git a/lib/common/services/database/dao/print_history_dao.dart b/lib/common/services/database/dao/print_history_dao.dart new file mode 100644 index 0000000..3cbfe6b --- /dev/null +++ b/lib/common/services/database/dao/print_history_dao.dart @@ -0,0 +1,34 @@ +import 'package:floor/floor.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +@dao +abstract class PrintHistoryDao { + @insert + Future save(PrintHistoryEntity entity); + + @insert + Future batchSave(List entitys); + + @delete + Future del(PrintHistoryEntity entity); + + @Query('DELETE FROM print_history') + Future delAll(); + + @delete + Future batchDel(List entitys); + + @update + Future edit(PrintHistoryEntity entity); + + @update + Future batchEdit(List entitys); + + @Query(''' + SELECT * FROM print_history + WHERE + (:status = '-1' OR status = :status) + ''') + Future> getAll(String status); +} diff --git a/lib/common/services/database/dao/product_dao.dart b/lib/common/services/database/dao/product_dao.dart new file mode 100644 index 0000000..4c7da19 --- /dev/null +++ b/lib/common/services/database/dao/product_dao.dart @@ -0,0 +1,112 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:floor/floor.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +@dao +abstract class ProductDao { + @insert + Future save(ProductEntity entity); + + @insert + Future batchSave(List entitys); + + Future delByIds(List codes) async { + if (ObjectUtil.isEmptyList(codes)) return -1; + final app = Get.find(); + final database = app.dataBase.database; + final placeholders = List.generate(codes.length, (index) => '?').join(', '); + final sql = ''' + UPDATE product SET delFlag = 1 WHERE serviceUniqueCode IN ($placeholders) + '''; + return await database.rawUpdate(sql, [...codes]); + } + + @Query(''' + DELETE FROM product + WHERE + (:sysType = '-1' OR sysType = :sysType) + ''') + Future delAllByType( + String sysType, + ); + + @delete + Future batchDel(List entitys); + + @Query(""" + UPDATE product SET isDefault = :isDefault + WHERE + delFlag = 0 AND serviceUniqueCode = :uniqueCode AND hourType = :hourType + """) + Future edit(bool isDefault, String uniqueCode, String hourType); + + @update + Future batchEdit(List entitys); + + @Query(""" + SELECT DISTINCT COUNT(0) FROM product + WHERE + delFlag = 0 + AND (:sysType = '-1' OR sysType = :sysType) + AND (:hourType = '-1' OR INSTR(',' || hourType || ',', ',' || :hourType || ',') > 0) + """) + Future getCount( + String sysType, + String hourType, + ); + + @Query(''' + SELECT * FROM product WHERE delFlag = 0 AND serviceUniqueCode = :code + ''') + Future getByCode(String code); + + @Query(''' + SELECT product.* + FROM product + LEFT JOIN classify ON product.classifyServiceUniqueCode = classify.serviceUniqueCode + WHERE + product.delFlag = 0 AND + (:classifyServiceUniqueCode = '-1' OR product.classifyServiceUniqueCode = :classifyServiceUniqueCode) AND + (:hourType = '-1' OR INSTR(',' || hourType || ',', ',' || :hourType || ',') > 0) AND + (:sysType = '-1' OR product.sysType = :sysType) + ORDER BY classify.sort, product.price + ''') + Future> getAll( + String classifyServiceUniqueCode, + String hourType, + String sysType, + ); + + // GROUP BY product.serviceUniqueCode + @Query(''' + SELECT product.* + FROM product + LEFT JOIN classify ON product.classifyServiceUniqueCode = classify.serviceUniqueCode + WHERE + product.delFlag = 0 AND + (:classifyServiceUniqueCode = '-1' OR product.classifyServiceUniqueCode = :classifyServiceUniqueCode) AND + (:hourType = '-1' OR INSTR(',' || product.hourType || ',', ',' || :hourType || ',') > 0) AND + (:sysType = '-1' OR product.sysType = :sysType) + ORDER BY classify.sort, classify.id, product.price + LIMIT :pageSize OFFSET :offset + ''') + Future> getAllPage( + String classifyServiceUniqueCode, + String hourType, + String sysType, + int pageSize, + int offset, + ); + + @Query(''' + SELECT * FROM product + WHERE + product.delFlag = 0 + AND isDefault = 1 + AND (:sysType = '-1' OR sysType = :sysType) + AND (:hourType = '-1' OR hourType = :hourType) + ''') + Future> getDefault(String sysType, String hourType); +} diff --git a/lib/common/services/database/dao/setting_dao.dart b/lib/common/services/database/dao/setting_dao.dart new file mode 100644 index 0000000..fc6f5b4 --- /dev/null +++ b/lib/common/services/database/dao/setting_dao.dart @@ -0,0 +1,20 @@ +import 'package:floor/floor.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +@dao +abstract class SettingDao { + @insert + Future save(SettingEntity entity); + + @delete + Future del(SettingEntity entity); + + @update + Future edit(SettingEntity entity); + + @Query(''' + SELECT * FROM setting LIMIT 1 + ''') + Future get(); +} diff --git a/lib/common/services/database/dao/shop_card_dao.dart b/lib/common/services/database/dao/shop_card_dao.dart new file mode 100644 index 0000000..0cf797c --- /dev/null +++ b/lib/common/services/database/dao/shop_card_dao.dart @@ -0,0 +1,31 @@ +import 'package:floor/floor.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +@dao +abstract class ShopCardDao { + @insert + Future save(ShopCardEntity entity); + + @insert + Future batchSave(List entitys); + + @delete + Future del(ShopCardEntity entity); + + @delete + Future batchDel(List entitys); + + @update + Future edit(ShopCardEntity entity); + + @update + Future batchEdit(List entitys); + + @Query(''' + SELECT * FROM shop_card + WHERE + orderId = :orderId + ''') + Future> findByOrderId(int orderId); +} diff --git a/lib/common/services/database/dao/user_dao.dart b/lib/common/services/database/dao/user_dao.dart new file mode 100644 index 0000000..b8baefc --- /dev/null +++ b/lib/common/services/database/dao/user_dao.dart @@ -0,0 +1,39 @@ +import 'package:floor/floor.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +// user_record 表 +@dao +abstract class UserDao { + @insert + Future save(UserEntity entity); + + @insert + Future batchSave(List entitys); + + @delete + Future del(UserEntity entity); + + @delete + Future batchDel(List entitys); + + @update + Future edit(UserEntity entity); + + @update + Future batchEdit(List entitys); + + @Query(''' + SELECT * FROM user_record + WHERE + userId = :userId + ''') + Future findByUserId(String userId); + + @Query(''' + SELECT * FROM user_record + WHERE + walletCardNumber = :walletCardNumber + ''') + Future findByCardNumber(String walletCardNumber); +} diff --git a/lib/common/services/database/database.dart b/lib/common/services/database/database.dart new file mode 100644 index 0000000..5aed5ba --- /dev/null +++ b/lib/common/services/database/database.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:floor/floor.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +import 'package:sqflite/sqflite.dart' as sqflite; + +part 'database.g.dart'; + +const entities = [ + OrderEntity, + ProductEntity, + ShopCardEntity, + ClassifyEntity, + PrintHistoryEntity, + UserEntity, + FaceEntity, + SettingEntity, +]; + +const typeConverters = [ + DateTimeConverter, + DateTimeEmptyConverter, + OrderStatusConverter, + OrderTypeConverter, + OrderPayTypeConverter, + SysTypeConverter, + PrintStatusConverter, + ListIntConverter, + BoolConverter, +]; + +@Database(version: 1, entities: entities) +@TypeConverters(typeConverters) +abstract class AppDatabase extends FloorDatabase { + OrderDao get getOrderDao; + ProductDao get getProductDao; + ShopCardDao get getShopCardDao; + ClassifyDao get getClassifyDao; + PrintHistoryDao get getPrintHistoryDao; + UserDao get getUserDao; + FaceDao get getFaceDao; + SettingDao get getSettingDao; + + static Future getDataBase() async { + final dbPath = await AppFileTool.getDatabasePath(); + return await $FloorAppDatabase.databaseBuilder("cash_register.db").build(); + } + + // static Future getDataBase() async { + // final dbPath = await AppFileTool.getDatabasePath(); + // return await $FloorAppDatabase + // .databaseBuilder("$dbPath/cash_register.db") + // .addMigrations([]).build(); + // } +} diff --git a/lib/common/services/database/database.g.dart b/lib/common/services/database/database.g.dart new file mode 100644 index 0000000..3066ee5 --- /dev/null +++ b/lib/common/services/database/database.g.dart @@ -0,0 +1,1403 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ************************************************************************** +// FloorGenerator +// ************************************************************************** + +abstract class $AppDatabaseBuilderContract { + /// Adds migrations to the builder. + $AppDatabaseBuilderContract addMigrations(List migrations); + + /// Adds a database [Callback] to the builder. + $AppDatabaseBuilderContract addCallback(Callback callback); + + /// Creates the database and initializes it. + Future build(); +} + +// ignore: avoid_classes_with_only_static_members +class $FloorAppDatabase { + /// Creates a database builder for a persistent database. + /// Once a database is built, you should keep a reference to it and re-use it. + static $AppDatabaseBuilderContract databaseBuilder(String name) => + _$AppDatabaseBuilder(name); + + /// Creates a database builder for an in memory database. + /// Information stored in an in memory database disappears when the process is killed. + /// Once a database is built, you should keep a reference to it and re-use it. + static $AppDatabaseBuilderContract inMemoryDatabaseBuilder() => + _$AppDatabaseBuilder(null); +} + +class _$AppDatabaseBuilder implements $AppDatabaseBuilderContract { + _$AppDatabaseBuilder(this.name); + + final String? name; + + final List _migrations = []; + + Callback? _callback; + + @override + $AppDatabaseBuilderContract addMigrations(List migrations) { + _migrations.addAll(migrations); + return this; + } + + @override + $AppDatabaseBuilderContract addCallback(Callback callback) { + _callback = callback; + return this; + } + + @override + Future build() async { + final path = name != null + ? await sqfliteDatabaseFactory.getDatabasePath(name!) + : ':memory:'; + final database = _$AppDatabase(); + database.database = await database.open( + path, + _migrations, + _callback, + ); + return database; + } +} + +class _$AppDatabase extends AppDatabase { + _$AppDatabase([StreamController? listener]) { + changeListener = listener ?? StreamController.broadcast(); + } + + OrderDao? _getOrderDaoInstance; + + ProductDao? _getProductDaoInstance; + + ShopCardDao? _getShopCardDaoInstance; + + ClassifyDao? _getClassifyDaoInstance; + + PrintHistoryDao? _getPrintHistoryDaoInstance; + + UserDao? _getUserDaoInstance; + + FaceDao? _getFaceDaoInstance; + + SettingDao? _getSettingDaoInstance; + + Future open( + String path, + List migrations, [ + Callback? callback, + ]) async { + final databaseOptions = sqflite.OpenDatabaseOptions( + version: 1, + onConfigure: (database) async { + await database.execute('PRAGMA foreign_keys = ON'); + await callback?.onConfigure?.call(database); + }, + onOpen: (database) async { + await callback?.onOpen?.call(database); + }, + onUpgrade: (database, startVersion, endVersion) async { + await MigrationAdapter.runMigrations( + database, startVersion, endVersion, migrations); + + await callback?.onUpgrade?.call(database, startVersion, endVersion); + }, + onCreate: (database, version) async { + await database.execute( + 'CREATE TABLE IF NOT EXISTS `order_record` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `uniqueCode` TEXT NOT NULL, `serviceUniqueCode` TEXT NOT NULL, `sysType` TEXT NOT NULL, `userCode` TEXT NOT NULL, `username` TEXT, `hourType` TEXT, `price` INTEGER NOT NULL, `extraPrice` INTEGER NOT NULL, `payPrice` INTEGER NOT NULL, `walletId` TEXT, `orderSourceTypeId` TEXT, `type` TEXT NOT NULL, `status` TEXT NOT NULL, `payType` TEXT NOT NULL, `productDesc` TEXT, `count` INTEGER NOT NULL, `weight` REAL NOT NULL, `payTime` TEXT, `createTime` TEXT NOT NULL)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `product` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `serviceUniqueCode` TEXT NOT NULL, `sysType` TEXT NOT NULL, `classifyServiceUniqueCode` TEXT NOT NULL, `hourType` INTEGER, `name` TEXT NOT NULL, `price` INTEGER NOT NULL, `image` TEXT, `isDefault` INTEGER NOT NULL, `status` INTEGER NOT NULL, `sort` INTEGER NOT NULL, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL, `delFlag` INTEGER NOT NULL)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `shop_card` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `orderId` INTEGER NOT NULL, `productServiceUniqueCode` TEXT NOT NULL, `productName` TEXT NOT NULL, `productPic` TEXT NOT NULL, `productPrice` INTEGER NOT NULL, `productNumber` INTEGER NOT NULL, `productWidget` REAL NOT NULL, `totalPrice` INTEGER NOT NULL)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `classify` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `serviceUniqueCode` TEXT NOT NULL, `sysType` TEXT NOT NULL, `name` TEXT NOT NULL, `categoryType` TEXT NOT NULL, `sort` INTEGER NOT NULL, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL, `delFlag` INTEGER NOT NULL)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `print_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `orderId` TEXT NOT NULL, `serviceOrderId` TEXT NOT NULL, `servicePrintId` TEXT NOT NULL, `invoiceUrl` TEXT NOT NULL, `status` TEXT NOT NULL, `printTime` TEXT, `syncTime` TEXT, `createTime` TEXT NOT NULL)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `user_record` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `userId` TEXT NOT NULL, `nickname` TEXT NOT NULL, `mobile` TEXT, `avatar` TEXT, `walletCardNumber` TEXT, `walletId` TEXT, `walletTypeId` TEXT, `walletBalance` INTEGER, `createTime` TEXT, `updateTime` TEXT)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `face` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `userId` TEXT NOT NULL, `sampleId` TEXT NOT NULL, `userName` TEXT NOT NULL, `deptId` TEXT, `deptName` TEXT, `imagePath` TEXT NOT NULL, `path` TEXT, `faceModel` TEXT, `createTime` TEXT, `updateTime` TEXT)'); + await database.execute( + 'CREATE TABLE IF NOT EXISTS `setting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `userJson` TEXT NOT NULL, `sysJson` TEXT NOT NULL, `canteenExtraPriceConfigJson` TEXT)'); + + await callback?.onCreate?.call(database, version); + }, + ); + return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions); + } + + @override + OrderDao get getOrderDao { + return _getOrderDaoInstance ??= _$OrderDao(database, changeListener); + } + + @override + ProductDao get getProductDao { + return _getProductDaoInstance ??= _$ProductDao(database, changeListener); + } + + @override + ShopCardDao get getShopCardDao { + return _getShopCardDaoInstance ??= _$ShopCardDao(database, changeListener); + } + + @override + ClassifyDao get getClassifyDao { + return _getClassifyDaoInstance ??= _$ClassifyDao(database, changeListener); + } + + @override + PrintHistoryDao get getPrintHistoryDao { + return _getPrintHistoryDaoInstance ??= + _$PrintHistoryDao(database, changeListener); + } + + @override + UserDao get getUserDao { + return _getUserDaoInstance ??= _$UserDao(database, changeListener); + } + + @override + FaceDao get getFaceDao { + return _getFaceDaoInstance ??= _$FaceDao(database, changeListener); + } + + @override + SettingDao get getSettingDao { + return _getSettingDaoInstance ??= _$SettingDao(database, changeListener); + } +} + +class _$OrderDao extends OrderDao { + _$OrderDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _orderEntityInsertionAdapter = InsertionAdapter( + database, + 'order_record', + (OrderEntity item) => { + 'id': item.id, + 'uniqueCode': item.uniqueCode, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'userCode': item.userCode, + 'username': item.username, + 'hourType': item.hourType, + 'price': item.price, + 'extraPrice': item.extraPrice, + 'payPrice': item.payPrice, + 'walletId': item.walletId, + 'orderSourceTypeId': item.orderSourceTypeId, + 'type': _orderTypeConverter.encode(item.type), + 'status': _orderStatusConverter.encode(item.status), + 'payType': _orderPayTypeConverter.encode(item.payType), + 'productDesc': item.productDesc, + 'count': item.count, + 'weight': item.weight, + 'payTime': _dateTimeEmptyConverter.encode(item.payTime), + 'createTime': _dateTimeConverter.encode(item.createTime) + }), + _orderEntityUpdateAdapter = UpdateAdapter( + database, + 'order_record', + ['id'], + (OrderEntity item) => { + 'id': item.id, + 'uniqueCode': item.uniqueCode, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'userCode': item.userCode, + 'username': item.username, + 'hourType': item.hourType, + 'price': item.price, + 'extraPrice': item.extraPrice, + 'payPrice': item.payPrice, + 'walletId': item.walletId, + 'orderSourceTypeId': item.orderSourceTypeId, + 'type': _orderTypeConverter.encode(item.type), + 'status': _orderStatusConverter.encode(item.status), + 'payType': _orderPayTypeConverter.encode(item.payType), + 'productDesc': item.productDesc, + 'count': item.count, + 'weight': item.weight, + 'payTime': _dateTimeEmptyConverter.encode(item.payTime), + 'createTime': _dateTimeConverter.encode(item.createTime) + }), + _orderEntityDeletionAdapter = DeletionAdapter( + database, + 'order_record', + ['id'], + (OrderEntity item) => { + 'id': item.id, + 'uniqueCode': item.uniqueCode, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'userCode': item.userCode, + 'username': item.username, + 'hourType': item.hourType, + 'price': item.price, + 'extraPrice': item.extraPrice, + 'payPrice': item.payPrice, + 'walletId': item.walletId, + 'orderSourceTypeId': item.orderSourceTypeId, + 'type': _orderTypeConverter.encode(item.type), + 'status': _orderStatusConverter.encode(item.status), + 'payType': _orderPayTypeConverter.encode(item.payType), + 'productDesc': item.productDesc, + 'count': item.count, + 'weight': item.weight, + 'payTime': _dateTimeEmptyConverter.encode(item.payTime), + 'createTime': _dateTimeConverter.encode(item.createTime) + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _orderEntityInsertionAdapter; + + final UpdateAdapter _orderEntityUpdateAdapter; + + final DeletionAdapter _orderEntityDeletionAdapter; + + @override + Future editOfflineStatus(String codes) async { + await _queryAdapter.queryNoReturn( + 'UPDATE order_record SET status = \'PAY\' WHERE uniqueCode IN ?1', + arguments: [codes]); + } + + @override + Future> findAll( + String status, + DateTime startCreateOrderTime, + DateTime endCreateOrderTime, + int pageSize, + int offset, + String sysType, + String hourType, + ) async { + return _queryAdapter.queryList( + 'SELECT * FROM order_record WHERE (?1 == \"-1\" OR status = ?1) AND (?6 = \'-1\' OR sysType = ?6) AND (?7 = \'-1\' OR hourType = ?7) AND createTime >= ?2 AND createTime <= ?3 ORDER BY createTime DESC LIMIT ?4 OFFSET ?5', + mapper: (Map row) => OrderEntity( + id: row['id'] as int?, + uniqueCode: row['uniqueCode'] as String, + serviceUniqueCode: row['serviceUniqueCode'] as String, + sysType: _sysTypeConverter.decode(row['sysType'] as String), + userCode: row['userCode'] as String, + username: row['username'] as String?, + hourType: row['hourType'] as String?, + price: row['price'] as int, + extraPrice: row['extraPrice'] as int, + payPrice: row['payPrice'] as int, + walletId: row['walletId'] as String?, + orderSourceTypeId: row['orderSourceTypeId'] as String?, + type: _orderTypeConverter.decode(row['type'] as String), + status: _orderStatusConverter.decode(row['status'] as String), + payType: _orderPayTypeConverter.decode(row['payType'] as String), + productDesc: row['productDesc'] as String?, + count: row['count'] as int, + weight: row['weight'] as double, + payTime: _dateTimeEmptyConverter.decode(row['payTime'] as String), + createTime: _dateTimeConverter.decode(row['createTime'] as String)), + arguments: [ + status, + _dateTimeConverter.encode(startCreateOrderTime), + _dateTimeConverter.encode(endCreateOrderTime), + pageSize, + offset, + sysType, + hourType + ]); + } + + @override + Future count( + String status, + DateTime startCreateOrderTime, + DateTime endCreateOrderTime, + String sysType, + String hourType, + ) async { + return _queryAdapter.query( + 'SELECT COUNT(0) FROM order_record WHERE (?1 == \"-1\" OR status = ?1) AND (?4 = \'-1\' OR sysType = ?4) AND (?5 = \'-1\' OR hourType = ?5) AND createTime >= ?2 AND createTime <= ?3', + mapper: (Map row) => row.values.first as int, + arguments: [ + status, + _dateTimeConverter.encode(startCreateOrderTime), + _dateTimeConverter.encode(endCreateOrderTime), + sysType, + hourType + ]); + } + + @override + Future> findOffline( + String sysType, + DateTime startCreateOrderTime, + DateTime endCreateOrderTime, + ) async { + return _queryAdapter.queryList( + 'SELECT * FROM order_record WHERE status = \'OFFLINE_PAY\' AND (?1 = \'-1\' OR sysType = ?1) AND createTime >= ?2 AND createTime <= ?3', + mapper: (Map row) => OrderEntity( + id: row['id'] as int?, + uniqueCode: row['uniqueCode'] as String, + serviceUniqueCode: row['serviceUniqueCode'] as String, + sysType: _sysTypeConverter.decode(row['sysType'] as String), + userCode: row['userCode'] as String, + username: row['username'] as String?, + hourType: row['hourType'] as String?, + price: row['price'] as int, + extraPrice: row['extraPrice'] as int, + payPrice: row['payPrice'] as int, + walletId: row['walletId'] as String?, + orderSourceTypeId: row['orderSourceTypeId'] as String?, + type: _orderTypeConverter.decode(row['type'] as String), + status: _orderStatusConverter.decode(row['status'] as String), + payType: _orderPayTypeConverter.decode(row['payType'] as String), + productDesc: row['productDesc'] as String?, + count: row['count'] as int, + weight: row['weight'] as double, + payTime: _dateTimeEmptyConverter.decode(row['payTime'] as String), + createTime: _dateTimeConverter.decode(row['createTime'] as String)), + arguments: [ + sysType, + _dateTimeConverter.encode(startCreateOrderTime), + _dateTimeConverter.encode(endCreateOrderTime) + ]); + } + + @override + Future findById(int id) async { + return _queryAdapter.query( + 'SELECT * FROM order_record WHERE id = ?1', + mapper: (Map row) => OrderEntity( + id: row['id'] as int?, + uniqueCode: row['uniqueCode'] as String, + serviceUniqueCode: row['serviceUniqueCode'] as String, + sysType: _sysTypeConverter.decode(row['sysType'] as String), + userCode: row['userCode'] as String, + username: row['username'] as String?, + hourType: row['hourType'] as String?, + price: row['price'] as int, + extraPrice: row['extraPrice'] as int, + payPrice: row['payPrice'] as int, + walletId: row['walletId'] as String?, + orderSourceTypeId: row['orderSourceTypeId'] as String?, + type: _orderTypeConverter.decode(row['type'] as String), + status: _orderStatusConverter.decode(row['status'] as String), + payType: _orderPayTypeConverter.decode(row['payType'] as String), + productDesc: row['productDesc'] as String?, + count: row['count'] as int, + weight: row['weight'] as double, + payTime: _dateTimeEmptyConverter.decode(row['payTime'] as String), + createTime: _dateTimeConverter.decode(row['createTime'] as String)), + arguments: [id]); + } + + @override + Future save(OrderEntity entity) { + return _orderEntityInsertionAdapter.insertAndReturnId( + entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _orderEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future edit(OrderEntity entity) async { + await _orderEntityUpdateAdapter.update(entity, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _orderEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } + + @override + Future del(OrderEntity entity) async { + await _orderEntityDeletionAdapter.delete(entity); + } + + @override + Future batchDel(List entitys) async { + await _orderEntityDeletionAdapter.deleteList(entitys); + } +} + +class _$ProductDao extends ProductDao { + _$ProductDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _productEntityInsertionAdapter = InsertionAdapter( + database, + 'product', + (ProductEntity item) => { + 'id': item.id, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'classifyServiceUniqueCode': item.classifyServiceUniqueCode, + 'hourType': item.hourType?.index, + 'name': item.name, + 'price': item.price, + 'image': item.image, + 'isDefault': _boolConverter.encode(item.isDefault), + 'status': item.status, + 'sort': item.sort, + 'createTime': _dateTimeConverter.encode(item.createTime), + 'updateTime': _dateTimeConverter.encode(item.updateTime), + 'delFlag': _boolConverter.encode(item.delFlag) + }), + _productEntityUpdateAdapter = UpdateAdapter( + database, + 'product', + ['id'], + (ProductEntity item) => { + 'id': item.id, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'classifyServiceUniqueCode': item.classifyServiceUniqueCode, + 'hourType': item.hourType?.index, + 'name': item.name, + 'price': item.price, + 'image': item.image, + 'isDefault': _boolConverter.encode(item.isDefault), + 'status': item.status, + 'sort': item.sort, + 'createTime': _dateTimeConverter.encode(item.createTime), + 'updateTime': _dateTimeConverter.encode(item.updateTime), + 'delFlag': _boolConverter.encode(item.delFlag) + }), + _productEntityDeletionAdapter = DeletionAdapter( + database, + 'product', + ['id'], + (ProductEntity item) => { + 'id': item.id, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'classifyServiceUniqueCode': item.classifyServiceUniqueCode, + 'hourType': item.hourType?.index, + 'name': item.name, + 'price': item.price, + 'image': item.image, + 'isDefault': _boolConverter.encode(item.isDefault), + 'status': item.status, + 'sort': item.sort, + 'createTime': _dateTimeConverter.encode(item.createTime), + 'updateTime': _dateTimeConverter.encode(item.updateTime), + 'delFlag': _boolConverter.encode(item.delFlag) + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _productEntityInsertionAdapter; + + final UpdateAdapter _productEntityUpdateAdapter; + + final DeletionAdapter _productEntityDeletionAdapter; + + @override + Future delAllByType(String sysType) async { + await _queryAdapter.queryNoReturn( + 'DELETE FROM product WHERE (?1 = \'-1\' OR sysType = ?1)', + arguments: [sysType]); + } + + @override + Future edit( + bool isDefault, + String uniqueCode, + String hourType, + ) async { + await _queryAdapter.queryNoReturn( + 'UPDATE product SET isDefault = ?1 WHERE delFlag = 0 AND serviceUniqueCode = ?2 AND hourType = ?3', + arguments: [_boolConverter.encode(isDefault), uniqueCode, hourType]); + } + + @override + Future getCount( + String sysType, + String hourType, + ) async { + return _queryAdapter.query( + 'SELECT DISTINCT COUNT(0) FROM product WHERE delFlag = 0 AND (?1 = \'-1\' OR sysType = ?1) AND (?2 = \'-1\' OR INSTR(\',\' || hourType || \',\', \',\' || ?2 || \',\') > 0)', + mapper: (Map row) => row.values.first as int, + arguments: [sysType, hourType]); + } + + @override + Future getByCode(String code) async { + return _queryAdapter.query( + 'SELECT * FROM product WHERE delFlag = 0 AND serviceUniqueCode = ?1', + mapper: (Map row) => ProductEntity( + id: row['id'] as int?, + serviceUniqueCode: row['serviceUniqueCode'] as String, + sysType: _sysTypeConverter.decode(row['sysType'] as String), + classifyServiceUniqueCode: + row['classifyServiceUniqueCode'] as String, + hourType: row['hourType'] == null + ? null + : ProductHourType.values[row['hourType'] as int], + name: row['name'] as String, + price: row['price'] as int, + image: row['image'] as String?, + isDefault: _boolConverter.decode(row['isDefault'] as int), + status: row['status'] as int, + sort: row['sort'] as int, + createTime: _dateTimeConverter.decode(row['createTime'] as String), + updateTime: _dateTimeConverter.decode(row['updateTime'] as String), + delFlag: _boolConverter.decode(row['delFlag'] as int)), + arguments: [code]); + } + + @override + Future> getAll( + String classifyServiceUniqueCode, + String hourType, + String sysType, + ) async { + return _queryAdapter.queryList( + 'SELECT product.* FROM product LEFT JOIN classify ON product.classifyServiceUniqueCode = classify.serviceUniqueCode WHERE product.delFlag = 0 AND (?1 = \'-1\' OR product.classifyServiceUniqueCode = ?1) AND (?2 = \'-1\' OR INSTR(\',\' || hourType || \',\', \',\' || ?2 || \',\') > 0) AND (?3 = \'-1\' OR product.sysType = ?3) ORDER BY classify.sort, product.price', + mapper: (Map row) => ProductEntity(id: row['id'] as int?, serviceUniqueCode: row['serviceUniqueCode'] as String, sysType: _sysTypeConverter.decode(row['sysType'] as String), classifyServiceUniqueCode: row['classifyServiceUniqueCode'] as String, hourType: row['hourType'] == null ? null : ProductHourType.values[row['hourType'] as int], name: row['name'] as String, price: row['price'] as int, image: row['image'] as String?, isDefault: _boolConverter.decode(row['isDefault'] as int), status: row['status'] as int, sort: row['sort'] as int, createTime: _dateTimeConverter.decode(row['createTime'] as String), updateTime: _dateTimeConverter.decode(row['updateTime'] as String), delFlag: _boolConverter.decode(row['delFlag'] as int)), + arguments: [classifyServiceUniqueCode, hourType, sysType]); + } + + @override + Future> getAllPage( + String classifyServiceUniqueCode, + String hourType, + String sysType, + int pageSize, + int offset, + ) async { + return _queryAdapter.queryList( + 'SELECT product.* FROM product LEFT JOIN classify ON product.classifyServiceUniqueCode = classify.serviceUniqueCode WHERE product.delFlag = 0 AND (?1 = \'-1\' OR product.classifyServiceUniqueCode = ?1) AND (?2 = \'-1\' OR INSTR(\',\' || product.hourType || \',\', \',\' || ?2 || \',\') > 0) AND (?3 = \'-1\' OR product.sysType = ?3) ORDER BY classify.sort, classify.id, product.price LIMIT ?4 OFFSET ?5', + mapper: (Map row) => ProductEntity(id: row['id'] as int?, serviceUniqueCode: row['serviceUniqueCode'] as String, sysType: _sysTypeConverter.decode(row['sysType'] as String), classifyServiceUniqueCode: row['classifyServiceUniqueCode'] as String, hourType: row['hourType'] == null ? null : ProductHourType.values[row['hourType'] as int], name: row['name'] as String, price: row['price'] as int, image: row['image'] as String?, isDefault: _boolConverter.decode(row['isDefault'] as int), status: row['status'] as int, sort: row['sort'] as int, createTime: _dateTimeConverter.decode(row['createTime'] as String), updateTime: _dateTimeConverter.decode(row['updateTime'] as String), delFlag: _boolConverter.decode(row['delFlag'] as int)), + arguments: [ + classifyServiceUniqueCode, + hourType, + sysType, + pageSize, + offset + ]); + } + + @override + Future> getDefault( + String sysType, + String hourType, + ) async { + return _queryAdapter.queryList( + 'SELECT * FROM product WHERE product.delFlag = 0 AND isDefault = 1 AND (?1 = \'-1\' OR sysType = ?1) AND (?2 = \'-1\' OR hourType = ?2)', + mapper: (Map row) => ProductEntity(id: row['id'] as int?, serviceUniqueCode: row['serviceUniqueCode'] as String, sysType: _sysTypeConverter.decode(row['sysType'] as String), classifyServiceUniqueCode: row['classifyServiceUniqueCode'] as String, hourType: row['hourType'] == null ? null : ProductHourType.values[row['hourType'] as int], name: row['name'] as String, price: row['price'] as int, image: row['image'] as String?, isDefault: _boolConverter.decode(row['isDefault'] as int), status: row['status'] as int, sort: row['sort'] as int, createTime: _dateTimeConverter.decode(row['createTime'] as String), updateTime: _dateTimeConverter.decode(row['updateTime'] as String), delFlag: _boolConverter.decode(row['delFlag'] as int)), + arguments: [sysType, hourType]); + } + + @override + Future save(ProductEntity entity) async { + await _productEntityInsertionAdapter.insert( + entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _productEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _productEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } + + @override + Future batchDel(List entitys) async { + await _productEntityDeletionAdapter.deleteList(entitys); + } +} + +class _$ShopCardDao extends ShopCardDao { + _$ShopCardDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _shopCardEntityInsertionAdapter = InsertionAdapter( + database, + 'shop_card', + (ShopCardEntity item) => { + 'id': item.id, + 'orderId': item.orderId, + 'productServiceUniqueCode': item.productServiceUniqueCode, + 'productName': item.productName, + 'productPic': item.productPic, + 'productPrice': item.productPrice, + 'productNumber': item.productNumber, + 'productWidget': item.productWidget, + 'totalPrice': item.totalPrice + }), + _shopCardEntityUpdateAdapter = UpdateAdapter( + database, + 'shop_card', + ['id'], + (ShopCardEntity item) => { + 'id': item.id, + 'orderId': item.orderId, + 'productServiceUniqueCode': item.productServiceUniqueCode, + 'productName': item.productName, + 'productPic': item.productPic, + 'productPrice': item.productPrice, + 'productNumber': item.productNumber, + 'productWidget': item.productWidget, + 'totalPrice': item.totalPrice + }), + _shopCardEntityDeletionAdapter = DeletionAdapter( + database, + 'shop_card', + ['id'], + (ShopCardEntity item) => { + 'id': item.id, + 'orderId': item.orderId, + 'productServiceUniqueCode': item.productServiceUniqueCode, + 'productName': item.productName, + 'productPic': item.productPic, + 'productPrice': item.productPrice, + 'productNumber': item.productNumber, + 'productWidget': item.productWidget, + 'totalPrice': item.totalPrice + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _shopCardEntityInsertionAdapter; + + final UpdateAdapter _shopCardEntityUpdateAdapter; + + final DeletionAdapter _shopCardEntityDeletionAdapter; + + @override + Future> findByOrderId(int orderId) async { + return _queryAdapter.queryList( + 'SELECT * FROM shop_card WHERE orderId = ?1', + mapper: (Map row) => ShopCardEntity( + id: row['id'] as int?, + orderId: row['orderId'] as int, + productServiceUniqueCode: row['productServiceUniqueCode'] as String, + productName: row['productName'] as String, + productPic: row['productPic'] as String, + productPrice: row['productPrice'] as int, + productNumber: row['productNumber'] as int, + productWidget: row['productWidget'] as double, + totalPrice: row['totalPrice'] as int), + arguments: [orderId]); + } + + @override + Future save(ShopCardEntity entity) async { + await _shopCardEntityInsertionAdapter.insert( + entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _shopCardEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future edit(ShopCardEntity entity) async { + await _shopCardEntityUpdateAdapter.update(entity, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _shopCardEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } + + @override + Future del(ShopCardEntity entity) async { + await _shopCardEntityDeletionAdapter.delete(entity); + } + + @override + Future batchDel(List entitys) async { + await _shopCardEntityDeletionAdapter.deleteList(entitys); + } +} + +class _$ClassifyDao extends ClassifyDao { + _$ClassifyDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _classifyEntityInsertionAdapter = InsertionAdapter( + database, + 'classify', + (ClassifyEntity item) => { + 'id': item.id, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'name': item.name, + 'categoryType': item.categoryType, + 'sort': item.sort, + 'createTime': _dateTimeConverter.encode(item.createTime), + 'updateTime': _dateTimeConverter.encode(item.updateTime), + 'delFlag': _boolConverter.encode(item.delFlag) + }), + _classifyEntityUpdateAdapter = UpdateAdapter( + database, + 'classify', + ['id'], + (ClassifyEntity item) => { + 'id': item.id, + 'serviceUniqueCode': item.serviceUniqueCode, + 'sysType': _sysTypeConverter.encode(item.sysType), + 'name': item.name, + 'categoryType': item.categoryType, + 'sort': item.sort, + 'createTime': _dateTimeConverter.encode(item.createTime), + 'updateTime': _dateTimeConverter.encode(item.updateTime), + 'delFlag': _boolConverter.encode(item.delFlag) + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _classifyEntityInsertionAdapter; + + final UpdateAdapter _classifyEntityUpdateAdapter; + + @override + Future getCount() async { + return _queryAdapter.query( + 'SELECT DISTINCT COUNT(0) FROM classify WHERE delFlag = 0', + mapper: (Map row) => row.values.first as int); + } + + @override + Future getByCode(String code) async { + return _queryAdapter.query( + 'SELECT * FROM classify WHERE delFlag = 0 AND serviceUniqueCode = ?1', + mapper: (Map row) => ClassifyEntity( + id: row['id'] as int?, + serviceUniqueCode: row['serviceUniqueCode'] as String, + sysType: _sysTypeConverter.decode(row['sysType'] as String), + name: row['name'] as String, + categoryType: row['categoryType'] as String, + sort: row['sort'] as int, + createTime: _dateTimeConverter.decode(row['createTime'] as String), + updateTime: _dateTimeConverter.decode(row['updateTime'] as String), + delFlag: _boolConverter.decode(row['delFlag'] as int)), + arguments: [code]); + } + + @override + Future> getAll( + String sysType, + String categoryType, + ) async { + return _queryAdapter.queryList( + 'SELECT classify.* FROM classify WHERE delFlag = 0 AND (?1 = \'-1\' OR sysType = ?1) AND (?2 = \'-1\' OR INSTR(\',\' || categoryType || \',\', \',\' || ?2 || \',\') > 0) ORDER BY classify.sort', + mapper: (Map row) => ClassifyEntity(id: row['id'] as int?, serviceUniqueCode: row['serviceUniqueCode'] as String, sysType: _sysTypeConverter.decode(row['sysType'] as String), name: row['name'] as String, categoryType: row['categoryType'] as String, sort: row['sort'] as int, createTime: _dateTimeConverter.decode(row['createTime'] as String), updateTime: _dateTimeConverter.decode(row['updateTime'] as String), delFlag: _boolConverter.decode(row['delFlag'] as int)), + arguments: [sysType, categoryType]); + } + + @override + Future save(ClassifyEntity entity) async { + await _classifyEntityInsertionAdapter.insert( + entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _classifyEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future edit(ClassifyEntity entity) async { + await _classifyEntityUpdateAdapter.update(entity, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _classifyEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } +} + +class _$PrintHistoryDao extends PrintHistoryDao { + _$PrintHistoryDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _printHistoryEntityInsertionAdapter = InsertionAdapter( + database, + 'print_history', + (PrintHistoryEntity item) => { + 'id': item.id, + 'orderId': item.orderId, + 'serviceOrderId': item.serviceOrderId, + 'servicePrintId': item.servicePrintId, + 'invoiceUrl': item.invoiceUrl, + 'status': _printStatusConverter.encode(item.status), + 'printTime': item.printTime, + 'syncTime': _dateTimeEmptyConverter.encode(item.syncTime), + 'createTime': _dateTimeConverter.encode(item.createTime) + }), + _printHistoryEntityUpdateAdapter = UpdateAdapter( + database, + 'print_history', + ['id'], + (PrintHistoryEntity item) => { + 'id': item.id, + 'orderId': item.orderId, + 'serviceOrderId': item.serviceOrderId, + 'servicePrintId': item.servicePrintId, + 'invoiceUrl': item.invoiceUrl, + 'status': _printStatusConverter.encode(item.status), + 'printTime': item.printTime, + 'syncTime': _dateTimeEmptyConverter.encode(item.syncTime), + 'createTime': _dateTimeConverter.encode(item.createTime) + }), + _printHistoryEntityDeletionAdapter = DeletionAdapter( + database, + 'print_history', + ['id'], + (PrintHistoryEntity item) => { + 'id': item.id, + 'orderId': item.orderId, + 'serviceOrderId': item.serviceOrderId, + 'servicePrintId': item.servicePrintId, + 'invoiceUrl': item.invoiceUrl, + 'status': _printStatusConverter.encode(item.status), + 'printTime': item.printTime, + 'syncTime': _dateTimeEmptyConverter.encode(item.syncTime), + 'createTime': _dateTimeConverter.encode(item.createTime) + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter + _printHistoryEntityInsertionAdapter; + + final UpdateAdapter _printHistoryEntityUpdateAdapter; + + final DeletionAdapter _printHistoryEntityDeletionAdapter; + + @override + Future delAll() async { + await _queryAdapter.queryNoReturn('DELETE FROM print_history'); + } + + @override + Future> getAll(String status) async { + return _queryAdapter.queryList( + 'SELECT * FROM print_history WHERE (?1 = \'-1\' OR status = ?1)', + mapper: (Map row) => PrintHistoryEntity(id: row['id'] as int?, orderId: row['orderId'] as String, serviceOrderId: row['serviceOrderId'] as String, servicePrintId: row['servicePrintId'] as String, invoiceUrl: row['invoiceUrl'] as String, status: _printStatusConverter.decode(row['status'] as String), printTime: row['printTime'] as String?, syncTime: _dateTimeEmptyConverter.decode(row['syncTime'] as String), createTime: _dateTimeConverter.decode(row['createTime'] as String)), + arguments: [status]); + } + + @override + Future save(PrintHistoryEntity entity) async { + await _printHistoryEntityInsertionAdapter.insert( + entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _printHistoryEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future edit(PrintHistoryEntity entity) async { + await _printHistoryEntityUpdateAdapter.update( + entity, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _printHistoryEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } + + @override + Future del(PrintHistoryEntity entity) async { + await _printHistoryEntityDeletionAdapter.delete(entity); + } + + @override + Future batchDel(List entitys) async { + await _printHistoryEntityDeletionAdapter.deleteList(entitys); + } +} + +class _$UserDao extends UserDao { + _$UserDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _userEntityInsertionAdapter = InsertionAdapter( + database, + 'user_record', + (UserEntity item) => { + 'id': item.id, + 'userId': item.userId, + 'nickname': item.nickname, + 'mobile': item.mobile, + 'avatar': item.avatar, + 'walletCardNumber': item.walletCardNumber, + 'walletId': item.walletId, + 'walletTypeId': item.walletTypeId, + 'walletBalance': item.walletBalance, + 'createTime': _dateTimeEmptyConverter.encode(item.createTime), + 'updateTime': _dateTimeEmptyConverter.encode(item.updateTime) + }), + _userEntityUpdateAdapter = UpdateAdapter( + database, + 'user_record', + ['id'], + (UserEntity item) => { + 'id': item.id, + 'userId': item.userId, + 'nickname': item.nickname, + 'mobile': item.mobile, + 'avatar': item.avatar, + 'walletCardNumber': item.walletCardNumber, + 'walletId': item.walletId, + 'walletTypeId': item.walletTypeId, + 'walletBalance': item.walletBalance, + 'createTime': _dateTimeEmptyConverter.encode(item.createTime), + 'updateTime': _dateTimeEmptyConverter.encode(item.updateTime) + }), + _userEntityDeletionAdapter = DeletionAdapter( + database, + 'user_record', + ['id'], + (UserEntity item) => { + 'id': item.id, + 'userId': item.userId, + 'nickname': item.nickname, + 'mobile': item.mobile, + 'avatar': item.avatar, + 'walletCardNumber': item.walletCardNumber, + 'walletId': item.walletId, + 'walletTypeId': item.walletTypeId, + 'walletBalance': item.walletBalance, + 'createTime': _dateTimeEmptyConverter.encode(item.createTime), + 'updateTime': _dateTimeEmptyConverter.encode(item.updateTime) + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _userEntityInsertionAdapter; + + final UpdateAdapter _userEntityUpdateAdapter; + + final DeletionAdapter _userEntityDeletionAdapter; + + @override + Future findByUserId(String userId) async { + return _queryAdapter.query( + 'SELECT * FROM user_record WHERE userId = ?1', + mapper: (Map row) => UserEntity( + id: row['id'] as int?, + userId: row['userId'] as String, + nickname: row['nickname'] as String, + mobile: row['mobile'] as String?, + avatar: row['avatar'] as String?, + walletCardNumber: row['walletCardNumber'] as String?, + walletId: row['walletId'] as String?, + walletTypeId: row['walletTypeId'] as String?, + walletBalance: row['walletBalance'] as int?, + createTime: + _dateTimeEmptyConverter.decode(row['createTime'] as String), + updateTime: + _dateTimeEmptyConverter.decode(row['updateTime'] as String)), + arguments: [userId]); + } + + @override + Future findByCardNumber(String walletCardNumber) async { + return _queryAdapter.query( + 'SELECT * FROM user_record WHERE walletCardNumber = ?1', + mapper: (Map row) => UserEntity( + id: row['id'] as int?, + userId: row['userId'] as String, + nickname: row['nickname'] as String, + mobile: row['mobile'] as String?, + avatar: row['avatar'] as String?, + walletCardNumber: row['walletCardNumber'] as String?, + walletId: row['walletId'] as String?, + walletTypeId: row['walletTypeId'] as String?, + walletBalance: row['walletBalance'] as int?, + createTime: + _dateTimeEmptyConverter.decode(row['createTime'] as String), + updateTime: + _dateTimeEmptyConverter.decode(row['updateTime'] as String)), + arguments: [walletCardNumber]); + } + + @override + Future save(UserEntity entity) async { + await _userEntityInsertionAdapter.insert(entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _userEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future edit(UserEntity entity) async { + await _userEntityUpdateAdapter.update(entity, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _userEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } + + @override + Future del(UserEntity entity) async { + await _userEntityDeletionAdapter.delete(entity); + } + + @override + Future batchDel(List entitys) async { + await _userEntityDeletionAdapter.deleteList(entitys); + } +} + +class _$FaceDao extends FaceDao { + _$FaceDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _faceEntityInsertionAdapter = InsertionAdapter( + database, + 'face', + (FaceEntity item) => { + 'id': item.id, + 'userId': item.userId, + 'sampleId': item.sampleId, + 'userName': item.userName, + 'deptId': item.deptId, + 'deptName': item.deptName, + 'imagePath': item.imagePath, + 'path': item.path, + 'faceModel': _listIntConverter.encode(item.faceModel), + 'createTime': _dateTimeEmptyConverter.encode(item.createTime), + 'updateTime': _dateTimeEmptyConverter.encode(item.updateTime) + }), + _faceEntityUpdateAdapter = UpdateAdapter( + database, + 'face', + ['id'], + (FaceEntity item) => { + 'id': item.id, + 'userId': item.userId, + 'sampleId': item.sampleId, + 'userName': item.userName, + 'deptId': item.deptId, + 'deptName': item.deptName, + 'imagePath': item.imagePath, + 'path': item.path, + 'faceModel': _listIntConverter.encode(item.faceModel), + 'createTime': _dateTimeEmptyConverter.encode(item.createTime), + 'updateTime': _dateTimeEmptyConverter.encode(item.updateTime) + }), + _faceEntityDeletionAdapter = DeletionAdapter( + database, + 'face', + ['id'], + (FaceEntity item) => { + 'id': item.id, + 'userId': item.userId, + 'sampleId': item.sampleId, + 'userName': item.userName, + 'deptId': item.deptId, + 'deptName': item.deptName, + 'imagePath': item.imagePath, + 'path': item.path, + 'faceModel': _listIntConverter.encode(item.faceModel), + 'createTime': _dateTimeEmptyConverter.encode(item.createTime), + 'updateTime': _dateTimeEmptyConverter.encode(item.updateTime) + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _faceEntityInsertionAdapter; + + final UpdateAdapter _faceEntityUpdateAdapter; + + final DeletionAdapter _faceEntityDeletionAdapter; + + @override + Future delAll() async { + await _queryAdapter.queryNoReturn('DELETE FROM face'); + } + + @override + Future delById(int id) async { + await _queryAdapter + .queryNoReturn('DELETE FROM face WHERE id = ?1', arguments: [id]); + } + + @override + Future delByUserId(String userId) async { + await _queryAdapter.queryNoReturn('DELETE FROM face WHERE userId = ?1', + arguments: [userId]); + } + + @override + Future findCount(String userName) async { + return _queryAdapter.query( + 'SELECT DISTINCT COUNT(0) FROM face WHERE (?1 = \'-1\' OR userName LIKE \'%\' || ?1 || \'%\')', + mapper: (Map row) => row.values.first as int, + arguments: [userName]); + } + + @override + Future getById(int id) async { + return _queryAdapter.query('SELECT * FROM face WHERE id = ?1', + mapper: (Map row) => FaceEntity( + id: row['id'] as int?, + userId: row['userId'] as String, + sampleId: row['sampleId'] as String, + userName: row['userName'] as String, + deptId: row['deptId'] as String?, + deptName: row['deptName'] as String?, + imagePath: row['imagePath'] as String, + path: row['path'] as String?, + faceModel: _listIntConverter.decode(row['faceModel'] as String), + createTime: + _dateTimeEmptyConverter.decode(row['createTime'] as String), + updateTime: + _dateTimeEmptyConverter.decode(row['updateTime'] as String)), + arguments: [id]); + } + + @override + Future findBySampleId(String sampleId) async { + return _queryAdapter.query('SELECT * FROM face WHERE sampleId = ?1', + mapper: (Map row) => FaceEntity( + id: row['id'] as int?, + userId: row['userId'] as String, + sampleId: row['sampleId'] as String, + userName: row['userName'] as String, + deptId: row['deptId'] as String?, + deptName: row['deptName'] as String?, + imagePath: row['imagePath'] as String, + path: row['path'] as String?, + faceModel: _listIntConverter.decode(row['faceModel'] as String), + createTime: + _dateTimeEmptyConverter.decode(row['createTime'] as String), + updateTime: + _dateTimeEmptyConverter.decode(row['updateTime'] as String)), + arguments: [sampleId]); + } + + @override + Future?> findByUserId(String userId) async { + return _queryAdapter.queryList('SELECT * FROM face WHERE userId = ?1', + mapper: (Map row) => FaceEntity( + id: row['id'] as int?, + userId: row['userId'] as String, + sampleId: row['sampleId'] as String, + userName: row['userName'] as String, + deptId: row['deptId'] as String?, + deptName: row['deptName'] as String?, + imagePath: row['imagePath'] as String, + path: row['path'] as String?, + faceModel: _listIntConverter.decode(row['faceModel'] as String), + createTime: + _dateTimeEmptyConverter.decode(row['createTime'] as String), + updateTime: + _dateTimeEmptyConverter.decode(row['updateTime'] as String)), + arguments: [userId]); + } + + @override + Future> findAll( + String userName, + int pageSize, + int offset, + ) async { + return _queryAdapter.queryList( + 'SELECT * FROM face WHERE (?1 = \'-1\' OR userName LIKE \'%\' || ?1 || \'%\') LIMIT ?2 OFFSET ?3', + mapper: (Map row) => FaceEntity(id: row['id'] as int?, userId: row['userId'] as String, sampleId: row['sampleId'] as String, userName: row['userName'] as String, deptId: row['deptId'] as String?, deptName: row['deptName'] as String?, imagePath: row['imagePath'] as String, path: row['path'] as String?, faceModel: _listIntConverter.decode(row['faceModel'] as String), createTime: _dateTimeEmptyConverter.decode(row['createTime'] as String), updateTime: _dateTimeEmptyConverter.decode(row['updateTime'] as String)), + arguments: [userName, pageSize, offset]); + } + + @override + Future> findAllUserIds() async { + return _queryAdapter.queryList( + 'SELECT DISTINCT a.deptId FROM face a WHERE a.deptId IS NOT NULL', + mapper: (Map row) => row.values.first as String); + } + + @override + Future save(FaceEntity entity) { + return _faceEntityInsertionAdapter.insertAndReturnId( + entity, OnConflictStrategy.abort); + } + + @override + Future batchSave(List entitys) async { + await _faceEntityInsertionAdapter.insertList( + entitys, OnConflictStrategy.abort); + } + + @override + Future edit(FaceEntity entity) async { + await _faceEntityUpdateAdapter.update(entity, OnConflictStrategy.abort); + } + + @override + Future batchEdit(List entitys) async { + await _faceEntityUpdateAdapter.updateList( + entitys, OnConflictStrategy.abort); + } + + @override + Future del(FaceEntity entity) async { + await _faceEntityDeletionAdapter.delete(entity); + } + + @override + Future batchDel(List entitys) async { + await _faceEntityDeletionAdapter.deleteList(entitys); + } +} + +class _$SettingDao extends SettingDao { + _$SettingDao( + this.database, + this.changeListener, + ) : _queryAdapter = QueryAdapter(database), + _settingEntityInsertionAdapter = InsertionAdapter( + database, + 'setting', + (SettingEntity item) => { + 'id': item.id, + 'userJson': item.userJson, + 'sysJson': item.sysJson, + 'canteenExtraPriceConfigJson': + item.canteenExtraPriceConfigJson + }), + _settingEntityUpdateAdapter = UpdateAdapter( + database, + 'setting', + ['id'], + (SettingEntity item) => { + 'id': item.id, + 'userJson': item.userJson, + 'sysJson': item.sysJson, + 'canteenExtraPriceConfigJson': + item.canteenExtraPriceConfigJson + }), + _settingEntityDeletionAdapter = DeletionAdapter( + database, + 'setting', + ['id'], + (SettingEntity item) => { + 'id': item.id, + 'userJson': item.userJson, + 'sysJson': item.sysJson, + 'canteenExtraPriceConfigJson': + item.canteenExtraPriceConfigJson + }); + + final sqflite.DatabaseExecutor database; + + final StreamController changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _settingEntityInsertionAdapter; + + final UpdateAdapter _settingEntityUpdateAdapter; + + final DeletionAdapter _settingEntityDeletionAdapter; + + @override + Future get() async { + return _queryAdapter.query('SELECT * FROM setting LIMIT 1', + mapper: (Map row) => SettingEntity( + id: row['id'] as int?, + userJson: row['userJson'] as String, + sysJson: row['sysJson'] as String, + canteenExtraPriceConfigJson: + row['canteenExtraPriceConfigJson'] as String?)); + } + + @override + Future save(SettingEntity entity) { + return _settingEntityInsertionAdapter.insertAndReturnId( + entity, OnConflictStrategy.abort); + } + + @override + Future edit(SettingEntity entity) async { + await _settingEntityUpdateAdapter.update(entity, OnConflictStrategy.abort); + } + + @override + Future del(SettingEntity entity) async { + await _settingEntityDeletionAdapter.delete(entity); + } +} + +// ignore_for_file: unused_element +final _dateTimeConverter = DateTimeConverter(); +final _dateTimeEmptyConverter = DateTimeEmptyConverter(); +final _orderStatusConverter = OrderStatusConverter(); +final _orderTypeConverter = OrderTypeConverter(); +final _orderPayTypeConverter = OrderPayTypeConverter(); +final _sysTypeConverter = SysTypeConverter(); +final _printStatusConverter = PrintStatusConverter(); +final _listIntConverter = ListIntConverter(); +final _boolConverter = BoolConverter(); diff --git a/lib/common/services/database/entity/classify_entity.dart b/lib/common/services/database/entity/classify_entity.dart new file mode 100644 index 0000000..8e2e3f5 --- /dev/null +++ b/lib/common/services/database/entity/classify_entity.dart @@ -0,0 +1,65 @@ +import 'package:equatable/equatable.dart'; +import 'package:floor/floor.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/models/index.dart'; + +part 'classify_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "classify") +class ClassifyEntity extends Equatable { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 服务端唯一标识 + String serviceUniqueCode; + + /// 平台类型 + SysType sysType; + + /// 分类名 + String name; + + /// 用餐类型,早餐、中餐、晚餐 + String categoryType; + + /// 排序 倒序排列 + int sort; + + /// 创建时间 + DateTime createTime; + + /// 同步时间 + DateTime updateTime; + + /// 是否删除 + bool delFlag; + + ClassifyEntity({ + this.id, + required this.serviceUniqueCode, + required this.sysType, + required this.name, + required this.categoryType, + required this.sort, + required this.createTime, + required this.updateTime, + required this.delFlag, + }); + + factory ClassifyEntity.fromJson(Map json) => + _$ClassifyEntityFromJson(json); + + Map toJson() => _$ClassifyEntityToJson(this); + + @override + List get props => [ + serviceUniqueCode, + sysType, + name, + categoryType, + sort, + ]; +} diff --git a/lib/common/services/database/entity/classify_entity.g.dart b/lib/common/services/database/entity/classify_entity.g.dart new file mode 100644 index 0000000..1aaa16b --- /dev/null +++ b/lib/common/services/database/entity/classify_entity.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'classify_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ClassifyEntity _$ClassifyEntityFromJson(Map json) => + ClassifyEntity( + id: (json['id'] as num?)?.toInt(), + serviceUniqueCode: json['serviceUniqueCode'] as String, + sysType: $enumDecode(_$SysTypeEnumMap, json['sysType']), + name: json['name'] as String, + categoryType: json['categoryType'] as String, + sort: (json['sort'] as num).toInt(), + createTime: DateTime.parse(json['createTime'] as String), + updateTime: DateTime.parse(json['updateTime'] as String), + delFlag: json['delFlag'] as bool, + ); + +Map _$ClassifyEntityToJson(ClassifyEntity instance) => + { + 'id': instance.id, + 'serviceUniqueCode': instance.serviceUniqueCode, + 'sysType': _$SysTypeEnumMap[instance.sysType]!, + 'name': instance.name, + 'categoryType': instance.categoryType, + 'sort': instance.sort, + 'createTime': instance.createTime.toIso8601String(), + 'updateTime': instance.updateTime.toIso8601String(), + 'delFlag': instance.delFlag, + }; + +const _$SysTypeEnumMap = { + SysType.supermarket: 'supermarket', + SysType.canteen: 'canteen', +}; diff --git a/lib/common/services/database/entity/face_entity.dart b/lib/common/services/database/entity/face_entity.dart new file mode 100644 index 0000000..0cdfaa0 --- /dev/null +++ b/lib/common/services/database/entity/face_entity.dart @@ -0,0 +1,65 @@ +import 'package:floor/floor.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'face_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "face") +class FaceEntity extends Equatable { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 用户id + String userId; + + /// 样本id + String sampleId; + + /// 用户名 + String userName; + + /// 部门id + String? deptId; + + /// 部门名 + String? deptName; + + /// 图片路径 + String imagePath; + + /// 本地位置 + String? path; + + /// 人脸模型数据 + List? faceModel; + + /// 创建时间 + DateTime? createTime; + + /// 同步时间 + DateTime? updateTime; + + FaceEntity({ + this.id, + required this.userId, + required this.sampleId, + required this.userName, + this.deptId, + this.deptName, + required this.imagePath, + this.path, + this.faceModel, + this.createTime, + this.updateTime, + }); + + factory FaceEntity.fromJson(Map json) => + _$FaceEntityFromJson(json); + + Map toJson() => _$FaceEntityToJson(this); + + @override + List get props => [userId, sampleId, userName, imagePath]; +} diff --git a/lib/common/services/database/entity/face_entity.g.dart b/lib/common/services/database/entity/face_entity.g.dart new file mode 100644 index 0000000..b9816fa --- /dev/null +++ b/lib/common/services/database/entity/face_entity.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'face_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FaceEntity _$FaceEntityFromJson(Map json) => FaceEntity( + id: (json['id'] as num?)?.toInt(), + userId: json['userId'] as String, + sampleId: json['sampleId'] as String, + userName: json['userName'] as String, + deptId: json['deptId'] as String?, + deptName: json['deptName'] as String?, + imagePath: json['imagePath'] as String, + path: json['path'] as String?, + faceModel: json['faceModel'] as List?, + createTime: json['createTime'] == null + ? null + : DateTime.parse(json['createTime'] as String), + updateTime: json['updateTime'] == null + ? null + : DateTime.parse(json['updateTime'] as String), + ); + +Map _$FaceEntityToJson(FaceEntity instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'sampleId': instance.sampleId, + 'userName': instance.userName, + 'deptId': instance.deptId, + 'deptName': instance.deptName, + 'imagePath': instance.imagePath, + 'path': instance.path, + 'faceModel': instance.faceModel, + 'createTime': instance.createTime?.toIso8601String(), + 'updateTime': instance.updateTime?.toIso8601String(), + }; diff --git a/lib/common/services/database/entity/order_entity.dart b/lib/common/services/database/entity/order_entity.dart new file mode 100644 index 0000000..9ec762c --- /dev/null +++ b/lib/common/services/database/entity/order_entity.dart @@ -0,0 +1,99 @@ +import 'package:floor/floor.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'order_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "order_record") +class OrderEntity { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 唯一code + String uniqueCode; + + /// 服务端唯一标识 + String serviceUniqueCode; + + /// 平台类型 + SysType sysType; + + /// 用户唯一标识 + String userCode; + + /// 用户名 + String? username; + + /// 订单用餐类型:早、中、晚 + String? hourType; + + /// 购物车的总金额 + int price; + + /// 购物车额外金额 + int extraPrice; + + /// 结算总金额 + int payPrice; + + /// 钱包id + String? walletId; + + /// 订单类型渠道id + String? orderSourceTypeId; + + /// 订单类型 + OrderType type; + + /// 订单状态 + OrderStatus status; + + /// 支付方式 + OrderPayType payType; + + /// 商品描述 + String? productDesc; + + /// 商品总数量 食堂特有 + int count; + + /// 重量 超市特有 + double weight; + + /// 支付时间 + DateTime? payTime; + + /// 创建时间 + DateTime createTime; + + OrderEntity({ + this.id, + required this.uniqueCode, + required this.serviceUniqueCode, + required this.sysType, + required this.userCode, + this.username, + this.hourType, + required this.price, + required this.extraPrice, + required this.payPrice, + this.walletId, + this.orderSourceTypeId, + required this.type, + required this.status, + required this.payType, + this.productDesc, + required this.count, + required this.weight, + this.payTime, + required this.createTime, + }); + + factory OrderEntity.fromJson(Map json) => + _$OrderEntityFromJson(json); + + Map toJson() => _$OrderEntityToJson(this); +} diff --git a/lib/common/services/database/entity/order_entity.g.dart b/lib/common/services/database/entity/order_entity.g.dart new file mode 100644 index 0000000..234dc0d --- /dev/null +++ b/lib/common/services/database/entity/order_entity.g.dart @@ -0,0 +1,85 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OrderEntity _$OrderEntityFromJson(Map json) => OrderEntity( + id: (json['id'] as num?)?.toInt(), + uniqueCode: json['uniqueCode'] as String, + serviceUniqueCode: json['serviceUniqueCode'] as String, + sysType: $enumDecode(_$SysTypeEnumMap, json['sysType']), + userCode: json['userCode'] as String, + username: json['username'] as String?, + hourType: json['hourType'] as String?, + price: (json['price'] as num).toInt(), + extraPrice: (json['extraPrice'] as num).toInt(), + payPrice: (json['payPrice'] as num).toInt(), + walletId: json['walletId'] as String?, + orderSourceTypeId: json['orderSourceTypeId'] as String?, + type: $enumDecode(_$OrderTypeEnumMap, json['type']), + status: $enumDecode(_$OrderStatusEnumMap, json['status']), + payType: $enumDecode(_$OrderPayTypeEnumMap, json['payType']), + productDesc: json['productDesc'] as String?, + count: (json['count'] as num).toInt(), + weight: (json['weight'] as num).toDouble(), + payTime: json['payTime'] == null + ? null + : DateTime.parse(json['payTime'] as String), + createTime: DateTime.parse(json['createTime'] as String), + ); + +Map _$OrderEntityToJson(OrderEntity instance) => + { + 'id': instance.id, + 'uniqueCode': instance.uniqueCode, + 'serviceUniqueCode': instance.serviceUniqueCode, + 'sysType': _$SysTypeEnumMap[instance.sysType]!, + 'userCode': instance.userCode, + 'username': instance.username, + 'hourType': instance.hourType, + 'price': instance.price, + 'extraPrice': instance.extraPrice, + 'payPrice': instance.payPrice, + 'walletId': instance.walletId, + 'orderSourceTypeId': instance.orderSourceTypeId, + 'type': _$OrderTypeEnumMap[instance.type]!, + 'status': _$OrderStatusEnumMap[instance.status]!, + 'payType': _$OrderPayTypeEnumMap[instance.payType]!, + 'productDesc': instance.productDesc, + 'count': instance.count, + 'weight': instance.weight, + 'payTime': instance.payTime?.toIso8601String(), + 'createTime': instance.createTime.toIso8601String(), + }; + +const _$SysTypeEnumMap = { + SysType.supermarket: 'supermarket', + SysType.canteen: 'canteen', +}; + +const _$OrderTypeEnumMap = { + OrderType.below: 'below', + OrderType.reserve: 'reserve', + OrderType.foreignTicket: 'foreignTicket', + OrderType.strideDept: 'strideDept', +}; + +const _$OrderStatusEnumMap = { + OrderStatus.nonePay: 'nonePay', + OrderStatus.pay: 'pay', + OrderStatus.offlinePay: 'offlinePay', + OrderStatus.refund: 'refund', +}; + +const _$OrderPayTypeEnumMap = { + OrderPayType.walletDefault: 'walletDefault', + OrderPayType.walletQrCode: 'walletQrCode', + OrderPayType.walletCard: 'walletCard', + OrderPayType.face: 'face', + OrderPayType.strideDeptQrCode: 'strideDeptQrCode', + OrderPayType.foreignTicketQrCode: 'foreignTicketQrCode', + OrderPayType.reserveQrCode: 'reserveQrCode', +}; diff --git a/lib/common/services/database/entity/print_history_entity.dart b/lib/common/services/database/entity/print_history_entity.dart new file mode 100644 index 0000000..dcc44c7 --- /dev/null +++ b/lib/common/services/database/entity/print_history_entity.dart @@ -0,0 +1,55 @@ +import 'package:floor/floor.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +part 'print_history_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "print_history") +class PrintHistoryEntity { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 对应的订单id + String orderId; + + /// 对应的服务端id + String serviceOrderId; + + /// 服务端的打印id + String servicePrintId; + + /// 发票图片地址 + String invoiceUrl; + + /// 打印状态 + PrintStatus status; + + /// 打印时间 + String? printTime; + + /// 同步时间 + DateTime? syncTime; + + /// 创建时间 + DateTime createTime; + + PrintHistoryEntity({ + this.id, + required this.orderId, + required this.serviceOrderId, + required this.servicePrintId, + required this.invoiceUrl, + required this.status, + this.printTime, + this.syncTime, + required this.createTime, + }); + + factory PrintHistoryEntity.fromJson(Map json) => + _$PrintHistoryEntityFromJson(json); + + Map toJson() => _$PrintHistoryEntityToJson(this); +} diff --git a/lib/common/services/database/entity/print_history_entity.g.dart b/lib/common/services/database/entity/print_history_entity.g.dart new file mode 100644 index 0000000..65fa70e --- /dev/null +++ b/lib/common/services/database/entity/print_history_entity.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'print_history_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PrintHistoryEntity _$PrintHistoryEntityFromJson(Map json) => + PrintHistoryEntity( + id: (json['id'] as num?)?.toInt(), + orderId: json['orderId'] as String, + serviceOrderId: json['serviceOrderId'] as String, + servicePrintId: json['servicePrintId'] as String, + invoiceUrl: json['invoiceUrl'] as String, + status: $enumDecode(_$PrintStatusEnumMap, json['status']), + printTime: json['printTime'] as String?, + syncTime: json['syncTime'] == null + ? null + : DateTime.parse(json['syncTime'] as String), + createTime: DateTime.parse(json['createTime'] as String), + ); + +Map _$PrintHistoryEntityToJson(PrintHistoryEntity instance) => + { + 'id': instance.id, + 'orderId': instance.orderId, + 'serviceOrderId': instance.serviceOrderId, + 'servicePrintId': instance.servicePrintId, + 'invoiceUrl': instance.invoiceUrl, + 'status': _$PrintStatusEnumMap[instance.status]!, + 'printTime': instance.printTime, + 'syncTime': instance.syncTime?.toIso8601String(), + 'createTime': instance.createTime.toIso8601String(), + }; + +const _$PrintStatusEnumMap = { + PrintStatus.none: 'none', + PrintStatus.success: 'success', + PrintStatus.sync: 'sync', + PrintStatus.error: 'error', +}; diff --git a/lib/common/services/database/entity/product_entity.dart b/lib/common/services/database/entity/product_entity.dart new file mode 100644 index 0000000..1609e4d --- /dev/null +++ b/lib/common/services/database/entity/product_entity.dart @@ -0,0 +1,89 @@ +import 'package:floor/floor.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pj_supermarket_cloud/common/models/index.dart'; + +part 'product_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "product") +class ProductEntity extends Equatable { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 服务端唯一标识 + String serviceUniqueCode; + + /// 平台类型 + SysType sysType; + + /// 分类 serviceID + String classifyServiceUniqueCode; + + /// 菜品时间类型 + ProductHourType? hourType; + + /// 商品名 + String name; + + /// 商品价格(分) + int price; + + /// 商品图片 + String? image; + + /// 是否为默认 + bool isDefault; + + /// 商品状态 + int status; + + /// 排序 倒序排列 + int sort; + + /// 创建时间 + DateTime createTime; + + /// 同步时间 + DateTime updateTime; + + /// 是否删除 + bool delFlag; + + ProductEntity({ + this.id, + required this.serviceUniqueCode, + required this.sysType, + required this.classifyServiceUniqueCode, + required this.hourType, + required this.name, + required this.price, + this.image, + required this.isDefault, + required this.status, + required this.sort, + required this.createTime, + required this.updateTime, + required this.delFlag, + }); + + factory ProductEntity.fromJson(Map json) => + _$ProductEntityFromJson(json); + + Map toJson() => _$ProductEntityToJson(this); + + @override + List get props => [ + serviceUniqueCode, + sysType, + classifyServiceUniqueCode, + hourType, + name, + price, + image, + status, + sort, + ]; +} diff --git a/lib/common/services/database/entity/product_entity.g.dart b/lib/common/services/database/entity/product_entity.g.dart new file mode 100644 index 0000000..4307757 --- /dev/null +++ b/lib/common/services/database/entity/product_entity.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'product_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ProductEntity _$ProductEntityFromJson(Map json) => + ProductEntity( + id: (json['id'] as num?)?.toInt(), + serviceUniqueCode: json['serviceUniqueCode'] as String, + sysType: $enumDecode(_$SysTypeEnumMap, json['sysType']), + classifyServiceUniqueCode: json['classifyServiceUniqueCode'] as String, + hourType: $enumDecodeNullable(_$ProductHourTypeEnumMap, json['hourType']), + name: json['name'] as String, + price: (json['price'] as num).toInt(), + image: json['image'] as String?, + isDefault: json['isDefault'] as bool, + status: (json['status'] as num).toInt(), + sort: (json['sort'] as num).toInt(), + createTime: DateTime.parse(json['createTime'] as String), + updateTime: DateTime.parse(json['updateTime'] as String), + delFlag: json['delFlag'] as bool, + ); + +Map _$ProductEntityToJson(ProductEntity instance) => + { + 'id': instance.id, + 'serviceUniqueCode': instance.serviceUniqueCode, + 'sysType': _$SysTypeEnumMap[instance.sysType]!, + 'classifyServiceUniqueCode': instance.classifyServiceUniqueCode, + 'hourType': _$ProductHourTypeEnumMap[instance.hourType], + 'name': instance.name, + 'price': instance.price, + 'image': instance.image, + 'isDefault': instance.isDefault, + 'status': instance.status, + 'sort': instance.sort, + 'createTime': instance.createTime.toIso8601String(), + 'updateTime': instance.updateTime.toIso8601String(), + 'delFlag': instance.delFlag, + }; + +const _$SysTypeEnumMap = { + SysType.supermarket: 'supermarket', + SysType.canteen: 'canteen', +}; + +const _$ProductHourTypeEnumMap = { + ProductHourType.all: 'all', + ProductHourType.morning: 'morning', + ProductHourType.noon: 'noon', + ProductHourType.night: 'night', +}; diff --git a/lib/common/services/database/entity/setting_entity.dart b/lib/common/services/database/entity/setting_entity.dart new file mode 100644 index 0000000..0d02919 --- /dev/null +++ b/lib/common/services/database/entity/setting_entity.dart @@ -0,0 +1,33 @@ +import 'package:floor/floor.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'setting_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "setting") +class SettingEntity { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 用户信息 + String userJson; + + /// 设备信息 + String sysJson; + + /// 食堂额外金额信息 + String? canteenExtraPriceConfigJson; + + SettingEntity({ + this.id, + required this.userJson, + required this.sysJson, + this.canteenExtraPriceConfigJson, + }); + + factory SettingEntity.fromJson(Map json) => + _$SettingEntityFromJson(json); + + Map toJson() => _$SettingEntityToJson(this); +} diff --git a/lib/common/services/database/entity/setting_entity.g.dart b/lib/common/services/database/entity/setting_entity.g.dart new file mode 100644 index 0000000..2b7ab91 --- /dev/null +++ b/lib/common/services/database/entity/setting_entity.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'setting_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SettingEntity _$SettingEntityFromJson(Map json) => + SettingEntity( + id: (json['id'] as num?)?.toInt(), + userJson: json['userJson'] as String, + sysJson: json['sysJson'] as String, + canteenExtraPriceConfigJson: + json['canteenExtraPriceConfigJson'] as String?, + ); + +Map _$SettingEntityToJson(SettingEntity instance) => + { + 'id': instance.id, + 'userJson': instance.userJson, + 'sysJson': instance.sysJson, + 'canteenExtraPriceConfigJson': instance.canteenExtraPriceConfigJson, + }; diff --git a/lib/common/services/database/entity/shop_card_entity.dart b/lib/common/services/database/entity/shop_card_entity.dart new file mode 100644 index 0000000..28d6140 --- /dev/null +++ b/lib/common/services/database/entity/shop_card_entity.dart @@ -0,0 +1,53 @@ +import 'package:floor/floor.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'shop_card_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "shop_card") +class ShopCardEntity { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 购物车的id字段 + int orderId; + + /// 商品code + String productServiceUniqueCode; + + /// 商品名 + String productName; + + /// 商品图片 + String productPic; + + /// 商品价格(分) + int productPrice; + + /// 数量 食堂特有 + int productNumber; + + /// 重量 超市特有 + double productWidget; + + /// 单商品总价(分) + int totalPrice; + + ShopCardEntity({ + this.id, + required this.orderId, + required this.productServiceUniqueCode, + required this.productName, + required this.productPic, + required this.productPrice, + required this.productNumber, + required this.productWidget, + required this.totalPrice, + }); + + factory ShopCardEntity.fromJson(Map json) => + _$ShopCardEntityFromJson(json); + + Map toJson() => _$ShopCardEntityToJson(this); +} diff --git a/lib/common/services/database/entity/shop_card_entity.g.dart b/lib/common/services/database/entity/shop_card_entity.g.dart new file mode 100644 index 0000000..527185b --- /dev/null +++ b/lib/common/services/database/entity/shop_card_entity.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shop_card_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ShopCardEntity _$ShopCardEntityFromJson(Map json) => + ShopCardEntity( + id: (json['id'] as num?)?.toInt(), + orderId: (json['orderId'] as num).toInt(), + productServiceUniqueCode: json['productServiceUniqueCode'] as String, + productName: json['productName'] as String, + productPic: json['productPic'] as String, + productPrice: (json['productPrice'] as num).toInt(), + productNumber: (json['productNumber'] as num).toInt(), + productWidget: (json['productWidget'] as num).toDouble(), + totalPrice: (json['totalPrice'] as num).toInt(), + ); + +Map _$ShopCardEntityToJson(ShopCardEntity instance) => + { + 'id': instance.id, + 'orderId': instance.orderId, + 'productServiceUniqueCode': instance.productServiceUniqueCode, + 'productName': instance.productName, + 'productPic': instance.productPic, + 'productPrice': instance.productPrice, + 'productNumber': instance.productNumber, + 'productWidget': instance.productWidget, + 'totalPrice': instance.totalPrice, + }; diff --git a/lib/common/services/database/entity/user_entity.dart b/lib/common/services/database/entity/user_entity.dart new file mode 100644 index 0000000..8fd8d4d --- /dev/null +++ b/lib/common/services/database/entity/user_entity.dart @@ -0,0 +1,61 @@ +import 'package:floor/floor.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_entity.g.dart'; + +@JsonSerializable() +@Entity(tableName: "user_record") +class UserEntity { + /// 自增ID + @PrimaryKey(autoGenerate: true) + int? id; + + /// 用户id + String userId; + + /// 用户名 + String nickname; + + /// 手机号码 + String? mobile; + + /// 用户头像 + String? avatar; + + /// 钱包卡号 + String? walletCardNumber; + + /// 钱包id + String? walletId; + + /// 钱包类型id + String? walletTypeId; + + /// 钱包余额 + int? walletBalance; + + /// 创建时间 + DateTime? createTime; + + /// 同步时间 + DateTime? updateTime; + + UserEntity({ + this.id, + required this.userId, + required this.nickname, + this.mobile, + this.avatar, + this.walletCardNumber, + this.walletId, + this.walletTypeId, + this.walletBalance, + this.createTime, + this.updateTime, + }); + + factory UserEntity.fromJson(Map json) => + _$UserEntityFromJson(json); + + Map toJson() => _$UserEntityToJson(this); +} diff --git a/lib/common/services/database/entity/user_entity.g.dart b/lib/common/services/database/entity/user_entity.g.dart new file mode 100644 index 0000000..7a15502 --- /dev/null +++ b/lib/common/services/database/entity/user_entity.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserEntity _$UserEntityFromJson(Map json) => UserEntity( + id: (json['id'] as num?)?.toInt(), + userId: json['userId'] as String, + nickname: json['nickname'] as String, + mobile: json['mobile'] as String?, + avatar: json['avatar'] as String?, + walletCardNumber: json['walletCardNumber'] as String?, + walletId: json['walletId'] as String?, + walletTypeId: json['walletTypeId'] as String?, + walletBalance: (json['walletBalance'] as num?)?.toInt(), + createTime: json['createTime'] == null + ? null + : DateTime.parse(json['createTime'] as String), + updateTime: json['updateTime'] == null + ? null + : DateTime.parse(json['updateTime'] as String), + ); + +Map _$UserEntityToJson(UserEntity instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'nickname': instance.nickname, + 'mobile': instance.mobile, + 'avatar': instance.avatar, + 'walletCardNumber': instance.walletCardNumber, + 'walletId': instance.walletId, + 'walletTypeId': instance.walletTypeId, + 'walletBalance': instance.walletBalance, + 'createTime': instance.createTime?.toIso8601String(), + 'updateTime': instance.updateTime?.toIso8601String(), + }; diff --git a/lib/common/services/database/type_converter/type_converter.dart b/lib/common/services/database/type_converter/type_converter.dart new file mode 100644 index 0000000..2c307a0 --- /dev/null +++ b/lib/common/services/database/type_converter/type_converter.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; + +import 'package:floor/floor.dart'; +import 'package:common_utils/common_utils.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class DateTimeConverter extends TypeConverter { + @override + DateTime decode(String databaseValue) { + return DateUtil.getDateTime(databaseValue)!; + } + + @override + String encode(DateTime value) { + return DateUtil.formatDate(value); + } +} + +class DateTimeEmptyConverter extends TypeConverter { + @override + DateTime? decode(String databaseValue) { + if (ObjectUtil.isEmpty(databaseValue)) { + return null; + } + return DateUtil.getDateTime(databaseValue); + } + + @override + String encode(DateTime? value) { + if (ObjectUtil.isEmpty(value)) { + return ""; + } + return DateUtil.formatDate(value); + } +} + +class SysTypeConverter extends TypeConverter { + @override + SysType decode(String databaseValue) { + return SysType.values.byName(databaseValue); + } + + @override + String encode(SysType value) { + return value.name; + } +} + +class OrderTypeConverter extends TypeConverter { + @override + OrderType decode(String databaseValue) { + return OrderType.values.byName(databaseValue); + } + + @override + String encode(OrderType value) { + return value.name; + } +} + +class OrderPayTypeConverter extends TypeConverter { + @override + OrderPayType decode(String databaseValue) { + return OrderPayType.values.byName(databaseValue); + } + + @override + String encode(OrderPayType value) { + return value.name; + } +} + +class OrderStatusConverter extends TypeConverter { + @override + OrderStatus decode(String databaseValue) { + return OrderStatus.values.byName(databaseValue); + } + + @override + String encode(OrderStatus value) { + return value.name; + } +} + +class PrintStatusConverter extends TypeConverter { + @override + PrintStatus decode(String databaseValue) { + return PrintStatus.values.byName(databaseValue); + } + + @override + String encode(PrintStatus value) { + return value.name; + } +} + +class ProductHourTypeConverter extends TypeConverter { + @override + ProductHourType decode(String databaseValue) { + return ProductHourType.values.byName(databaseValue); + } + + @override + String encode(ProductHourType value) { + return value.name; + } +} + +class ListIntConverter extends TypeConverter?, String> { + @override + List decode(String databaseValue) { + if (databaseValue.isEmpty) { + return []; + } + return json.decode(databaseValue); + } + + @override + String encode(List? value) { + if (ObjectUtil.isEmptyList(value)) { + return ''; + } + return json.encode(value); + } +} + +class BoolConverter extends TypeConverter { + @override + bool decode(int databaseValue) { + return databaseValue == 1 ? true : false; + } + + @override + int encode(bool value) { + return value ? 1 : 0; + } +} diff --git a/lib/common/services/index.dart b/lib/common/services/index.dart new file mode 100644 index 0000000..184e382 --- /dev/null +++ b/lib/common/services/index.dart @@ -0,0 +1,23 @@ +library; + +export './database/database.dart'; + +export './database/type_converter/type_converter.dart'; + +export './database/dao/classify_dao.dart'; +export './database/dao/face_dao.dart'; +export './database/dao/order_dao.dart'; +export './database/dao/print_history_dao.dart'; +export './database/dao/product_dao.dart'; +export './database/dao/setting_dao.dart'; +export './database/dao/shop_card_dao.dart'; +export './database/dao/user_dao.dart'; + +export './database/entity/classify_entity.dart'; +export './database/entity/face_entity.dart'; +export './database/entity/order_entity.dart'; +export './database/entity/print_history_entity.dart'; +export './database/entity/product_entity.dart'; +export './database/entity/setting_entity.dart'; +export './database/entity/shop_card_entity.dart'; +export './database/entity/user_entity.dart'; diff --git a/lib/common/style/index.dart b/lib/common/style/index.dart new file mode 100644 index 0000000..fa8d106 --- /dev/null +++ b/lib/common/style/index.dart @@ -0,0 +1,3 @@ +library; + +// export './xxxx.dart'; diff --git a/lib/common/utils/app/app_face_tool.dart b/lib/common/utils/app/app_face_tool.dart new file mode 100644 index 0000000..6301843 --- /dev/null +++ b/lib/common/utils/app/app_face_tool.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter_subscreen_plugin/flutter_subscreen_plugin.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class AppFaceTool { + static const String _tag = "app 人脸工具"; + + // 跳转到人脸识别页面 + static Future jumpFacePage() async { + final faceCount = await FaceDiscernPlugs.getFaceCount(); + if (faceCount == 0) return false; + final isJump = await FaceDiscernPlugs.jumpFaceDiscernPage(); + if (!isJump) return false; + Future.delayed(const Duration(seconds: 2), () { + SubScreenPlugin.doubleScreenCancel(); + }); + return true; + } + + // 检查人脸激活参数 + static bool _validateSysFaceParams(SysSetting sysSetting) { + if (ObjectUtil.isEmpty(sysSetting.faceAppId)) { + return false; + } + if (ObjectUtil.isEmpty(sysSetting.faceSdkKey)) { + return false; + } + if (ObjectUtil.isEmpty(sysSetting.faceActiveKey)) { + return false; + } + return true; + } + + // 批量注册人脸 + static Future faceRegister({ + String? userId, + bool forceRegister = false, + }) async { + final isActive = await AppFaceTool.isActiveSdk(); + if (!isActive) return false; + var faceCount = await FaceDiscernPlugs.getFaceCount(); + if (faceCount == 0 || forceRegister) { + final list = await FaceDiscernPlugs.registerFaceByDatabase( + userId: userId, + ); + if (!ObjectUtil.isEmptyList(list)) { + final app = Get.find(); + final faceDao = app.dataBase.getFaceDao; + logger.i("|$_tag|批量注册人脸,错误的人脸:${json.encode(list)}"); + for (final v in list) { + final sampleId = v.toString(); + final userFace = await faceDao.findBySampleId(sampleId); + if (ObjectUtil.isNotEmpty(userFace)) { + if (ObjectUtil.isNotEmpty(userFace?.path)) { + await SyncFaceTool.delFaceByStorage(filePath: userFace!.path!); + } + await faceDao.delById(userFace!.id!); + await AppFaceTool.removeFace(sampleId: sampleId); + } + } + } + } + faceCount = await FaceDiscernPlugs.getFaceCount(); + if (faceCount == 0) return false; + return true; + } + + // 是否激活人脸sdk + static Future isActiveSdk() async { + var faceConfig = await FaceDiscernPlugs.getConfig(); + if (faceConfig == null || faceConfig.isActive == false) { + final sysSetting = await AppSettingTool.getSysSetting(); + final valid = AppFaceTool._validateSysFaceParams(sysSetting); + if (!valid) return false; + final isActive = await FaceDiscernPlugs.active( + appId: sysSetting.faceAppId!, + sdkKey: sysSetting.faceSdkKey!, + activeKey: sysSetting.faceActiveKey!, + ); + if (isActive) { + faceConfig = await FaceDiscernPlugs.getConfig(); + } + } + if (faceConfig == null) { + return false; + } + return faceConfig.isActive; + } + + // 删除人脸 + static Future removeFace({String? sampleId}) async { + if (sampleId == null) return false; + final isActive = await AppFaceTool.isActiveSdk(); + if (!isActive) return false; + return await FaceDiscernPlugs.removeFace(sampleId: sampleId); + } +} diff --git a/lib/common/utils/app/app_file_tool.dart b/lib/common/utils/app/app_file_tool.dart new file mode 100644 index 0000000..40bcd9c --- /dev/null +++ b/lib/common/utils/app/app_file_tool.dart @@ -0,0 +1,85 @@ +import 'dart:io'; + +import 'package:external_path/external_path.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +import 'package:pj_supermarket_cloud/common/utils/index.dart'; + +abstract class AppFileTool { + static const _tag = "app文件工具"; + static const _baseDir = "pj"; + static const _baseBackupDir = "backup"; + static const _backupDatabaseDir = ".database"; + static const _backupLoggerDir = "logger"; + static const _backupFaceDir = ".face"; + + /// 检查并创建目录 + static Future _checkAndcreateDir(String dir) async { + Directory targetDir = Directory(dir); + if (!await targetDir.exists()) { + await targetDir.create(recursive: true); + } + return dir; + } + + /// 获取外部路径 + static Future getExternalPath({ + bool backupExternalPath = true, + }) async { + final documentsDir = await ExternalPath.getExternalStoragePublicDirectory( + ExternalPath.DIRECTORY_DOCUMENTS, + ); + final backupDir = "$documentsDir/$_baseDir"; + if (backupExternalPath) { + final sysSetting = await AppSettingTool.getSysSetting(); + sysSetting.externalPath = documentsDir; + await AppSettingTool.setSysSetting(sysSetting); + } + return backupDir; + } + + /// 创建数据库文件地址 + static Future getDatabasePath() async { + final backupDir = await getExternalPath( + backupExternalPath: false, + ); + return _checkAndcreateDir("$backupDir/$_backupDatabaseDir"); + } + + /// 创建数据库文件备份地址 + static Future getDatabaseBackupPath() async { + final backupDir = await getExternalPath(); + return _checkAndcreateDir("$backupDir/$_baseBackupDir/$_backupDatabaseDir"); + } + + /// 创建日志地址 + static Future getLoggerPath() async { + final backupDir = await getExternalPath(); + return _checkAndcreateDir("$backupDir/$_backupLoggerDir"); + } + + /// 创建日志备份地址 + static Future getLoggerBackupPath() async { + final backupDir = await getExternalPath(); + String dir = "$backupDir/$_baseBackupDir/$_backupLoggerDir"; + DateTime now = DateTime.now(); + int year = now.year; + int month = now.month; + int day = now.day; + dir = "$dir/$year-$month"; + dir = await _checkAndcreateDir(dir); + return "$dir/$day.log"; + } + + /// 创建人脸地址 + static Future getFacePath() async { + final backupDir = await getExternalPath(); + return _checkAndcreateDir("$backupDir/$_backupFaceDir"); + } + + /// 创建人脸备份地址 + static Future getFaceBackupPath() async { + final backupDir = await getExternalPath(); + return _checkAndcreateDir("$backupDir/$_baseBackupDir/$_backupFaceDir"); + } +} diff --git a/lib/common/utils/app/app_mq_tool.dart b/lib/common/utils/app/app_mq_tool.dart new file mode 100644 index 0000000..9735300 --- /dev/null +++ b/lib/common/utils/app/app_mq_tool.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; + +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; +import 'package:mqtt_client/mqtt_client.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class AppMqTool { + static const String _tag = "app mq 工具"; + + static Future mqRefresh() async { + final isNetwork = AppNetworkTool.isAutoNetwork(); + if (!isNetwork) { + logger.e("|$_tag|mqtt监听消息错误没有网络"); + return; + } + final app = Get.find(); + var mqListen = app.mqListen; + + final sysInfo = await AppSettingTool.getSysSetting(); + // mqtt + if (ObjectUtil.isEmpty(sysInfo.serviceId)) return; + await MqManager.refresh(); + if (MqManager.state != MqttConnectionState.connected) { + return; + } + if (ObjectUtil.isNotEmpty(mqListen)) AppMqTool.mqListenClose(); + final appMerchantDeptId = sysInfo.merchantDeptId!; + final appHardwareId = sysInfo.serviceId!; + final appHardGroupId = sysInfo.hardGroupId!; + mqListen = MqManager.mqttClient?.updates?.listen((event) { + String topic = event[0].topic; + final msg = event[0].payload as MqttPublishMessage; + topic = topic.replaceAll('topic/android/', ''); + if (ObjectUtil.isEmpty(topic)) return; + final splitList = topic.split("/"); + String? merchantDeptId; + String? hardwareId; + String? hardGroupId; + if (splitList.isEmpty) { + return; + } + if (splitList.length == 1) { + hardGroupId = splitList[0].toString(); + if (appHardGroupId != hardGroupId) return; + } + if (splitList.length == 2) { + hardGroupId = splitList[0].toString(); + merchantDeptId = splitList[1].toString(); + if (appHardGroupId != hardGroupId) return; + if (appMerchantDeptId != merchantDeptId) return; + } + if (splitList.length == 3) { + hardGroupId = splitList[0].toString(); + merchantDeptId = splitList[1].toString(); + hardwareId = splitList[2].toString(); + if (appHardGroupId != hardGroupId) return; + if (appMerchantDeptId != merchantDeptId) return; + if (appHardwareId != hardwareId) return; + } + try { + final message = msg.payload.message; + String str = MqttPublishPayload.bytesToStringAsString(message); + logger.i("|$_tag|mqtt 接收到的消息:$str;"); + final mqMsg = MqBaseMsg.fromJson(json.decode(str)); + dispatchMqttMessage(mqMsg, str); + } catch (e) { + logger.e("|$_tag|mqtt监听消息错误error:|$e|"); + } + }); + } + + // 根据 msg action 来处理 mq 消息 + static void dispatchMqttMessage(MqBaseMsg msg, String orgStr) async { + if (ObjectUtil.isEmpty(msg.action)) return; + final app = Get.find(); + final mqUpdateEventBus = app.mqUpdateEventBus; + switch (msg.action!) { + case MqtActionEnum.config: + await SyncConfigTool.autoSyncSysInfo(); + break; + case MqtActionEnum.canteenProduct: + await SyncProductTool.syncProduct(); + mqUpdateEventBus.fire(MqtActionEnum.canteenProduct); + break; + case MqtActionEnum.supermarketProduct: + await SyncProductTool.syncProduct(); + mqUpdateEventBus.fire(MqtActionEnum.supermarketProduct); + break; + case MqtActionEnum.canteenClassify: + await SyncProductTool.syncClassify(); + mqUpdateEventBus.fire(MqtActionEnum.canteenClassify); + break; + case MqtActionEnum.supermarketClassify: + await SyncProductTool.syncClassify(); + mqUpdateEventBus.fire(MqtActionEnum.supermarketClassify); + break; + case MqtActionEnum.print: + mqUpdateEventBus.fire(MqtActionEnum.print); + final msg = MqttInvoiceMsg.fromJson(json.decode(orgStr)); + if (ObjectUtil.isNotEmpty(msg.historyId) && + ObjectUtil.isNotEmpty(msg.orderId) && + ObjectUtil.isNotEmpty(msg.androidOrderCode) && + ObjectUtil.isNotEmpty(msg.pic)) { + SyncPrintTool.addTask( + orderId: msg.orderId!, + androidOrderId: msg.androidOrderCode!, + historyId: msg.historyId!, + pic: msg.pic!, + ); + } + break; + case MqtActionEnum.face: + final msg = MqttInvoiceMsg.fromJson(json.decode(orgStr)); + final storagePath = await AppFileTool.getFacePath(); + if (ObjectUtil.isEmpty(storagePath)) { + logger.e("|$_tag|mq同步人脸错误,创建人脸存储路径错误"); + return; + } + final sysSetting = await AppSettingTool.getSysSetting(); + var merchantDeptId = sysSetting.merchantDeptId; + if (ObjectUtil.isEmpty(storagePath)) { + logger.e("|$_tag|mq同步人脸错误,merchantId为空"); + return; + } + await SyncFaceTool.syncGroupFace( + pageNo: 1, + userId: msg.userId, + merchantDeptId: merchantDeptId!, + storagePath: storagePath!, + isGlobalFace: msg.isGlobalFace, + ); + mqUpdateEventBus.fire(MqtActionEnum.face); + break; + default: + break; + } + } + + static void mqListenClose() { + final app = Get.find(); + var mqListen = app.mqListen; + mqListen?.cancel(); + mqListen = null; + } +} diff --git a/lib/common/utils/app/app_network_tool.dart b/lib/common/utils/app/app_network_tool.dart new file mode 100644 index 0000000..6674526 --- /dev/null +++ b/lib/common/utils/app/app_network_tool.dart @@ -0,0 +1,132 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:image/image.dart' as imglib; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class AppNetworkTool { + static const _tag = "判断是否有网络"; + + static StreamSubscription>? subscription; + + static bool isNetworkOssImage(String imagePath) { + return imagePath.contains("?fileName="); + } + + // 是否是网络图片 + static bool isNetworkImage(String imagePath) { + if (isNetworkOssImage(imagePath)) return true; + final networkImagePattern = RegExp(r'^(http|https)://'); + return networkImagePattern.hasMatch(imagePath); + } + + // 手动判断有没有要网络 todo 不建议使用 + static Future isHandNetwork() async { + Stopwatch apiStopwatch = Stopwatch(); + apiStopwatch.start(); + final List list = + await Connectivity().checkConnectivity(); + final bool = !list.contains(ConnectivityResult.none); + apiStopwatch.stop(); + logger.d('|$_tag|判断是否有网络的耗时: ${apiStopwatch.elapsedMilliseconds} 毫秒'); + return bool; + } + + // 自动判断是否有网络 + static bool isAutoNetwork() { + final app = Get.find(); + return app.isNetwork.value; + } + + // 自动判断是否有网络加msg + static bool isAutoNetworkMsg() { + final isNetwork = AppNetworkTool.isAutoNetwork(); + if (!isNetwork) { + Toast.error(msg: "当前没有网络"); + return false; + } + return true; + } + + // 生成离线网络用户 + static ServiceUser generateOfflinePayUser({ + String userName = "离线用户", + String cardNumber = "离线用户", + }) { + return ServiceUser( + username: userName, + walletCardNumber: cardNumber, + userMobile: "离线用户", + ); + } + + // 自动监听网络变化 + static void listenNetwork() async { + subscription = Connectivity() + .onConnectivityChanged + .listen((List list) { + final bool = !list.contains(ConnectivityResult.none); + final app = Get.find(); + if (bool != app.isNetwork.value) { + app.isNetwork.value = bool; + AppMqTool.mqRefresh(); + logger.w("|$_tag|监听网络变化,当前网络已经${bool ? '连接' : '断开'}"); + if (bool) SyncOrderTool.syncOfflineOrder(); + } + }); + } + + // 关闭监听网络 + static void closeListenNetwork() { + subscription?.cancel(); + } + + /// 从网络中获取图片 + static Future loadImageFromNetwork(String url) async { + try { + String networkImage = url; + if (AppNetworkTool.isNetworkOssImage(networkImage)) { + networkImage = "${Constants.ossUrl}$networkImage"; + } + final response = await http.get(Uri.parse(networkImage)); + if (response.statusCode == 200) { + final Uint8List imageData = response.bodyBytes; + final decodeImage = imglib.decodeImage(imageData); + return decodeImage; + } else { + logger.e( + '|$_tag|从网络中获取图片statusCode错误statusCode:|${response.statusCode}|'); + return null; + } + } catch (e) { + logger.e('|$_tag|从网络中获取图片错误:|$e|'); + return null; + } + } + + // 从网络中下载图片 + static Future downloadImage(String url) async { + String networkImage = url; + if (AppNetworkTool.isNetworkOssImage(networkImage)) { + networkImage = "${Constants.ossUrl}$networkImage"; + } + try { + final response = await http + .get(Uri.parse(networkImage)) + .timeout(const Duration(seconds: 30)); + if (response.statusCode != 200) return null; + return response.bodyBytes; + } catch (e) { + logger.e('|$_tag|从网络中下载图片错误:|$e|'); + return null; + } + } + + static String ossImage(String src) { + return "${Constants.ossUrl}$src"; + } +} diff --git a/lib/common/utils/app/app_setting_tool.dart b/lib/common/utils/app/app_setting_tool.dart new file mode 100644 index 0000000..6b081dc --- /dev/null +++ b/lib/common/utils/app/app_setting_tool.dart @@ -0,0 +1,218 @@ +import 'dart:convert'; + +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class AppSettingTool { + static const String _tag = "app配置工具"; + + static Future _getSetting() async { + final app = Get.find(); + final settingDao = app.dataBase.getSettingDao; + final setting = await settingDao.get(); + if (setting == null) { + final initSysInfo = SysSetting( + sysType: SysType.canteen, + androidId: await AppSysTool.getAndroidId(), + sqlAddress: "", + ); + final initUserInfo = UserSetting( + username: "", + password: "", + ); + SettingEntity addObj = SettingEntity( + userJson: json.encode(initUserInfo.toJson()), + sysJson: json.encode(initSysInfo.toJson()), + ); + final addId = await settingDao.save(addObj); + if (ObjectUtil.isEmpty(addId)) { + throw Exception("|$_tag|_getSetting新增错误"); + } + addObj.id = addId; + return addObj; + } + return setting; + } + + static Future _setSetting({ + SysSetting? sysJson, + UserSetting? userJson, + CanteenExtraPriceSetting? canteenExtraPriceConfig, + }) async { + if (ObjectUtil.isEmpty(sysJson) && + ObjectUtil.isEmpty(userJson) && + ObjectUtil.isEmpty(canteenExtraPriceConfig)) { + return false; + } + final app = Get.find(); + final settingDao = app.dataBase.getSettingDao; + SettingEntity? setting = await settingDao.get(); + if (setting == null) { + logger.e("|$_tag|_setSetting新增错误settingDao.get()方法返回为null"); + return false; + } + if (sysJson != null) { + setting.sysJson = json.encode(sysJson.toJson()); + } + if (userJson != null) { + setting.userJson = json.encode(userJson.toJson()); + } + if (canteenExtraPriceConfig != null) { + setting.canteenExtraPriceConfigJson = json.encode( + canteenExtraPriceConfig.toJson(), + ); + } + if (ObjectUtil.isNotEmpty(userJson)) {} + if (ObjectUtil.isNotEmpty(canteenExtraPriceConfig)) {} + await settingDao.edit(setting); + return true; + } + + static Future getSysSetting() async { + final setting = await _getSetting(); + return SysSetting.fromJson(json.decode(setting.sysJson)); + } + + static Future getUserSetting() async { + final setting = await _getSetting(); + return UserSetting.fromJson(json.decode(setting.userJson)); + } + + static Future getCanteenExtraPriceConfig() async { + final setting = await _getSetting(); + if (ObjectUtil.isEmpty(setting.canteenExtraPriceConfigJson)) { + return null; + } + return CanteenExtraPriceSetting.fromJson( + json.decode(setting.canteenExtraPriceConfigJson!), + ); + } + + static Future setSysSetting(SysSetting sysInfo) async { + final globalSysInfo = await getSysSetting(); + if (globalSysInfo == sysInfo) return; + await _setSetting(sysJson: sysInfo); + } + + static Future setUserSetting(UserSetting userInfo) async { + final globalUserInfo = await getUserSetting(); + if (globalUserInfo == userInfo) return; + await _setSetting(userJson: userInfo); + } + + static Future setCanteenExtraPriceConfig( + CanteenExtraPriceSetting canteenExtraPriceConfig, + ) async { + final config = await getCanteenExtraPriceConfig(); + if (config == canteenExtraPriceConfig) return; + await _setSetting(canteenExtraPriceConfig: canteenExtraPriceConfig); + } + + // 根据后台返回的数据更新到缓存 + static Future updateSysInfoByService(ServiceHardware data) async { + if (ObjectUtil.isEmpty(data)) { + final setting = SysSetting( + sysType: SysType.canteen, + androidId: await AppSysTool.getAndroidId(), + sqlAddress: await AppFileTool.getDatabaseBackupPath(), + ); + await _setSetting(sysJson: setting); + return; + } + final sysSetting = await AppSettingTool.getSysSetting(); + final id = data.id; + if (ObjectUtil.isNotEmpty(id)) { + sysSetting.serviceId = id; + } + final name = data.name; + if (ObjectUtil.isNotEmpty(name)) { + sysSetting.serviceName = name; + } + final type = data.type.toString(); + if (ObjectUtil.isNotEmpty(type)) { + final t = SysType.values.fromCode(type); + if (ObjectUtil.isNotEmpty(t)) { + sysSetting.sysType = t!; + } + } + final merchantId = data.merchantId; + if (ObjectUtil.isNotEmpty(merchantId)) { + sysSetting.serviceMerchantId = merchantId; + } + final hardGroupId = data.hardGroupId; + if (ObjectUtil.isNotEmpty(hardGroupId)) { + sysSetting.hardGroupId = hardGroupId; + } + final merchantDeptId = data.merchantDeptId; + if (ObjectUtil.isNotEmpty(merchantDeptId)) { + sysSetting.merchantDeptId = merchantDeptId; + } + final mqClientId = data.mqClientId; + if (ObjectUtil.isNotEmpty(mqClientId)) { + sysSetting.mqClientId = mqClientId; + } + final mainTopic = data.mainTopic; + if (ObjectUtil.isNotEmpty(mainTopic)) { + sysSetting.mainTopic = mainTopic; + } + final mqSocketAddress = data.mqSocketAddress; + if (ObjectUtil.isNotEmpty(mqSocketAddress)) { + sysSetting.mqSocketAddress = mqSocketAddress; + } + final mqSocketPort = data.mqSocketPort; + if (ObjectUtil.isNotEmpty(mqSocketPort)) { + sysSetting.mqSocketPort = mqSocketPort; + } + final mqUsername = data.mqUsername; + if (ObjectUtil.isNotEmpty(mqUsername)) { + sysSetting.mqUsername = mqUsername; + } + final mqPassword = data.mqPassword; + if (ObjectUtil.isNotEmpty(mqPassword)) { + sysSetting.mqPassword = mqPassword; + } + final faceAppId = data.faceAppId; + if (ObjectUtil.isNotEmpty(faceAppId)) { + sysSetting.faceAppId = faceAppId; + } + final faceSdkKey = data.faceSdkKey; + if (ObjectUtil.isNotEmpty(faceSdkKey)) { + sysSetting.faceSdkKey = faceSdkKey; + } + final faceActiveKey = data.faceActiveKey; + if (ObjectUtil.isNotEmpty(faceActiveKey)) { + sysSetting.faceActiveKey = faceActiveKey; + } + final android = data.uniqueCode; + if (ObjectUtil.isNotEmpty(android)) { + sysSetting.androidId = android; + } + sysSetting.syncTime = DateTime.now(); + await _setSetting(sysJson: sysSetting); + } + + /// 绑定设备信息 + static Future bindSys(int serviceId) async { + final sysSetting = await AppSettingTool.getSysSetting(); + var androidId = sysSetting.androidId; + if (ObjectUtil.isEmpty(androidId)) { + androidId = await AppSysTool.getAndroidId(); + if (ObjectUtil.isEmpty(androidId)) return false; + } + final pa = {"id": serviceId, "uniqueCode": androidId}; + try { + final data = await HardApi.bind(pa); + if (data == null) { + logger.e("绑定设备信息pa:|${jsonEncode(pa)}|;res:$data;"); + return false; + } + await AppSettingTool.updateSysInfoByService(data); + return true; + } catch (e, stack) { + logger.e("|$_tag|绑定信息错误error:|$e|stack:$stack"); + } + return false; + } +} diff --git a/lib/common/utils/app/app_sys_tool.dart b/lib/common/utils/app/app_sys_tool.dart new file mode 100644 index 0000000..24847e1 --- /dev/null +++ b/lib/common/utils/app/app_sys_tool.dart @@ -0,0 +1,21 @@ +import 'package:flutter/foundation.dart'; + +import 'package:android_id/android_id.dart'; + +class AppSysTool { + static const String _tag = "app系统工具"; + + static Future getAndroidId() async { + const androidIdPlugin = AndroidId(); + final String? androidId = await androidIdPlugin.getId(); + return androidId ?? ""; + } + + static bool isProd() { + return !kDebugMode; + } + + static bool isDev() { + return kDebugMode; + } +} diff --git a/lib/common/utils/app/app_user_tool.dart b/lib/common/utils/app/app_user_tool.dart new file mode 100644 index 0000000..84ae269 --- /dev/null +++ b/lib/common/utils/app/app_user_tool.dart @@ -0,0 +1,12 @@ +import 'package:common_utils/common_utils.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class AppUserTool { + // 是否登陆 + static Future isLogin() async { + final userSetting = await AppSettingTool.getUserSetting(); + final accessToken = userSetting.accessToken; + return ObjectUtil.isNotEmpty(accessToken); + } +} diff --git a/lib/common/utils/app/control/app_control.dart b/lib/common/utils/app/control/app_control.dart new file mode 100644 index 0000000..c11a07b --- /dev/null +++ b/lib/common/utils/app/control/app_control.dart @@ -0,0 +1,57 @@ +import 'dart:async'; +import 'package:event_bus/event_bus.dart'; + +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class AppControl extends GetxController { + static const String _tag = "AppControl"; + + /// 打印机 + late PrintManage printManage; + + /// 二维码读头 + late ScanCodeManage scanCodeManage; + + /// 读卡器串口 + late SerialCardManage serialCardManage; + + /// 称串口 + late SerialSteelyardManage srialSteelyardManage; + + /// mq + StreamSubscription? mqListen; + EventBus mqUpdateEventBus = EventBus(); + + /// 数据库 + late final AppDatabase dataBase; + + // 是否有网络 + RxBool isNetwork = RxBool(false); + + Rx hourType = Rx(ProductHourType.morning); + + @override + void onClose() { + super.onClose(); + printManage.close(); + scanCodeManage.close(); + serialCardManage.close(); + srialSteelyardManage.close(); + AppNetworkTool.closeListenNetwork(); + } + + Future initAsync() async { + final stopwatch = Stopwatch()..start(); + dataBase = await AppDatabase.getDataBase(); + printManage = PrintManage(); + scanCodeManage = ScanCodeManage(); + serialCardManage = SerialCardManage(); + srialSteelyardManage = SerialSteelyardManage(); + AppNetworkTool.listenNetwork(); + Get.offAllNamed(RouteNames.login); + stopwatch.stop(); + logger.d('|$_tag|initAsync耗时:${stopwatch.elapsedMilliseconds}毫秒'); + } +} diff --git a/lib/common/utils/app/control/app_theme_control.dart b/lib/common/utils/app/control/app_theme_control.dart new file mode 100644 index 0000000..2900d78 --- /dev/null +++ b/lib/common/utils/app/control/app_theme_control.dart @@ -0,0 +1,10 @@ +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +class AppThemeControl extends GetxController { + /// 主题 + Rx theme = FThemes.zinc.light.obs; + + /// 字体大小 + RxDouble sizeScalar = 1.4.obs; +} diff --git a/lib/common/utils/index.dart b/lib/common/utils/index.dart new file mode 100644 index 0000000..fcbcd12 --- /dev/null +++ b/lib/common/utils/index.dart @@ -0,0 +1,41 @@ +library; + +export './app/control/app_control.dart'; +export './app/control/app_theme_control.dart'; + +export './app/app_face_tool.dart'; +export './app/app_file_tool.dart'; +export './app/app_mq_tool.dart'; +export './app/app_network_tool.dart'; +export './app/app_setting_tool.dart'; +export './app/app_sys_tool.dart'; +export './app/app_user_tool.dart'; + +export './json/json_tool.dart'; + +export './keyboard/keyboard_tool.dart'; + +export './logger/logger_tool.dart'; + +export './manage/base/serial_base.dart'; +export './manage/base/base_facility_manage.dart'; +export './manage/tool/serial_read_card_tool.dart'; +export './manage/tool/serial_tool.dart'; +export './manage/http_manage.dart'; +export './manage/mq_manager.dart'; +export './manage/print_manage.dart'; +export './manage/serial_card_manage.dart'; +export './manage/serial_steelyard_manage.dart'; +export './manage/scan_code_manage.dart'; + +export './plugs/face_discern_plugs.dart'; +export './plugs/local_tts_plugs.dart'; + +export './router/router_observer_tool.dart'; + +export './sync/sync_config_tool.dart'; +export './sync/sync_face_tool.dart'; +export './sync/sync_order_tool.dart'; +export './sync/sync_print_tool.dart'; +export './sync/sync_product_tool.dart'; +export './sync/sync_user_tool.dart'; diff --git a/lib/common/utils/json/json_tool.dart b/lib/common/utils/json/json_tool.dart new file mode 100644 index 0000000..7ea6fe6 --- /dev/null +++ b/lib/common/utils/json/json_tool.dart @@ -0,0 +1,30 @@ +abstract class JsonTool { + static bool stringToBoolean(String value) { + return value == '1'; + } + + static String? booleanToString(bool? value) { + if (value == null) return null; + return value ? '1' : '0'; + } + + static int? stringToInt(String? value) { + if (value == null) return null; + return int.tryParse(value); + } + + static String? intToString(int? value) { + if (value == null) return null; + return value.toString(); + } + + static String? anyToString(dynamic value) { + if (value == null) return null; + return value.toString(); + } + + static Map? anyToMap(dynamic value) { + if (value == null) return null; + return dynamic as Map?; + } +} diff --git a/lib/common/utils/keyboard/keyboard_tool.dart b/lib/common/utils/keyboard/keyboard_tool.dart new file mode 100644 index 0000000..156cab9 --- /dev/null +++ b/lib/common/utils/keyboard/keyboard_tool.dart @@ -0,0 +1,13 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/services.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class KeywordTool { + static Future copy(String? text) async { + if (ObjectUtil.isEmpty(text)) return; + Clipboard.setData(ClipboardData(text: text!)).then( + (v) => Toast.success(msg: "复制成功"), + ); + } +} diff --git a/lib/common/utils/logger/logger_tool.dart b/lib/common/utils/logger/logger_tool.dart new file mode 100644 index 0000000..7fcee46 --- /dev/null +++ b/lib/common/utils/logger/logger_tool.dart @@ -0,0 +1,65 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:common_utils/common_utils.dart'; +import 'package:logger/logger.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +final logger = Logger( + printer: customPrinter, + output: ConsoleOutput(), +); +final customPrinter = PrettyPrinter( + lineLength: 0, + methodCount: 1, + errorMethodCount: 1, + colors: true, + printEmojis: false, + dateTimeFormat: DateTimeFormat.dateAndTime, +); + +class ConsoleOutput extends LogOutput { + String? _filePath; + IOSink? _sink; + Timer? _timer; + + @override + Future init() async { + _filePath = await AppFileTool.getLoggerBackupPath(); + if (ObjectUtil.isNotEmpty(_filePath)) { + _sink = File(_filePath!).openWrite( + mode: FileMode.writeOnlyAppend, + encoding: utf8, + ); + } + _timer = Timer.periodic(const Duration(seconds: 60 * 60 * 2), + (Timer timer) async { + final path = await AppFileTool.getLoggerBackupPath(); + if (path == null) return; + _filePath = path; + }); + } + + @override + Future destroy() async { + await _sink?.flush(); + await _sink?.close(); + _timer?.cancel(); + } + + @override + void output(OutputEvent event) { + if (AppSysTool.isDev()) { + for (final v in event.lines) { + print(v); + } + } else { + if (_filePath == null) return; + if (_sink == null) return; + if (Level.debug == event.level) return; + _sink?.writeAll(event.lines, '\n'); + } + } +} diff --git a/lib/common/utils/manage/base/base_facility_manage.dart b/lib/common/utils/manage/base/base_facility_manage.dart new file mode 100644 index 0000000..293fd7c --- /dev/null +++ b/lib/common/utils/manage/base/base_facility_manage.dart @@ -0,0 +1,63 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +class BaseFacilityManage { + bool isPermission = false; + int applyPermissionCount = 0; + int maxApplyPermissionCount = 1; + + String setName() { + return "未知"; + } + + // 打开设备 + Future open() async { + isPermission = await checkPermission(); + if (!isPermission) { + logger.i("usb设备设备|${setName()}|的权限错误"); + await _applyPermission(); + if (!isPermission) { + return false; + } + } + return true; + } + + // 关闭设备 + Future close() async {} + + // 检查权限 + Future checkPermission() async { + return false; + } + + // 监听 + void listen(void Function(T event)? fc) {} + + // 申请权限 + Future applyPermission() async { + return false; + } + + // 申请权限 + Future _applyPermission() async { + logger.i("尝试申请权限,申请次数上线|$maxApplyPermissionCount|"); + while (true) { + if (applyPermissionCount >= maxApplyPermissionCount) { + applyPermissionCount = 0; + break; + } + final flag = await applyPermission(); + final str = + "尝试申请权限,第|$applyPermissionCount|次,申请次数上线|$maxApplyPermissionCount|,申请${flag ? "成功" : "失败"}"; + logger.i(str); + if (isPermission != flag) { + isPermission = flag; + } + if (flag) { + applyPermissionCount = 0; + break; + } + applyPermissionCount++; + } + } +} diff --git a/lib/common/utils/manage/base/serial_base.dart b/lib/common/utils/manage/base/serial_base.dart new file mode 100644 index 0000000..0a0b385 --- /dev/null +++ b/lib/common/utils/manage/base/serial_base.dart @@ -0,0 +1,83 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter_libserialport/flutter_libserialport.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SerialBase { + String _name = "未知"; + String _portName = ""; + int _baudRate = 9600; + int _readTimeout = 500; + + late SerialPort serialClient; + SerialPortReader? readPort; + + bool isConnect = false; + + set name(String name) { + _name = name; + } + + set portName(String portName) { + _portName = portName; + } + + set baudRate(int baudRate) { + _baudRate = baudRate; + } + + set readTimeout(int readTimeout) { + _readTimeout = readTimeout; + } + + Future connect() async { + try { + if (ObjectUtil.isEmpty(_portName)) return; + if (isConnect) return; + serialClient = SerialPort(_portName); + if (serialClient.isOpen) { + serialClient.close(); + } + bool isOpenRead = serialClient.openReadWrite(); + if (isOpenRead && serialClient.isOpen) { + SerialPortConfig config = serialClient.config; + config.baudRate = _baudRate; + serialClient.config = config; + isConnect = true; + logger.d(logMsg("连接成功")); + } + } catch (e) { + logger.e(logMsg("连接失败error:|$e|")); + } + } + + Future listen(Function(List) fc) async { + if (isConnect == false) return; + readPort = SerialPortReader(serialClient, timeout: _readTimeout); + if (ObjectUtil.isEmpty(readPort)) return; + logger.d(logMsg("开始监听串口数据")); + readPort!.stream.listen((event) async { + // logger.w(logMsg("监听串口数据:event:|$event|string:${bytesToString(event)}")); + fc(event); + }); + } + + void readerClose() { + if (ObjectUtil.isNotEmpty(readPort)) { + readPort!.close(); + readPort = null; + } + } + + void close() { + if (isConnect == true) { + serialClient.dispose(); + serialClient.close(); + isConnect = false; + } + } + + String logMsg(String msg) { + return "串口设备|$_name|$msg"; + } +} diff --git a/lib/common/utils/manage/http_manage.dart b/lib/common/utils/manage/http_manage.dart new file mode 100644 index 0000000..89c658d --- /dev/null +++ b/lib/common/utils/manage/http_manage.dart @@ -0,0 +1,168 @@ +import 'dart:io'; + +import 'package:common_utils/common_utils.dart'; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +const ignoreMsg = ["账号未登录"]; + +class HttpManage { + static const _tag = "http管理"; + + static final HttpManage _instance = HttpManage._internal(); + + factory HttpManage() => _instance; + + late Dio dio; + + HttpManage._internal() { + BaseOptions globalOptions = BaseOptions( + baseUrl: Constants.apiUrl, + connectTimeout: const Duration(seconds: 10), + responseType: ResponseType.json, + ); + dio = Dio(globalOptions); + // 拦截 + dio.interceptors.add( + InterceptorsWrapper( + onRequest: (RequestOptions options, handler) async { + options.headers["CLIENT-TOC"] = "Y"; + options.headers["TENANT-ID"] = 1; + if (!options.path.contains(Constants.oauth2AppApiUrl)) { + final userInfo = await AppSettingTool.getUserSetting(); + if (ObjectUtil.isNotEmpty(userInfo.accessToken)) { + options.headers['Authorization'] = + "Bearer ${userInfo.accessToken}"; + } + } + return handler.next(options); + }, + onResponse: (e, handler) async { + // 先判断是不是登录接口 + late ServiceBase serviceData; + if (e.requestOptions.path.contains(Constants.oauth2AppApiUrl)) { + serviceData = ServiceBase(msg: "", code: 0, data: e.data); + } else { + serviceData = ServiceBase.fromJson(e.data); + } + if (serviceData.code == 401 && + ObjectUtil.isNotEmpty(serviceData.msg)) { + if (!ignoreMsg.contains(serviceData.msg)) { + Toast.error(msg: serviceData.msg ?? "服务器未知错误"); + } + final isLogin = await _autoLogin(); + if (!isLogin) { + Get.offAndToNamed(RouteNames.login); + } + } else if (serviceData.code != 0) { + Toast.error(msg: serviceData.msg ?? "服务器未知错误"); + } + // 续一下token + if (!e.realUri.path.contains("refresh-token")) { + // _refreshToken(); + } + e.data = serviceData.data; + handler.next(e); + }, + onError: (e, handler) async { + final response = e.response; + final statusCode = response?.statusCode; + if (statusCode != null) { + if (statusCode == 424) { + final isLogin = await _autoLogin(); + if (!isLogin) { + Get.offAndToNamed(RouteNames.login); + } + handler.next(e); + } + } + if (response != null && response.data != null) { + ServiceBase data = ServiceBase.fromJson(response.data); + if (data.code != 0) { + Toast.error(msg: data.msg ?? "服务器未知错误"); + handler.next(e); + return; + } + } + return handler.next(e); + }, + ), + ); + + dio.httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () { + final HttpClient client = + HttpClient(context: SecurityContext(withTrustedRoots: false)); + client.badCertificateCallback = (cert, host, port) => true; + return client; + }, + ); + } + + Future _autoLogin() async { + final userSetting = await AppSettingTool.getUserSetting(); + final sysSetting = await AppSettingTool.getSysSetting(); + if (ObjectUtil.isEmpty(userSetting.username) || + ObjectUtil.isEmpty(userSetting.password) || + ObjectUtil.isEmpty(userSetting.accessToken)) { + return false; + } + final pa = { + 'username': userSetting.username, + 'password': userSetting.password, + 'sysType': sysSetting.sysType.name, + 'grant_type': 'password', + 'scope': 'server', + }; + try { + final data = await UserApi.login(pa); + if (data != null) throw Error(); + userSetting.userId = data['user_id']; + userSetting.accessToken = data['access_token']; + userSetting.refreshToken = data['refresh_token']; + userSetting.expiresTime = int.tryParse("${data['exp']}"); + await AppSettingTool.setUserSetting(userSetting); + return true; + } catch (e) { + await AppSettingTool.setUserSetting(UserSetting()); + logger.e("|$_tag|自动登录意外的错误error:|$e|"); + } + return false; + } + + Future _refreshToken() async { + final userSetting = await AppSettingTool.getUserSetting(); + if (ObjectUtil.isEmpty(userSetting.refreshToken) || + ObjectUtil.isEmpty(userSetting.expiresTime)) { + return; + } + final now = DateTime.now().millisecondsSinceEpoch; + final remain = userSetting.expiresTime! - now; + logger.i("|$_tag|token还有${remain / 1000 / 60}分钟${remain / 1000}秒过期"); + if (remain > 1000 * 60 * 20) return; + final pa = {"refreshToken": userSetting.refreshToken}; + try { + final res = await UserApi.refreshToken(pa); + if (res.code != 0) throw Error(); + final data = res.data as ServiceAuth; + if (ObjectUtil.isNotEmpty(data.userId)) { + userSetting.userId = data.userId; + } + if (ObjectUtil.isNotEmpty(data.accessToken)) { + userSetting.accessToken = data.accessToken; + } + if (ObjectUtil.isNotEmpty(data.refreshToken)) { + userSetting.refreshToken = data.refreshToken; + } + if (ObjectUtil.isNotEmpty(data.expiresTime)) { + userSetting.expiresTime = data.expiresTime; + } + await AppSettingTool.setUserSetting(userSetting); + } catch (e) { + logger.e("|$_tag|续存token意外的错误error:|$e|"); + } + } +} diff --git a/lib/common/utils/manage/mq_manager.dart b/lib/common/utils/manage/mq_manager.dart new file mode 100644 index 0000000..bb4eb52 --- /dev/null +++ b/lib/common/utils/manage/mq_manager.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/services.dart'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +/// mq +class MqManager { + static const String _tag = "mq管理"; + + static MqManager? _instance; + + // mq对象 + static MqttServerClient? mqttClient; + static MqttConnectionState state = MqttConnectionState.disconnected; + + // 当前设备的 topic + static late String mainTopic; + + MqManager._internal(); + + factory MqManager() { + _instance ??= MqManager._internal(); + return _instance!; + } + + static Future validate() async { + final config = await AppSettingTool.getSysSetting(); + logger.i("|$_tag||刷新mq|校验配置参数:config:|${json.encode(config)}|"); + if (ObjectUtil.isEmpty(config)) return null; + if (ObjectUtil.isEmpty(config.mqUsername)) return null; + if (ObjectUtil.isEmpty(config.mqPassword)) return null; + if (ObjectUtil.isEmpty(config.mqClientId)) return null; + if (ObjectUtil.isEmpty(config.mqSocketAddress)) return null; + if (ObjectUtil.isEmpty(config.mqSocketPort)) return null; + if (ObjectUtil.isEmpty(config.mainTopic)) return null; + return config; + } + + static Future closeMqtt() async { + if (MqManager.mqttClient != null) { + MqManager.mqttClient?.disconnect(); + MqManager.mqttClient = null; + } + MqManager.state = MqttConnectionState.disconnected; + } + + static Future initMqtt() async { + // 获取配置 + final config = await validate(); + if (ObjectUtil.isEmpty(config!)) return; + final port = int.tryParse(config.mqSocketPort!); + if (ObjectUtil.isEmpty(port)) return; + final client = MqttServerClient.withPort( + config.mqSocketAddress!, + config.mqClientId!, + port!, + ); + mqttClient = client; + + if (port == 8883) { + final context = SecurityContext.defaultContext; + final byte = await rootBundle.load(AssetsCrt.emqxCa); + context.setClientAuthoritiesBytes(byte.buffer.asUint8List()); + client.securityContext = context; + client.secure = true; + } + + client.onBadCertificate = (Object a) => true; + client.autoReconnect = false; + client.logging(on: false); + client.keepAlivePeriod = 60; + client.onConnected = _onConnected; + client.onDisconnected = _onDisconnected; + client.onSubscribed = _onSubscribed; + client.pongCallback = _pongCallback; + client.onAutoReconnect = _onAutoReconnect; + + final connMessage = + MqttConnectMessage().withProtocolName("MQTT").withProtocolVersion(4); + client.connectionMessage = connMessage; + + try { + final state = await client.connect(config.mqUsername, config.mqPassword); + if (ObjectUtil.isNotEmpty(state)) { + MqManager.state = state!.state; + if (ObjectUtil.isNotEmpty(MqManager.state)) { + if (MqManager.state == MqttConnectionState.connected) {} + } + } + } catch (e) { + logger.e("|$_tag|mqtt连接失败error:|$e|"); + } + } + + static Future refresh() async { + final config = await validate(); + if (ObjectUtil.isEmpty(config)) { + logger.e("|$_tag||刷新mq|错误mq参数校验不通过"); + return; + } + if (ObjectUtil.isEmpty(mqttClient)) { + await initMqtt(); + } else if (MqManager.state != MqttConnectionState.connected) { + final res = + await mqttClient!.connect(config!.mqUsername, config.mqPassword); + if (res != null && res.state != MqManager.state) { + MqManager.state = res.state; + } + } else { + logger.e("|$_tag||刷新mq|错误mq已连接"); + } + } + + static Future subscribeCurrentTopic() async { + if (ObjectUtil.isEmpty(mqttClient)) return; + final config = await validate(); + if (ObjectUtil.isEmpty(config)) return; + if (ObjectUtil.isEmpty(config?.mainTopic)) return; + mqttClient!.subscribe(config!.mainTopic!, MqttQos.atMostOnce); + } + + static void _onConnected() async { + logger.w("|$_tag|mqtt连接成功"); + final config = await AppSettingTool.getSysSetting(); + if (ObjectUtil.isNotEmpty(config)) { + config.mqSyncTime = DateTime.now(); + await AppSettingTool.setSysSetting(config); + } + await subscribeCurrentTopic(); + } + + static void _onDisconnected() async { + logger.w("|$_tag|mqtt断开链接"); + AppMqTool.mqListenClose(); + state = MqttConnectionState.disconnected; + } + + static void _onSubscribed(String topic) { + logger.w('|$_tag|mqtt 订阅主题成功:$topic'); + mainTopic = topic; + } + + static void _pongCallback() { + logger.w("|$_tag|mqtt ping"); + } + + // 自动重连接 + static void _onAutoReconnect() async { + logger.i("|$_tag|启动自动重连"); + await refresh(); + } +} diff --git a/lib/common/utils/manage/print_manage.dart b/lib/common/utils/manage/print_manage.dart new file mode 100644 index 0000000..5ec295b --- /dev/null +++ b/lib/common/utils/manage/print_manage.dart @@ -0,0 +1,72 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:drago_pos_printer/drago_pos_printer.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class PrintManage extends BaseFacilityManage { + USBPrinterManager? manager; + CapabilityProfile? profile; + int paperSizeWidthMM = PaperSizeWidth.mm80; + int maxPerLine = PaperSizeMaxPerLine.mm80; + + @override + String setName() => UsbFacilityNames.print.title; + + @override + Future open() async { + final check = await super.open(); + if (!check) return false; + final usbPrinter = await USBPrinterManager.discover(); + final sysSetting = await AppSettingTool.getSysSetting(); + final sysType = sysSetting.sysType; + final vendorId = sysType == SysType.canteen ? 19267 : 4070; + final target = usbPrinter.firstWhereOrNull((v) => v.vendorId == vendorId); + if (ObjectUtil.isEmpty(target)) return false; + profile = await CapabilityProfile.load(); + manager = USBPrinterManager(target!); + final res = await manager!.connect(); + return res.value == 1; + } + + Future printImg(String img) async { + if (ObjectUtil.isEmpty(manager)) return false; + // 图片 + final image = await AppNetworkTool.loadImageFromNetwork(img); + if (ObjectUtil.isEmpty(image)) { + Toast.error(msg: "找不到发票图片"); + return false; + } + EscGenerator generator = EscGenerator( + paperSizeWidthMM, + maxPerLine, + profile!, + ); + List bytes = []; + bytes += generator.image(image!); + bytes += generator.feed(1); + bytes += generator.cut(); + final res = await manager!.writeBytes(bytes); + return res.value == 1; + } + + @override + Future close() async { + if (ObjectUtil.isNotEmpty(manager)) { + if (manager != null) { + manager?.disconnect(); + } + manager = null; + } + } + + @override + Future checkPermission() async { + return true; + } + + @override + Future applyPermission() async { + return true; + } +} diff --git a/lib/common/utils/manage/scan_code_manage.dart b/lib/common/utils/manage/scan_code_manage.dart new file mode 100644 index 0000000..b36501f --- /dev/null +++ b/lib/common/utils/manage/scan_code_manage.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:common_utils/common_utils.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +class ScanCodeManage extends BaseFacilityManage { + bool isOpen = false; + String codeData = ""; + StreamSubscription? listenFc; + + @override + String setName() => UsbFacilityNames.scanCode.title; + + @override + Future open() async { + final check = await super.open(); + if (!check) return false; + isOpen = true; + return true; + } + + @override + void listen(void Function(T event)? fc) { + if (!isOpen) return; + if (ObjectUtil.isNotEmpty(listenFc)) { + listenFc?.cancel(); + listenFc = null; + } + // listenFc = FlutterAccessibilityService.accessStream.listen((event) { + // if (ObjectUtil.isNotEmpty(event.scanResult)) { + // final scanCode = event.scanResult; + // logger.i("二维码读头:|$scanCode|"); + // if (isOpen) { + // codeData = scanCode; + // if (ObjectUtil.isNotEmpty(fc!)) { + // fc(codeData as T); + // } + // } + // } + // }); + } + + @override + Future close() async { + if (isOpen) { + isOpen = false; + } + if (ObjectUtil.isNotEmpty(codeData)) { + codeData = ""; + } + if (ObjectUtil.isNotEmpty(listenFc)) { + listenFc!.cancel(); + listenFc = null; + } + } + + @override + Future checkPermission() async { + return true; + } + + @override + Future applyPermission() async { + return true; + } +} diff --git a/lib/common/utils/manage/serial_card_manage.dart b/lib/common/utils/manage/serial_card_manage.dart new file mode 100644 index 0000000..dabee9a --- /dev/null +++ b/lib/common/utils/manage/serial_card_manage.dart @@ -0,0 +1,158 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:common_utils/common_utils.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SerialCardManage extends SerialBase { + SerialCardCmdEnum? currentCmd; + Timer? errorTimer; + Timer? mqTimer; + DateTime sendDate = DateTime.now(); + + Function(SerialCardCmdEnum, SerialCardResStatus, dynamic)? callback; + + SerialCardManage() { + name = "读卡器"; + portName = "/dev/ttyS4"; + baudRate = 115200; + } + + @override + void readerClose() { + super.readerClose(); + if (ObjectUtil.isNotEmpty(errorTimer)) { + errorTimer!.cancel(); + errorTimer = null; + } + if (ObjectUtil.isNotEmpty(mqTimer)) { + mqTimer!.cancel(); + mqTimer = null; + } + } + + @override + void close() { + super.close(); + readerClose(); + if (ObjectUtil.isNotEmpty(errorTimer)) { + errorTimer!.cancel(); + errorTimer = null; + } + if (ObjectUtil.isNotEmpty(mqTimer)) { + mqTimer!.cancel(); + mqTimer = null; + } + } + + /// 回调时间5秒内接收不到消息就报错 + void callbackTimer() { + if (ObjectUtil.isNotEmpty(errorTimer)) return; + sendDate = DateTime.now(); + errorTimer = Timer(const Duration(seconds: 30), () { + callback!.call(currentCmd!, SerialCardResStatus.error, null); + readerClose(); + close(); + }); + } + + /// 发送写入命令 + void sendWrite(String cardNumber) { + final hexPadded = SerialTool.toHexPadded(str: cardNumber); + if (ObjectUtil.isEmpty(hexPadded)) { + return; + } + final writeHexReq = SerialReadCardTool.writeHexReq(hexPadded!); + final bytes = SerialTool.stringToBytes(writeHexReq); + + currentCmd = SerialCardCmdEnum.write; + callbackTimer(); + _listenEvent(); + + mqTimer = Timer.periodic(const Duration(seconds: 2), (Timer t) { + final write = serialClient.write(Uint8List.fromList(bytes)); + if (write == bytes.length) { + logger.d(logMsg("发送成功|写入命令|string|$writeHexReq|")); + } + }); + } + + /// 发送读卡命令 + void sendRead() { + final stringHex = SerialReadCardTool.readHexReq(); + final bytes = SerialTool.stringToBytes(stringHex); + if (ObjectUtil.isNotEmpty(mqTimer)) return; + + currentCmd = SerialCardCmdEnum.read; + callbackTimer(); + _listenEvent(); + + mqTimer = Timer.periodic(const Duration(seconds: 2), (Timer t) { + final write = serialClient.write(Uint8List.fromList(bytes)); + if (write == bytes.length) { + logger.d(logMsg("发送成功|读卡命令|string|$stringHex|")); + } + }); + } + + /// 监听mq返回 + void _listenEvent() { + if (ObjectUtil.isEmpty(errorTimer)) return; + if (ObjectUtil.isEmpty(currentCmd)) return; + listen((List event) { + if (ObjectUtil.isEmpty(event)) return; + switch (currentCmd!) { + case SerialCardCmdEnum.write: + dispatchWrite(event); + break; + case SerialCardCmdEnum.read: + _dispatchRead(event); + break; + } + }); + } + + /// 处理写入 + void dispatchWrite(List event) { + final res = SerialTool.bytesToString(event); + if (ObjectUtil.isEmpty(res)) return; + if (res == SerialReadCardTool.writeHexResSuccess()) { + logger.d(logMsg("写入成功")); + if (ObjectUtil.isNotEmpty(callback)) { + callback!.call(currentCmd!, SerialCardResStatus.success, null); + } + readerClose(); + close(); + } + if (res == SerialReadCardTool.writeHexResError()) { + logger.d(logMsg("写入失败,未寻到卡")); + if (ObjectUtil.isNotEmpty(callback)) { + callback!.call(currentCmd!, SerialCardResStatus.warning, null); + } + } + } + + /// 处理读 + void _dispatchRead(List event) { + final res = SerialTool.bytesToString(event); + if (ObjectUtil.isEmpty(res)) return; + final readHexRes = SerialReadCardTool.readHexResSuccess(); + if (res.startsWith(readHexRes)) { + event = event.sublist(4); + String number = event.map((num) => String.fromCharCode(num)).join(); + number = number.length >= 12 ? number.substring(0, 12) : res; + logger.d(logMsg("读取成功,event:|$event|string:|$res|卡号为|$number|")); + if (ObjectUtil.isNotEmpty(callback)) { + callback!.call(currentCmd!, SerialCardResStatus.success, number); + } + readerClose(); + close(); + } + if (res == SerialReadCardTool.readHexResError()) { + logger.d(logMsg("读取失败,未寻到卡")); + if (ObjectUtil.isNotEmpty(callback)) { + callback!.call(currentCmd!, SerialCardResStatus.warning, null); + } + } + } +} diff --git a/lib/common/utils/manage/serial_steelyard_manage.dart b/lib/common/utils/manage/serial_steelyard_manage.dart new file mode 100644 index 0000000..8c881bc --- /dev/null +++ b/lib/common/utils/manage/serial_steelyard_manage.dart @@ -0,0 +1,29 @@ +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SerialSteelyardManage extends SerialBase { + SerialSteelyardManage() { + name = "称"; + portName = "/dev/ttyS2"; + baudRate = 9600; + readTimeout = 3000; + } + + double? parseWidget(List bytes) { + String result = String.fromCharCodes(bytes); + final RegExp regExp = RegExp(r'(\d+(\.\d+)?)'); + Iterable matches = regExp.allMatches(result); + final str = matches.map((match) => match.group(0)).join(' '); + return double.tryParse(str); + } + + @override + void readerClose() { + super.readerClose(); + } + + @override + void close() { + readerClose(); + super.close(); + } +} diff --git a/lib/common/utils/manage/tool/serial_read_card_tool.dart b/lib/common/utils/manage/tool/serial_read_card_tool.dart new file mode 100644 index 0000000..0914e18 --- /dev/null +++ b/lib/common/utils/manage/tool/serial_read_card_tool.dart @@ -0,0 +1,31 @@ +abstract class SerialReadCardTool { + /// 写入命令-发送 + static String writeHexReq(String str) { + return "AA120501$str"; + } + + /// 写入命令-接收-成功 + static String writeHexResSuccess() { + return "AA01FE"; + } + + /// 写入命令-接收-失败-未寻到卡 + static String writeHexResError() { + return "AA01E1"; + } + + /// 读命令-发送 + static String readHexReq() { + return "AA020401"; + } + + /// 读命令-接收-成功 + static String readHexResSuccess() { + return "AA120401"; + } + + /// 读命令-接收-失败-未寻到卡 + static String readHexResError() { + return "AA01E1"; + } +} diff --git a/lib/common/utils/manage/tool/serial_tool.dart b/lib/common/utils/manage/tool/serial_tool.dart new file mode 100644 index 0000000..4e6ea71 --- /dev/null +++ b/lib/common/utils/manage/tool/serial_tool.dart @@ -0,0 +1,32 @@ +abstract class SerialTool { + /// 转16进制转字符串 + static String bytesToString(List intList) { + return intList + .map((intVal) => intVal.toRadixString(16).toUpperCase().padLeft(2, '0')) + .join(); + } + + /// 转16进制转字符串 + static List stringToBytes(String strCode) { + List bytes = []; + for (int i = 0; i < strCode.length; i += 2) { + String hexPair = strCode.substring(i, i + 2); + int byteValue = int.parse(hexPair, radix: 16); + bytes.add(byteValue); + } + return bytes; + } + + /// 字符串转16进制不够补零 + static String? toHexPadded({ + required String str, + int max = 32, + }) { + String hex = + str.codeUnits.map((c) => c.toRadixString(16).padLeft(2, '0')).join(); + if (hex.length > max) { + return null; + } + return hex.padRight(max, '0'); + } +} diff --git a/lib/common/utils/plugs/face_discern_plugs.dart b/lib/common/utils/plugs/face_discern_plugs.dart new file mode 100644 index 0000000..352cd9e --- /dev/null +++ b/lib/common/utils/plugs/face_discern_plugs.dart @@ -0,0 +1,128 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class FaceDiscernPlugs { + static const String _tag = "人脸识别plug"; + + static const MethodChannel _channel = MethodChannel( + "local/face_discern", + ); + static const EventChannel _eventChannel = EventChannel( + "local/face_discern_event", + ); + + static MethodChannel get getChannel => _channel; + + static Stream? _stream; + + static Future jumpFaceDiscernPage() async { + try { + await _channel.invokeMethod('jumpFaceDiscern'); + return true; + } catch (e) { + logger.w("|$_tag| jumpFaceDiscernPage 错误: $e"); + } + return false; + } + + static Future getConfig() async { + try { + final Map? map = + await _channel.invokeMethod('getConfig'); + if (map == null) return null; + final encode = json.encode(map); + return FaceConfig.fromJson(json.decode(encode)); + } catch (e) { + logger.w("|$_tag| getConfig 错误: $e"); + } + return null; + } + + static Future active({ + required String appId, + required String sdkKey, + required String activeKey, + }) async { + try { + final pa = { + "appId": appId, + "sdkKey": sdkKey, + "activeKey": activeKey, + }; + final isActive = await _channel.invokeMethod('active', pa); + return isActive; + } catch (e) { + logger.w("|$_tag| getConfig 错误: $e"); + } + return false; + } + + static Future registerAllStorageFace({ + required String filePath, + String? fileName, + }) async { + final pa = { + "filePath": filePath, + "fileName": fileName, + }; + final map = + await _channel.invokeMethod('registerAllStorageFace', pa); + if (map == null) return null; + final encode = json.encode(map); + return RegisterAllStorageFaceRes.fromJson(json.decode(encode)); + } + + static Future getFace({ + required int sampleId, + }) async { + final pa = { + "sampleId": sampleId, + }; + final map = await _channel.invokeMethod('getFace', pa); + if (map == null) return null; + final encode = json.encode(map); + return FaceRes.fromJson(json.decode(encode)); + } + + static Future getFaceCount() async { + final pa = {}; + final res = await _channel.invokeMethod('getFaceCount', pa); + if (res == null) return 0; + return res; + } + + static Future removeFace({ + required String sampleId, + }) async { + final sampleIdInt = int.tryParse(sampleId); + final pa = { + "sampleId": sampleIdInt, + }; + final res = await _channel.invokeMethod('removeFace', pa); + if (res == null) return false; + return res; + } + + static Future> registerFaceByDatabase({ + String? userId, + }) async { + final pa = { + "userId": userId, + }; + final res = await _channel.invokeMethod('registerFaceByDatabase', pa); + final errorSampleIdList = (res as List).cast(); + return errorSampleIdList; + } + + static Stream get faceStream { + _stream ??= _eventChannel.receiveBroadcastStream().map((event) { + if (event == null) return null; + if (event == 'null') return null; + return FaceStream.fromJson(jsonDecode(event)); + }); + return _stream!; + } +} diff --git a/lib/common/utils/plugs/local_tts_plugs.dart b/lib/common/utils/plugs/local_tts_plugs.dart new file mode 100644 index 0000000..0df4765 --- /dev/null +++ b/lib/common/utils/plugs/local_tts_plugs.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class LocalTtsPlugs { + static const String _tag = "本地tts插件"; + + static const MethodChannel _channel = MethodChannel( + Constants.localTTSPlugsMethodChannel, + ); + + static Future speeching({ + required String appId, + required String str, + String speed = "80", + }) async { + try { + final pa = { + "appId": appId, + "str": str, + "speed": speed, + }; + await _channel.invokeMethod('speeching', pa); + return true; + } catch (e) { + logger.w("|$_tag| jumpFaceDiscernPage 错误: $e"); + } + return false; + } +} diff --git a/lib/common/utils/router/router_observer_tool.dart b/lib/common/utils/router/router_observer_tool.dart new file mode 100644 index 0000000..bf00126 --- /dev/null +++ b/lib/common/utils/router/router_observer_tool.dart @@ -0,0 +1,57 @@ +import 'package:event_bus/event_bus.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +enum RouterActionType { didPush, didReplace, didPop, didRemove } + +class GlobalRouter { + final String? name; + final Route? router; + final RouterActionType action; + + GlobalRouter({ + this.name, + this.router, + required this.action, + }); +} + +class GlobalRouteObserver extends GetObserver { + static EventBus globalRouteObserverBus = EventBus(); + + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + globalRouteObserverBus.fire( + GlobalRouter( + name: route.settings.name, + router: route, + action: RouterActionType.didPush, + ), + ); + } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + globalRouteObserverBus.fire( + GlobalRouter( + name: route.settings.name, + router: route, + action: RouterActionType.didPop, + ), + ); + } + + @override + void didRemove(Route route, Route? previousRoute) { + super.didRemove(route, previousRoute); + globalRouteObserverBus.fire( + GlobalRouter( + name: route.settings.name, + router: route, + action: RouterActionType.didRemove, + ), + ); + } +} diff --git a/lib/common/utils/sync/sync_config_tool.dart b/lib/common/utils/sync/sync_config_tool.dart new file mode 100644 index 0000000..60322c6 --- /dev/null +++ b/lib/common/utils/sync/sync_config_tool.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:common_utils/common_utils.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class SyncConfigTool { + static const _tag = "sync配置工具"; + + // 自动同步设备信息导本地缓存 + static Future autoSyncSysInfo() async { + final login = await AppUserTool.isLogin(); + if (!login) return; + final sysSetting = await AppSettingTool.getSysSetting(); + logger.i("|$_tag|开始同步设备信息 sysInfo:|${json.encode(sysSetting)}|"); + if (ObjectUtil.isEmpty(sysSetting)) return; + if (ObjectUtil.isEmpty(sysSetting.serviceId)) return; + final serviceId = sysSetting.serviceId; + final pa = {"id": serviceId}; + final res = await HardApi.get(pa); + if (res.code != 0) { + logger.e("|$_tag|同步设备信息错误serviceId:|$serviceId|;res:$res;"); + return; + } + final data = res.data as ServiceHardware; + // 基本信息 + await AppSettingTool.updateSysInfoByService(data); + // 人脸sdk + await autoSyncFaceSdk(detail: data, sysInfo: sysSetting); + logger.i("|$_tag|结束同步设备信息 sysInfo:|${json.encode(sysSetting)}|"); + } + + // 自动同步设备信息导本地缓存-同步人脸sdk + static Future autoSyncFaceSdk({ + required ServiceHardware detail, + required SysSetting sysInfo, + }) async { + final appId = detail.faceAppId; + final sdkKey = detail.faceSdkKey; + final activeKey = detail.faceActiveKey; + bool isUpdate = false; + if (appId != sysInfo.faceAppId) { + sysInfo.faceAppId = appId; + isUpdate = true; + } + if (sdkKey != sysInfo.faceSdkKey) { + sysInfo.faceSdkKey = sdkKey; + isUpdate = true; + } + if (activeKey != sysInfo.faceActiveKey) { + sysInfo.faceActiveKey = activeKey; + isUpdate = true; + } + if (!isUpdate) return; + final activeSdk = await AppFaceTool.isActiveSdk(); + await AppSettingTool.setSysSetting(sysInfo); + logger.i("|$_tag||同步人脸sdk参数自动激活${activeSdk ? '成功' : '失败'}"); + } +} diff --git a/lib/common/utils/sync/sync_face_tool.dart b/lib/common/utils/sync/sync_face_tool.dart new file mode 100644 index 0000000..5901c24 --- /dev/null +++ b/lib/common/utils/sync/sync_face_tool.dart @@ -0,0 +1,193 @@ +import 'dart:io'; + +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SyncFaceTool { + static const _tag = "sync人脸工具"; + + static Future syncGroupFace({ + required int pageNo, + required String merchantDeptId, + required String storagePath, + String? userId, + bool? isGlobalFace, + }) async { + final app = Get.find(); + final faceDao = app.dataBase.getFaceDao; + const size = 20; + var pa = { + "current": pageNo, + "size": size, + "userId": userId, + "merchantDeptId": merchantDeptId, + }; + if (isGlobalFace != null && isGlobalFace) pa["merchantDeptId"] = null; + logger.i("|$_tag|同步人脸api参数:|$pa|"); + try { + final res = await UserApi.getFaceList(pa); + if (res.code != 0 || res.data == null) throw Error(); + final data = res.data as List; + if (ObjectUtil.isEmptyList(data)) { + if (ObjectUtil.isEmpty(userId)) return false; + // 删除人脸图片 + final faceList = await faceDao.findByUserId(userId!); + if (!ObjectUtil.isEmptyList(faceList)) { + for (final v in faceList!) { + if (ObjectUtil.isNotEmpty(v.path)) { + await delFaceByStorage(filePath: v.path!); + await AppFaceTool.removeFace(sampleId: v.sampleId); + } + } + } + // 删除数据库 + await faceDao.delByUserId(userId); + return false; + } + if (isGlobalFace != null && !isGlobalFace) return false; + if (userId != null) { + final dbUserList = await faceDao.findByUserId(userId); + if (!ObjectUtil.isEmptyList(dbUserList)) { + for (final v in dbUserList!) { + final target = data.firstWhereOrNull((i) => i.id == v.sampleId); + if (target == null) { + if (ObjectUtil.isNotEmpty(v.path)) { + await delFaceByStorage(filePath: v.path!); + await AppFaceTool.removeFace(sampleId: v.sampleId); + } + await faceDao.delById(v.id!); + } + } + } + } + for (final v in data) { + final faceEntity = await faceDao.findBySampleId(v.autoCode!); + if (faceEntity == null) { + final add = FaceEntity( + userId: v.userId!, + sampleId: v.autoCode!, + userName: v.username ?? "", + deptId: v.deptId!, + deptName: v.deptName ?? "", + imagePath: v.faceImg ?? "", + createTime: DateTime.now(), + ); + final addId = await faceDao.save(add); + if (addId != null) { + String? fileName = generateFaceFileName( + userId: add.userId, + sampleId: add.sampleId, + username: add.userName, + facePath: add.imagePath, + ); + final filePath = await writeFaceByStorage( + storagePath: storagePath, + fileName: fileName, + facePath: add.imagePath, + ); + if (ObjectUtil.isNotEmpty(filePath)) { + add.id = addId; + add.updateTime = DateTime.now(); + add.path = filePath; + await faceDao.edit(add); + } + // 重新生成人脸模型 + await AppFaceTool.removeFace(sampleId: add.sampleId); + await AppFaceTool.faceRegister( + userId: add.userId.toString(), + forceRegister: true, + ); + } + continue; + } + final edit = FaceEntity( + id: faceEntity.id!, + userId: v.userId!, + sampleId: v.autoCode!, + userName: v.username ?? "", + deptId: v.deptId!, + deptName: v.deptName ?? "", + imagePath: v.faceImg ?? "", + createTime: DateTime.now(), + ); + if (edit == faceEntity) { + continue; + } + String? fileName = generateFaceFileName( + userId: edit.userId, + sampleId: edit.sampleId, + username: edit.userName, + facePath: edit.imagePath, + ); + final filePath = await writeFaceByStorage( + storagePath: storagePath, + facePath: edit.imagePath, + fileName: fileName, + ); + if (ObjectUtil.isNotEmpty(filePath)) { + if (ObjectUtil.isNotEmpty(edit.path)) { + await delFaceByStorage(filePath: edit.path!); + } + edit.path = filePath; + } + await faceDao.edit(edit); + // 重新生成人脸模型 + await AppFaceTool.removeFace(sampleId: edit.sampleId); + await AppFaceTool.faceRegister( + userId: edit.userId.toString(), + forceRegister: true, + ); + } + if (data.length >= size) return true; + } catch (e, stack) { + logger.e("|$_tag|同步人脸意外的错误error:|$e|stack:|$stack|"); + } + return false; + } + + /// 删除人脸图片 + static Future delFaceByStorage({ + required String filePath, + }) async { + try { + final file = File(filePath); + if (await file.exists()) { + await file.delete(); + } + } catch (e) { + logger.e("|$_tag|删除人脸图片意外的错误error:|$e|"); + } + } + + static String? generateFaceFileName({ + required String userId, + required String sampleId, + required String username, + required String facePath, + }) { + if (ObjectUtil.isEmpty(facePath)) return null; + return "${userId}_${sampleId}_$username$facePath"; + } + + /// 写入人脸头像到本地磁盘 + static Future writeFaceByStorage({ + required String storagePath, + required String? fileName, + required String facePath, + }) async { + if (ObjectUtil.isEmpty(fileName)) return null; + final storageDirectory = Directory(storagePath); + final file = File('${storageDirectory.path}/$fileName'); + if (await file.exists()) { + await file.delete(); + } + final bytesImg = await AppNetworkTool.downloadImage(facePath); + if (bytesImg == null) return null; + final writeFile = await file.writeAsBytes(bytesImg); + if (await writeFile.exists()) { + return file.path; + } + return null; + } +} diff --git a/lib/common/utils/sync/sync_order_tool.dart b/lib/common/utils/sync/sync_order_tool.dart new file mode 100644 index 0000000..d826c3c --- /dev/null +++ b/lib/common/utils/sync/sync_order_tool.dart @@ -0,0 +1,95 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class SyncOrderTool { + static const _tag = "sync订单工具"; + + // 同步离线订单 + static Future syncOfflineOrder() async { + final app = Get.find(); + final sysSetting = await AppSettingTool.getSysSetting(); + final sysType = sysSetting.sysType.name; + + final orderDao = app.dataBase.getOrderDao; + + // 查询离线订单 + final startDate = DateTime.now().subtract(const Duration(days: 30)); + final endDate = DateTime.now().add(const Duration(days: 1)); + final list = await orderDao.findOffline( + sysType, + startDate, + endDate, + ); + if (ObjectUtil.isEmptyList(list)) { + logger.i("|$_tag|错误,没有需要同步的离线订单"); + return; + } + final chunkList = chunkArray(list, 5); + for (var v in chunkList) { + await _syncOfflineGroupOrder(v); + } + // 更新同步时间 + sysSetting.offlineOrderSyncTime = DateTime.now(); + await AppSettingTool.setSysSetting(sysSetting); + } + + // 同步一组离线订单 + static Future _syncOfflineGroupOrder(List list) async { + final app = Get.find(); + + final sysSetting = await AppSettingTool.getSysSetting(); + final serviceId = sysSetting.serviceId; + final serviceMerchantId = sysSetting.serviceMerchantId; + + final shopCardDao = app.dataBase.getShopCardDao; + List> uploadList = []; + for (final OrderEntity v in list) { + Map item = v.toJson(); + if (v.id != null) { + final carList = await shopCardDao.findByOrderId(v.id!); + if (!ObjectUtil.isEmptyList(carList)) { + item["carList"] = carList.map((i) => i.toJson()).toList(); + } + } + uploadList.add(item); + } + final pa = { + "list": uploadList, + "hardwareId": serviceId, + "merchantId": serviceMerchantId!, + }; + logger.i("|$_tag|同步参数:|$pa|"); + try { + final res = await HardApi.syncOfflineOrder(pa); + if (res.code != 0 || res.data == null) throw Error(); + final data = res.data as List; + final batchEditCount = await handEditOfflineStatus(data); + logger.i("|$_tag|同步成功的数量为:|$batchEditCount|ids为:|$data|"); + } catch (e) { + logger.e("|$_tag|同步一组离线订单意外的错误error:$e"); + } + } + + static Future handEditOfflineStatus(List codes) async { + final app = Get.find(); + final placeholders = List.generate(codes.length, (index) => '?').join(', '); + final sql = ''' + UPDATE order_record SET status = ? WHERE uniqueCode IN ($placeholders) + '''; + final database = app.dataBase.database; + return await database.rawUpdate(sql, [OrderStatus.pay.name, ...codes]); + } + + // 切割数组 + static List> chunkArray(List array, int chunkSize) { + if (chunkSize <= 0) return []; + List> chunks = []; + for (var i = 0; i < array.length; i += chunkSize) { + int end = (i + chunkSize < array.length) ? i + chunkSize : array.length; + chunks.add(array.sublist(i, end)); + } + return chunks; + } +} diff --git a/lib/common/utils/sync/sync_print_tool.dart b/lib/common/utils/sync/sync_print_tool.dart new file mode 100644 index 0000000..e8dfeff --- /dev/null +++ b/lib/common/utils/sync/sync_print_tool.dart @@ -0,0 +1,94 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class SyncPrintTool { + static const _tag = "sync打印工具"; + + static bool _isPrint = false; + + static Future addTask({ + required String orderId, + required String androidOrderId, + required String historyId, + required String pic, + }) async { + final app = Get.find(); + final printHistoryDao = app.dataBase.getPrintHistoryDao; + final saveObj = PrintHistoryEntity( + orderId: androidOrderId, + serviceOrderId: orderId, + servicePrintId: historyId, + invoiceUrl: pic, + status: PrintStatus.none, + createTime: DateTime.now(), + ); + await printHistoryDao.save(saveObj); + logger.i("|$_tag|添加到打印记录到本地数据库,json:|${saveObj.toJson()}|"); + await SyncPrintTool.doCheckPrint(); + } + + // 检查打印 + static Future doCheckPrint() async { + if (_isPrint) { + logger.i("|$_tag|检查打印,正在打印中"); + return; + } + final app = Get.find(); + final printHistoryDao = app.dataBase.getPrintHistoryDao; + final printFacility = app.printManage; + try { + // 检查有没有需要打印的 + final list = await printHistoryDao.getAll(PrintStatus.none.name); + if (ObjectUtil.isEmptyList(list)) { + _isPrint = false; + logger.i("|$_tag|检查打印,没有需要打印的发票"); + return; + } + final isOpen = await printFacility.open(); + if (!isOpen) { + _isPrint = false; + logger.e("|$_tag|检查打印,打开打印机失败"); + return; + } + for (final v in list) { + await doPrint(v); + } + } catch (e) { + logger.e("|$_tag|检查打印,打印错误,error:|$e|"); + } finally { + _isPrint = false; + } + } + + // 执行打印 + static Future doPrint(PrintHistoryEntity v) async { + final app = Get.find(); + final printHistoryDao = app.dataBase.getPrintHistoryDao; + final printFacility = app.printManage; + try { + final isPrint = await printFacility.printImg(v.invoiceUrl); + if (!isPrint) { + v.status = PrintStatus.error; + await printHistoryDao.edit(v); + throw Error(); + } + // 更新本地数据库 + v.status = PrintStatus.success; + v.printTime = DateUtil.formatDate(DateTime.now()); + await printHistoryDao.edit(v); + // 同步到服务端 + final printPa = { + "orderId": v.serviceOrderId, + "historyId": v.servicePrintId, + }; + await HardApi.printInvoice(printPa); + v.status = PrintStatus.sync; + v.syncTime = DateTime.now(); + await printHistoryDao.edit(v); + } catch (e) { + logger.i("|$_tag|执行打印,打印错误,error:|$e|"); + } + } +} diff --git a/lib/common/utils/sync/sync_product_tool.dart b/lib/common/utils/sync/sync_product_tool.dart new file mode 100644 index 0000000..c554cad --- /dev/null +++ b/lib/common/utils/sync/sync_product_tool.dart @@ -0,0 +1,275 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class SyncProductTool { + static const _tag = "sync商品工具"; + + static Future updateDatabase() async { + final isNetwork = AppNetworkTool.isAutoNetwork(); + if (!isNetwork) return; + EasyLoading.show( + status: '初始化资源...', + maskType: EasyLoadingMaskType.black, + ); + await syncClassify(); + await syncProduct(); + EasyLoading.dismiss(); + } + + /// 同步商品分类 + static Future syncClassify() async { + final sysSetting = await AppSettingTool.getSysSetting(); + final sysType = sysSetting.sysType; + try { + List? apiList; + if (sysType == SysType.canteen) { + apiList = await ProductApi.cookbookClassify(null); + } else if (sysType == SysType.supermarket) { + apiList = await ProductApi.supermarketClassify(null); + } + if (apiList == null) return; + if (ObjectUtil.isEmptyList(apiList)) { + return; + } + await _batchUpdateClassify(list: apiList, sysType: sysSetting.sysType); + // 更新同步时间 + if (sysType == SysType.canteen) { + sysSetting.canteenClassifySyncTime = DateTime.now(); + } + if (sysType == SysType.supermarket) { + sysSetting.supermarketClassifySyncTime = DateTime.now(); + } + await AppSettingTool.setSysSetting(sysSetting); + } catch (e, stack) { + logger.e("|$_tag|同步|${sysType.title}|分类信息意外的错误error:|$e|,stack:|$stack|"); + } + } + + static Future _batchUpdateClassify({ + required List list, + required SysType sysType, + }) async { + final app = Get.find(); + final dataBase = app.dataBase; + final classifyDao = dataBase.getClassifyDao; + final classifyList = await classifyDao.getAll(sysType.name, "-1"); + if (ObjectUtil.isEmpty(classifyList)) { + await _batchAddOrUpdateClassify( + list: list, + sysType: sysType, + isUpdate: false, + ); + return; + } + final apiServiceCodes = list.map((e) => e.id.toString()).toSet(); + final dbServiceCodes = classifyList.map((e) => e.serviceUniqueCode).toSet(); + + final addCodes = apiServiceCodes.difference(dbServiceCodes).toList(); + final deleteCodes = dbServiceCodes.difference(apiServiceCodes).toList(); + final updateCodes = apiServiceCodes.intersection(dbServiceCodes).toList(); + + if (ObjectUtil.isNotEmpty(addCodes)) { + await _batchAddOrUpdateClassify( + list: _filterClassifyListByCodes(list, addCodes), + sysType: sysType, + isUpdate: false, + ); + } + if (ObjectUtil.isNotEmpty(deleteCodes)) { + await classifyDao.delByIds(deleteCodes); + } + if (ObjectUtil.isNotEmpty(updateCodes)) { + await _batchAddOrUpdateClassify( + list: _filterClassifyListByCodes(list, updateCodes), + sysType: sysType, + isUpdate: true, + ); + } + } + + static Future _batchAddOrUpdateClassify({ + required List list, + required SysType sysType, + required bool isUpdate, + }) async { + if (ObjectUtil.isEmpty(list)) return; + final app = Get.find(); + final dataBase = app.dataBase; + final classifyDao = dataBase.getClassifyDao; + List changeList = []; + for (final v in list) { + ClassifyEntity item = ClassifyEntity( + serviceUniqueCode: v.id.toString(), + sysType: sysType, + name: v.name!, + categoryType: v.categoryType ?? "-1", + sort: v.sort ?? 999, + createTime: DateTime.now(), + updateTime: DateTime.now(), + delFlag: false, + ); + if (isUpdate) { + final detail = await classifyDao.getByCode(item.serviceUniqueCode); + if (ObjectUtil.isEmpty(detail)) continue; + if (detail == item) continue; + item.id = detail?.id; + } + changeList.add(item); + } + if (ObjectUtil.isNotEmpty(changeList)) { + isUpdate + ? await classifyDao.batchEdit(changeList) + : await classifyDao.batchSave(changeList); + } + } + + static List _filterClassifyListByCodes( + List sourceList, + List addCodes, + ) { + return sourceList + .where((item) => addCodes.contains(item.id.toString())) + .toList(); + } + + /// 同步商品 + static Future syncProduct() async { + final sysSetting = await AppSettingTool.getSysSetting(); + final sysType = sysSetting.sysType; + try { + List? apiList; + if (sysType == SysType.canteen) { + final day = DateUtil.formatDate( + DateTime.now(), + format: DateFormats.y_mo_d, + ); + final pa = { + "day": day, + "hourType": null, + "categoryId": null, + "merchantId": sysSetting.serviceMerchantId, + }; + apiList = await ProductApi.canteenProduct(pa); + } else if (sysType == SysType.supermarket) { + apiList = await ProductApi.supermarketProduct(null); + } + if (apiList == null) return; + if (ObjectUtil.isEmptyList(apiList)) { + return; + } + // 更新数据 + await _batchUpdateProduct(list: apiList, sysType: sysSetting.sysType); + // 更新同步时间 + if (sysType == SysType.canteen) { + sysSetting.canteenProductSyncTime = DateTime.now(); + } + if (sysType == SysType.supermarket) { + sysSetting.supermarketProductSyncTime = DateTime.now(); + } + await AppSettingTool.setSysSetting(sysSetting); + } catch (e, stack) { + logger.e("|$_tag|同步|${sysType.title}|商品信息意外的错误error:|$e|,stack:|$stack|"); + } + } + + static Future _batchUpdateProduct({ + required List list, + required SysType sysType, + }) async { + final app = Get.find(); + final dataBase = app.dataBase; + final productDao = dataBase.getProductDao; + final productList = await productDao.getAll( + "-1", + "-1", + "-1", + ); + if (ObjectUtil.isEmpty(productList)) { + await _batchAddOrUpdateProduct( + list: list, + sysType: sysType, + isUpdate: false, + ); + return; + } + final apiServiceCodes = list.map((e) => e.spuId.toString()).toSet(); + final dbServiceCodes = productList.map((e) => e.serviceUniqueCode).toSet(); + + final addCodes = apiServiceCodes.difference(dbServiceCodes).toList(); + final deleteCodes = dbServiceCodes.difference(apiServiceCodes).toList(); + final updateCodes = apiServiceCodes.intersection(dbServiceCodes).toList(); + if (ObjectUtil.isNotEmpty(addCodes)) { + await _batchAddOrUpdateProduct( + list: _filterProductListByCodes(list, addCodes), + sysType: sysType, + isUpdate: false, + ); + } + if (ObjectUtil.isNotEmpty(deleteCodes)) { + await productDao.delByIds(deleteCodes); + } + if (ObjectUtil.isNotEmpty(updateCodes)) { + await _batchAddOrUpdateProduct( + list: _filterProductListByCodes(list, updateCodes), + sysType: sysType, + isUpdate: true, + ); + } + } + + static Future _batchAddOrUpdateProduct({ + required List list, + required SysType sysType, + required bool isUpdate, + }) async { + if (ObjectUtil.isEmpty(list)) return; + final app = Get.find(); + final dataBase = app.dataBase; + final productDao = dataBase.getProductDao; + List changeList = []; + for (final v in list) { + ProductEntity item = ProductEntity( + serviceUniqueCode: sysType == SysType.canteen + ? v.spuId.toString() + : v.spuId.toString(), + sysType: sysType, + classifyServiceUniqueCode: + v.categoryId != null ? v.categoryId.toString() : "-1", + name: v.name!, + price: v.price ?? 0, + image: v.picUrl, + status: 1, + sort: v.sort ?? 999, + createTime: DateTime.now(), + updateTime: DateTime.now(), + hourType: ProductHourType.values.fromCode(v.hourType), + isDefault: false, + delFlag: false, + ); + if (isUpdate) { + final detail = await productDao.getByCode(item.serviceUniqueCode); + if (ObjectUtil.isEmpty(detail)) continue; + if (detail == item) continue; + item.id = detail?.id; + } + changeList.add(item); + } + if (ObjectUtil.isNotEmpty(changeList)) { + isUpdate + ? await productDao.batchEdit(changeList) + : await productDao.batchSave(changeList); + } + } + + static List _filterProductListByCodes( + List sourceList, + List codes, + ) { + return sourceList + .where((item) => codes.contains(item.spuId.toString())) + .toList(); + } +} diff --git a/lib/common/utils/sync/sync_user_tool.dart b/lib/common/utils/sync/sync_user_tool.dart new file mode 100644 index 0000000..ea6a1fd --- /dev/null +++ b/lib/common/utils/sync/sync_user_tool.dart @@ -0,0 +1,82 @@ +import 'package:get/get.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +abstract class SyncUserTool { + static const _tag = "sync商品工具"; + + // 获取存储用户 + static Future storageUserByCarNumber(String cardNumber) async { + final app = Get.find(); + final userDao = app.dataBase.getUserDao; + return await userDao.findByCardNumber(cardNumber); + } + + // 获取存储用户 + static Future storageUserByUserId(String userId) async { + final app = Get.find(); + final userDao = app.dataBase.getUserDao; + return await userDao.findByUserId(userId); + } + + /// 存储用户 + static Future storageUser({required ServiceUser user}) async { + try { + final app = Get.find(); + final userDao = app.dataBase.getUserDao; + final detail = await userDao.findByUserId(user.userId!); + if (detail == null) { + final obj = _parseStorageUser(user: user, createDate: true); + await userDao.save(obj); + logger.i("|$_tag|,存储用户,新增成功|${obj.toJson()}|"); + } else { + final obj = _parseStorageUser(user: user, updateDate: true); + obj.id = detail.id; + await userDao.edit(obj); + logger.i("|$_tag|,存储用户,修改成功|${obj.toJson()}|"); + } + } catch (e, stack) { + logger.e("|$_tag|,存储用户失败意外的错误error:|$e|stack:|$stack|"); + } + } + + static ServiceUser parsePayUser({ + required UserEntity user, + }) { + return ServiceUser( + userId: user.userId, + username: user.nickname, + deptId: null, + userMobile: user.mobile, + userAvatar: user.avatar, + walletId: user.walletId, + walletTypeId: user.walletTypeId, + walletBalance: user.walletBalance, + walletIsFreeze: false, + walletCardNumber: user.walletCardNumber, + ); + } + + static UserEntity _parseStorageUser({ + required ServiceUser user, + bool createDate = false, + bool updateDate = false, + }) { + final userEntity = UserEntity( + userId: user.userId ?? "-1", + nickname: user.username ?? "未知", + mobile: user.userMobile, + avatar: user.userAvatar, + walletCardNumber: user.walletCardNumber, + walletId: user.walletId, + walletTypeId: user.walletTypeId, + walletBalance: user.walletBalance, + ); + if (createDate) { + userEntity.createTime = DateTime.now(); + } + if (updateDate) { + userEntity.updateTime = DateTime.now(); + } + return userEntity; + } +} diff --git a/lib/common/values/constants.dart b/lib/common/values/constants.dart new file mode 100644 index 0000000..5de9b4f --- /dev/null +++ b/lib/common/values/constants.dart @@ -0,0 +1,21 @@ +/// 常量 +class Constants { + static const ttsId = "b83e0646"; + + // oauth2 APP模式客户端信息 + static const oauth2AppClient = 'mini:mini'; + + // oauth2 登录的url + static const oauth2AppApiUrl = 'auth/oauth2/token'; + + // 前端加密密钥 + static const pwdEncKey = 'pigxpigxpigxpigx'; + + /// 服务 url + static const apiUrl = 'https://stgl.pj.gov.cn/api'; + + /// 静态资源 url + static const ossUrl = apiUrl; + + static const localTTSPlugsMethodChannel = 'local/tts'; +} diff --git a/lib/common/values/crt.dart b/lib/common/values/crt.dart new file mode 100644 index 0000000..22c0970 --- /dev/null +++ b/lib/common/values/crt.dart @@ -0,0 +1,4 @@ +/// 证书 assets +class AssetsCrt { + static const emqxCa = "assets/crt/emqxsl-ca.crt"; +} diff --git a/lib/common/values/images.dart b/lib/common/values/images.dart new file mode 100644 index 0000000..99cbafe --- /dev/null +++ b/lib/common/values/images.dart @@ -0,0 +1,5 @@ +/// 图片 assets +class AssetsImages { + static const logo = "assets/images/logo.png"; + static const defaultAvatar = "assets/images/default_avatar.png"; +} diff --git a/lib/common/values/index.dart b/lib/common/values/index.dart new file mode 100644 index 0000000..e2fc4fc --- /dev/null +++ b/lib/common/values/index.dart @@ -0,0 +1,6 @@ +library; + +export './constants.dart'; +export './crt.dart'; +export './images.dart'; +export './svgs.dart'; diff --git a/lib/common/values/svgs.dart b/lib/common/values/svgs.dart new file mode 100644 index 0000000..d379140 --- /dev/null +++ b/lib/common/values/svgs.dart @@ -0,0 +1,3 @@ +/// svgs assets +class AssetsSvgs { +} diff --git a/lib/common/widgets/app/app_check_pay_type_dialog.dart b/lib/common/widgets/app/app_check_pay_type_dialog.dart new file mode 100644 index 0000000..875df68 --- /dev/null +++ b/lib/common/widgets/app/app_check_pay_type_dialog.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +Widget appCheckPayTypeDialog() { + return SizedBox.shrink(); +} diff --git a/lib/common/widgets/app/app_date_picker.dart b/lib/common/widgets/app/app_date_picker.dart new file mode 100644 index 0000000..b9da22f --- /dev/null +++ b/lib/common/widgets/app/app_date_picker.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +Widget appDatePicer( + BuildContext context, +) { + return FCalendar( + controller: FCalendarController.range( + initialSelection: (DateTime(2024, 7, 17), DateTime(2024, 7, 20)), + ), + start: DateTime(2024), + today: DateTime(2024, 7, 15), + end: DateTime(2030), + ); +} diff --git a/lib/common/widgets/app/app_divider_title.dart b/lib/common/widgets/app/app_divider_title.dart new file mode 100644 index 0000000..a808c41 --- /dev/null +++ b/lib/common/widgets/app/app_divider_title.dart @@ -0,0 +1,24 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +Widget appDividerTitle(BuildContext context, String title) { + final theme = FTheme.of(context); + return [ + FDivider( + style: theme.dividerStyles.horizontalStyle.copyWith( + padding: EdgeInsets.zero, + ), + ).flexible(flex: 1), + Text( + title, + textAlign: TextAlign.center, + style: theme.typography.xl, + ).padding(horizontal: 50.w), + FDivider( + style: theme.dividerStyles.horizontalStyle.copyWith( + padding: EdgeInsets.zero, + ), + ).flexible(flex: 1), + ].toRow(mainAxisAlignment: MainAxisAlignment.center); +} diff --git a/lib/common/widgets/app/app_icon.dart b/lib/common/widgets/app/app_icon.dart new file mode 100644 index 0000000..1d84b8a --- /dev/null +++ b/lib/common/widgets/app/app_icon.dart @@ -0,0 +1,19 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +Widget appPrefixIcon(BuildContext context, SvgAsset icon) { + final theme = FTheme.of(context); + final iconStyle = theme.style.iconStyle.copyWith( + size: theme.typography.xl.fontSize, + ); + return FIcon(icon, size: iconStyle.size).paddingLeft(20.w); +} + +Widget appSuffixIcon(BuildContext context, SvgAsset icon) { + final theme = FTheme.of(context); + final iconStyle = theme.style.iconStyle.copyWith( + size: theme.typography.xl.fontSize, + ); + return FIcon(icon, size: iconStyle.size).paddingRight(20.w); +} diff --git a/lib/common/widgets/app/app_img.dart b/lib/common/widgets/app/app_img.dart new file mode 100644 index 0000000..7f04444 --- /dev/null +++ b/lib/common/widgets/app/app_img.dart @@ -0,0 +1,112 @@ +import 'dart:io'; + +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +Widget appNetworkImg( + String src, + double errorSize, { + double imgWidth = 60, + double imgHeight = 60, + BoxFit fit = BoxFit.cover, +}) { + return Image.network( + AppNetworkTool.ossImage(src), + width: imgWidth, + height: imgHeight, + fit: fit, + loadingBuilder: ( + BuildContext context, + Widget child, + ImageChunkEvent? loadingProgress, + ) { + if (loadingProgress == null) { + return child; + } else { + return Center( + child: SizedBox( + width: imgHeight, + height: imgHeight, + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + (loadingProgress.expectedTotalBytes ?? 1) + : null, + ), + ), + ); + } + }, + errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { + final theme = FTheme.of(context); + return FIcon( + FAssets.icons.globeLock, + color: theme.colorScheme.error, + size: theme.typography.base.fontSize, + ).width(imgWidth * 0.5).height(imgHeight * 0.5); + }, + ); +} + +Widget appFileImg( + String str, + double errorSize, { + double imgWidth = 60, + double imgHeight = 60, + BoxFit fit = BoxFit.cover, +}) { + return Image.file( + File(str), + fit: fit, + width: imgWidth, + height: imgHeight, + errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { + final theme = FTheme.of(context); + return FIcon( + FAssets.icons.globeLock, + color: theme.colorScheme.error, + size: theme.typography.base.fontSize, + ).width(imgWidth * 0.5).height(imgHeight * 0.5); + }, + ); +} + +Widget appImg( + String? src, { + double width = 100, + double height = 100, + double errorSize = 20, + BoxFit fit = BoxFit.cover, +}) { + late Widget widget; + if (ObjectUtil.isEmpty(src)) { + final context = Get.context!; + final theme = FTheme.of(context); + widget = FIcon( + FAssets.icons.globeLock, + color: theme.colorScheme.error, + size: theme.typography.base.fontSize, + ).center().width(width * 0.5).height(width * 0.5); + } else { + widget = AppNetworkTool.isNetworkImage(src!) + ? appNetworkImg( + src, + errorSize, + imgWidth: double.infinity, + imgHeight: height, + fit: fit, + ) + : appFileImg( + src, + errorSize, + imgWidth: double.infinity, + imgHeight: height, + fit: fit, + ); + } + return widget.center().width(width).height(height); +} diff --git a/lib/common/widgets/app/app_logo.dart b/lib/common/widgets/app/app_logo.dart new file mode 100644 index 0000000..8dc8716 --- /dev/null +++ b/lib/common/widgets/app/app_logo.dart @@ -0,0 +1,42 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +Widget appLogo({double logoSize = 100}) { + return FutureBuilder( + future: AppSettingTool.getSysSetting(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.shrink(); + } + final sysType = snapshot.data!.sysType; + final theme = FTheme.of(context); + List logoTexts = [ + Text( + sysType.cnStr, + style: theme.typography.xl, + ), + Text( + sysType.usStr, + style: theme.typography.lg, + ), + ]; + List widgets = [ + Image.asset( + AssetsImages.logo, + fit: BoxFit.cover, + width: logoSize.w, + height: logoSize.w, + ), + SizedBox(width: 20.w), + logoTexts.toColumn(crossAxisAlignment: CrossAxisAlignment.start), + ]; + return widgets.toRow( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + ); + }, + ); +} diff --git a/lib/common/widgets/app/app_message_dialog.dart b/lib/common/widgets/app/app_message_dialog.dart new file mode 100644 index 0000000..5fe3ec0 --- /dev/null +++ b/lib/common/widgets/app/app_message_dialog.dart @@ -0,0 +1,42 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +Future appMessageDialog(BuildContext context, String msg) async { + final theme = FTheme.of(context); + final res = await showAdaptiveDialog( + context: context, + builder: (context) => FDialog( + direction: Axis.horizontal, + title: [ + FIcon( + FAssets.icons.circleAlert, + color: theme.colorScheme.primary, + size: theme.typography.xl2.fontSize, + ).paddingOnly(top: 5.h), + Text("警告", style: theme.typography.xl).paddingOnly(left: 10.w), + ].toRow(crossAxisAlignment: CrossAxisAlignment.center), + body: Text(msg, style: theme.typography.base) + .width(context.width * 0.5) + .marginSymmetric( + vertical: 10.h, + ), + actions: [ + FButton( + style: FButtonStyle.outline, + label: Text('取消', style: theme.typography.base), + onPress: () => Navigator.of(context).pop(false)), + FButton( + label: Text( + '确定', + style: theme.typography.base.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () => Navigator.of(context).pop(true)), + ], + ), + ); + return res ?? false; +} diff --git a/lib/common/widgets/app/app_page.dart b/lib/common/widgets/app/app_page.dart new file mode 100644 index 0000000..fd1d51d --- /dev/null +++ b/lib/common/widgets/app/app_page.dart @@ -0,0 +1,51 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; +import 'package:number_pagination/number_pagination.dart'; + +Widget appPage( + BuildContext context, { + required int pageNo, + required int pageSize, + required int total, + required Function(int) onPageChanged, +}) { + final theme = FTheme.of(context); + final totalPages = (total / pageSize).ceil(); + return [ + [ + Text( + "共 $total 条", + style: theme.typography.lg.copyWith( + fontWeight: theme.typography.xl8.fontWeight, + color: theme.colorScheme.primary, + ), + ), + ].toRow(), + total == 0 + ? const SizedBox.shrink() + : NumberPagination( + betweenNumberButtonSpacing: 10.w, + visiblePagesCount: 15, + totalPages: totalPages, + currentPage: pageNo, + unSelectedNumberColor: theme.colorScheme.primary, + selectedButtonColor: theme.colorScheme.primary, + selectedNumberColor: theme.colorScheme.secondary, + unSelectedButtonColor: theme.colorScheme.secondary, + controlButtonColor: theme.colorScheme.secondary, + buttonElevation: 1, + onPageChanged: (int pageNumber) { + onPageChanged(pageNumber); + }, + // enableInteraction: false, + ).expanded(), + ] + .toRow( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + ) + .marginOnly(bottom: 10.h) + .width(double.infinity); +} diff --git a/lib/common/widgets/app/app_price.dart b/lib/common/widgets/app/app_price.dart new file mode 100644 index 0000000..e001551 --- /dev/null +++ b/lib/common/widgets/app/app_price.dart @@ -0,0 +1,34 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +Widget appPrice(BuildContext context, dynamic price) { + final theme = FTheme.of(context); + double? priceNum = price is double + ? price + : double.tryParse( + price.toString(), + ); + priceNum ??= 0; + if (priceNum != 0) { + priceNum /= 100; + } + return [ + FIcon( + FAssets.icons.japaneseYen, + color: theme.colorScheme.error, + size: theme.typography.sm.fontSize, + ), + Text( + priceNum.toStringAsFixed(2), + style: theme.typography.lg.copyWith( + color: theme.colorScheme.error, + height: 1, + ), + ), + ].toRow( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + ); +} diff --git a/lib/common/widgets/app/app_radio_hour_type_dialog.dart b/lib/common/widgets/app/app_radio_hour_type_dialog.dart new file mode 100644 index 0000000..4a1bd53 --- /dev/null +++ b/lib/common/widgets/app/app_radio_hour_type_dialog.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +Widget appRadioHourTypeDialog( + BuildContext context, + Function(ProductHourType) onChanged, { + ProductHourType hourType = ProductHourType.morning, +}) { + final theme = FTheme.of(context); + final controller = FRadioSelectGroupController( + value: hourType.code, + ); + final list = ProductHourType.values.where((v) => v.code != "-1"); + return FSelectMenuTile( + title: Text( + "用餐时间", + style: theme.typography.base, + ), + groupController: controller, + validator: (value) => value == null ? '请选择' : null, + autoHide: true, + prefixIcon: FIcon( + FAssets.icons.sun, + size: theme.typography.xl.fontSize, + ), + menu: list.map((v) { + return FSelectTile( + title: Text( + v.title, + style: theme.typography.xl, + ), + value: v.code, + ); + }).toList(), + details: ListenableBuilder( + listenable: controller, + builder: (context, _) { + final v = controller.value.firstOrNull; + if (v == null) return const SizedBox(); + final hourType = ProductHourType.values.fromCode(v)!; + onChanged(hourType); + return Text( + hourType.title, + style: theme.typography.base.copyWith( + color: theme.colorScheme.primary, + ), + ); + }, + ), + ); +} diff --git a/lib/common/widgets/app/app_title.dart b/lib/common/widgets/app/app_title.dart new file mode 100644 index 0000000..e13d1c9 --- /dev/null +++ b/lib/common/widgets/app/app_title.dart @@ -0,0 +1,18 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +Widget appPrefixTitle(BuildContext context, String? str) { + final theme = FTheme.of(context); + final textStyle = theme.typography.xl; + if (ObjectUtil.isEmpty(str)) str = "--"; + return Text(str!, style: textStyle).paddingLeft(20.w); +} + +Widget appSuffixTitle(BuildContext context, String? str) { + final theme = FTheme.of(context); + final textStyle = theme.typography.xl; + if (ObjectUtil.isEmpty(str)) str = "--"; + return Text(str!, style: textStyle).paddingRight(20.w); +} diff --git a/lib/common/widgets/index.dart b/lib/common/widgets/index.dart new file mode 100644 index 0000000..1907aa0 --- /dev/null +++ b/lib/common/widgets/index.dart @@ -0,0 +1,22 @@ +library; + +export './toast/toast_widget.dart'; + +export './app/app_check_pay_type_dialog.dart'; +export './app/app_date_picker.dart'; +export './app/app_divider_title.dart'; +export './app/app_icon.dart'; +export './app/app_img.dart'; +export './app/app_logo.dart'; +export './app/app_message_dialog.dart'; +export './app/app_page.dart'; +export './app/app_price.dart'; +export './app/app_radio_hour_type_dialog.dart'; +export './app/app_title.dart'; + +export './order/mixin/order_mixin.dart'; +export './order/select_pay_type_dialog.dart'; +export './order/steelyard_container.dart'; +export 'order/order_common.dart'; + +export './user/user_info.dart'; diff --git a/lib/common/widgets/order/mixin/order_mixin.dart b/lib/common/widgets/order/mixin/order_mixin.dart new file mode 100644 index 0000000..6d3a3d0 --- /dev/null +++ b/lib/common/widgets/order/mixin/order_mixin.dart @@ -0,0 +1,521 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_subscreen_plugin/flutter_subscreen_plugin.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +mixin OrderMixin { + static const _tag = "OrderMixin"; + + final appController = Get.find(); + + // 路由变化 + StreamSubscription? routerBus; + + /// **************** start 需要重制 start **************** + + // 时间类型 + Rx hourType = Rx(ProductHourType.morning); + + // 设备类型 + SysType _sysType = SysType.canteen; + + // 语音锁 + bool _speechingLock = false; + + /// **************** end 需要重制 end **************** + + // 分类 + RxList classifyList = RxList([]); + + // 商品 + RxList productList = RxList([]); + + // 商品分类ids + RxList productClassifyList = RxList([]); + + // 总价 + RxInt totalPrice = RxInt(0); + + // 额外需要支付的金额土豪税 + RxInt amountMealPrice = RxInt(0); + + // 购物车 + RxList shopCardList = RxList([]); + + // 订单类型 + OrderType orderType = OrderType.below; + + // 订单支付类型 + Rxn orderPayType = Rxn(OrderPayType.face); + + // 显示的支付类型 + Rxn orderPayDisplayType = Rxn( + OrderPayDisplayType.qrCode, + ); + + // 当前的tab + RxInt currentTabIndex = RxInt(0); + + // 购买用户 + Rxn payUser = Rxn(null); + + // 二维码识别结果 + ServiceQRCode? globalQrCode; + + // 本地订单id + int? sqlOrderId; + + Future? putOffSendSubScreenDelayed; + + /// **************** start 更新属性 start **************** + set sysType(SysType type) { + _sysType = type; + sendSubScreenType(); + } + + /// **************** end 更新属性 end **************** + + // 点击分类tab + Future doTab(int i) async { + EasyLoading.show( + status: '切换中...', + maskType: EasyLoadingMaskType.black, + ); + currentTabIndex.value = i; + if (i <= classifyList.length - 1) { + final current = classifyList[i]; + await getProduct(classifyCode: current.serviceUniqueCode); + } + EasyLoading.dismiss(); + } + + // 获取分类 + Future getClassify() async { + final list = await appController.dataBase.getClassifyDao.getAll( + _sysType.name, + hourType.value.code, + ); + productClassifyList.value = list.map((v) => v.serviceUniqueCode).toList(); + list.insert( + 0, + ClassifyEntity( + serviceUniqueCode: '-1', + sysType: _sysType, + name: '全部', + createTime: DateTime.now(), + updateTime: DateTime.now(), + sort: 0, + categoryType: "-1", + delFlag: false, + ), + ); + classifyList.value = list; + } + + // 获取商品 + Future getProduct({ + String classifyCode = "-1", + }) async { + if (hourType.value != appController.hourType.value) { + hourType.value = appController.hourType.value; + } + final list = await appController.dataBase.getProductDao.getAll( + classifyCode, + hourType.value.code, + _sysType.name, + ); + productList.value = list; + productList.refresh(); + productClassifyList.refresh(); + } + + // 创建本地订单 + Future createSqlOrder() async { + final orderSourceTypeId = getOrderSourceTypeId(); + try { + final order = OrderEntity( + uniqueCode: await _getUniqueCode(), + serviceUniqueCode: '001', + sysType: _sysType, + userCode: '001', + username: "", + hourType: hourType.value.code, + payType: orderPayType.value!, + walletId: payUser.value?.walletId, + orderSourceTypeId: orderSourceTypeId, + type: orderType, + status: OrderStatus.nonePay, + price: getTotalPrice(), + extraPrice: amountMealPrice.value, + payPrice: getTotalPrice() + amountMealPrice.value, + productDesc: getProductDesc(), + count: getProductCount(), + weight: getProductWeight(), + createTime: DateTime.now(), + ); + final id = await appController.dataBase.getOrderDao.save(order); + if (id == null) { + logger.e("|$_tag|创建本地订单,创建父订单错误"); + return null; + } + logger.i("|$_tag|创建本地订单成功orderId:|$id|;"); + sqlOrderId = id; + return id; + } catch (e) { + logger.e("|$_tag|创建本地订单error:|$e|"); + } + return null; + } + + // 创建本地购物车 + Future createSqlChildCar() async { + if (ObjectUtil.isEmpty(sqlOrderId)) { + logger.e("|$_tag|创建本地购物车,失败本地订单id不存在"); + return false; + } + final orderDao = appController.dataBase.getOrderDao; + final sqlOrderDetail = await orderDao.findById(sqlOrderId!); + if (ObjectUtil.isEmpty(sqlOrderDetail)) { + logger.e("|$_tag|创建本地购物车,失败本地订单不存在,sqlOrderDetail:|$sqlOrderDetail|;"); + return false; + } + try { + final cardList = shopCardList.map((e) { + e.orderId = sqlOrderId!; + e.productWidget = double.parse(e.productWidget.toStringAsFixed(2)); + return e; + }).toList(); + await appController.dataBase.getShopCardDao.batchSave(cardList); + return true; + } catch (e) { + logger.i("|$_tag|创建本地购物车,失败orderId:|$sqlOrderId|;error:|$e|"); + } + return false; + } + + // 更新本地订单 + Future updateSqlOrder() async { + if (ObjectUtil.isEmpty(sqlOrderId)) { + logger.e("|$_tag|更新本地订单,失败本地订单id不存在"); + return false; + } + final orderDao = appController.dataBase.getOrderDao; + final sqlOrderDetail = await orderDao.findById(sqlOrderId!); + if (ObjectUtil.isEmpty(sqlOrderDetail)) { + logger.e("|$_tag|更新本地订单,失败本地订单不存在,sqlOrderDetail:|$sqlOrderDetail|;"); + return false; + } + try { + // 更新本地订单 + sqlOrderDetail!.username = payUser.value?.username ?? "未知"; + sqlOrderDetail.userCode = payUser.value!.userId.toString(); + sqlOrderDetail.productDesc = getProductDesc(); + sqlOrderDetail.count = getProductCount(); + sqlOrderDetail.weight = getProductWeight(); + sqlOrderDetail.price = getTotalPrice(); + sqlOrderDetail.hourType = hourType.value.code; + await orderDao.edit(sqlOrderDetail); + logger.i("|$_tag|更新本地订单,成功orderId:|$sqlOrderId|;"); + await createSqlChildCar(); + return true; + } catch (e) { + logger.i("|$_tag|更新本地订单,失败orderId:|$sqlOrderId|;error:|$e|"); + } + return false; + } + + // |接口|创建api订单 + Future createApiOrder() async { + logger.i("|$_tag|开始创建api订单payUserId:|${payUser.value?.userId}|"); + final appSetting = await AppSettingTool.getSysSetting(); + final serviceId = appSetting.serviceId; + if (ObjectUtil.isEmpty(serviceId)) { + logger.e("|$_tag|开始创建api订单,失败serviceId不存在"); + Toast.error(msg: "创建订单失败,改设备没有进行绑定"); + return; + } + final serviceMerchantId = appSetting.serviceMerchantId; + if (ObjectUtil.isEmpty(serviceMerchantId)) { + logger.e("|$_tag|开始创建api订单,失败serviceMerchantId不存在"); + Toast.error(msg: "创建订单失败,改设备没有进行绑定"); + return; + } + if (ObjectUtil.isEmpty(sqlOrderId)) { + logger.e("|$_tag|开始创建api订单,失败本地订单id不存在"); + Toast.error(msg: "创建订单失败,本地订单id不存在"); + return; + } + final orderDao = appController.dataBase.getOrderDao; + final sqlOrderDetail = await orderDao.findById(sqlOrderId!); + if (ObjectUtil.isEmpty(sqlOrderDetail)) { + logger.e("|$_tag|开始创建api订单,失败本地订单不存在,sqlOrderDetail:|$sqlOrderDetail|"); + Toast.error(msg: "创建订单失败,本地订单不存在"); + return; + } + // 获取下单商品 + List> shopList = []; + if (_sysType == SysType.canteen) { + shopList = shopCardList.where((v) => v.productNumber > 0).map((v) { + return { + "productId": v.productServiceUniqueCode, + "productCount": v.productNumber, + "productWeight": 0, + }; + }).toList(); + } else if (_sysType == SysType.supermarket) { + shopList = shopCardList.where((v) => v.productWidget > 0).map((v) { + return { + "productId": v.productServiceUniqueCode, + "productCount": 0, + "productWeight": v.productWidget, + }; + }).toList(); + } + final isNetwork = AppNetworkTool.isAutoNetwork(); + var pa = { + "androidOrderId": sqlOrderId, + "androidOrderCode": sqlOrderDetail!.uniqueCode, + "sysType": _sysType.name, + "userId": payUser.value!.userId, + "hourType": hourType.value.code, + "hardwareId": serviceId, + "merchantId": serviceMerchantId!, + "orderType": orderType.name, + "orderSourceTypeId": getOrderSourceTypeId(), + "orderPayType": orderPayType.value?.name, + "list": shopList, + }; + if (orderPayType.value != null && + orderPayType.value == OrderPayType.walletQrCode) { + pa["walletId"] = globalQrCode?.id; + } + logger.i("|$_tag|创建api订单,下单参数:|${json.encode(pa)}|"); + try { + if (isNetwork) { + final res = await HardApi.createHardwareOrder(pa); + if (res.code != 0) { + throw Error(); + } + final data = res.data; + if (ObjectUtil.isEmpty(data)) throw Error(); + logger.i("|$_tag||有网络|创建api订单,下单成功res:|${res.toJson()}|"); + sqlOrderDetail.status = OrderStatus.pay; + sqlOrderDetail.serviceUniqueCode = data.toString(); + } else { + sqlOrderDetail.status = OrderStatus.offlinePay; + } + // 更新本地订单 + sqlOrderDetail.payTime = DateTime.now(); + await orderDao.edit(sqlOrderDetail); + EasyLoading.dismiss(); + Toast.success(msg: "购买成功"); + } catch (e, stack) { + EasyLoading.dismiss(); + Toast.error(msg: "结算订单失败"); + logger.e("|$_tag|创建api订单,下单失败error:|$e|stack:|$stack|"); + } + } + + Future speeching(String str) async { + if (_speechingLock) return; + try { + _speechingLock = true; + await Future.delayed(const Duration(seconds: 1)); + await LocalTtsPlugs.speeching(appId: Constants.ttsId, str: str); + await Future.delayed(const Duration(seconds: 2)); + } finally { + _speechingLock = false; + } + } + + // 获取商品渠道id + String getOrderSourceTypeId() { + String orderSourceTypeId = ""; + if (globalQrCode == null) return ""; + switch (orderType) { + case OrderType.below: + break; + case OrderType.reserve: + if (ObjectUtil.isEmpty(globalQrCode!.id)) { + Toast.error(msg: "二维码错误"); + return ""; + } + orderSourceTypeId = globalQrCode!.id.toString(); + break; + case OrderType.foreignTicket: + if (ObjectUtil.isEmpty(globalQrCode!.itemId)) { + Toast.error(msg: "二维码错误"); + return ""; + } + orderSourceTypeId = globalQrCode!.itemId.toString(); + break; + case OrderType.strideDept: + if (ObjectUtil.isEmpty(globalQrCode!.itemId)) { + Toast.error(msg: "二维码错误"); + return ""; + } + orderSourceTypeId = globalQrCode!.itemId.toString(); + break; + default: + break; + } + return orderSourceTypeId; + } + + // 获取商品的描述信息 + String getProductDesc() { + String desc = ""; + for (var e in shopCardList) { + desc += "${e.productName}x${e.productNumber};"; + } + return desc; + } + + // 获取商品的总数 + int getProductCount() { + int total = 0; + if (_sysType == SysType.supermarket) return shopCardList.length; + for (var e in shopCardList) { + total += e.productNumber; + } + return total; + } + + // 获取商品的总重量 + double getProductWeight() { + double total = 0; + if (_sysType == SysType.canteen) return total; + for (var e in shopCardList) { + total += e.productWidget; + } + return double.tryParse(total.toStringAsFixed(2))!; + } + + // 获取总价格 + int getTotalPrice() { + return totalPrice.value; + } + + // 计算总价 + void computedTotalPrice({isSendSubScreen = true}) { + int total = 0; + for (var e in shopCardList) { + total += e.totalPrice; + } + totalPrice.value = total; + if (isSendSubScreen) sendSubScreen(); + } + + /// **************** start 重制相关 start **************** + + // 重置状态 + void closeStatus() { + _speechingLock = false; + } + + /// **************** end 重制相关 end **************** + + /// **************** start 副屏幕相关 start **************** + + void putOffSendSubScreen({ + int seconds = 5, + }) { + if (putOffSendSubScreenDelayed != null) { + sendSubScreen(); + putOffSendSubScreenDelayed = null; + return; + } + putOffSendSubScreenDelayed = Future.delayed(Duration(seconds: seconds), () { + sendSubScreen(); + putOffSendSubScreenDelayed = null; + }); + } + + void sendSubScreen() { + final List list = shopCardList; + SubScreenPlugin.sendMsgToViceScreen( + SubScreenAction.updateCard.name, + params: {}, + ); + } + + void sendSubScreenType() { + SubScreenPlugin.sendMsgToViceScreen( + SubScreenAction.updateCard.name, + params: {"type": _sysType.name}, + ); + } + + /// **************** end 副屏幕相关 end **************** + + /// **************** start 路由变化相关 start **************** + listenRouter() { + routerBus = GlobalRouteObserver.globalRouteObserverBus + .on() + .listen((event) { + if (event.action == RouterActionType.didPop && + event.name == RouteNames.faceDiscern) {} + }); + } + + /// **************** end 路由变化相关 end **************** + + // 生成安卓唯一的code + Future _getUniqueCode() async { + String code = ""; + final appSetting = await AppSettingTool.getSysSetting(); + final macAddress = appSetting.androidId; + if (ObjectUtil.isNotEmpty(macAddress)) { + code += "${macAddress}_"; + } + int timestamp = DateTime.now().millisecondsSinceEpoch; + code += "${timestamp}_"; + Random random = Random(); + final randomNumber = + List.generate(6, (index) => random.nextInt(10).toString()).join(''); + code += randomNumber; + return code; + } + + // 获取额外需要的金额 + Future getAmountMealPrice({ + required price, + }) async { + try { + final app = Get.find(); + var setting = await AppSettingTool.getCanteenExtraPriceConfig(); + final isNetwork = AppNetworkTool.isAutoNetwork(); + // 有网络 + if (isNetwork) { + final data = await WalletApi.getPaySetting(); + if (data == null) throw Error(); + final storageData = CanteenExtraPriceSetting( + payAmountEnable: data.payAmountEnable!, + payAmountMealLimit: data.payAmountMealLimit!, + payAmountMealTimes: data.payAmountMealTimes!, + ); + if (setting == null || setting != storageData) { + await AppSettingTool.setCanteenExtraPriceConfig(storageData); + setting = storageData; + } + } + if (setting == null) return null; + if (!setting.payAmountEnable) return null; + double amountMealPrice = 0; + if (price > setting.payAmountMealLimit) { + amountMealPrice = + setting.payAmountMealTimes * (price - setting.payAmountMealLimit); + return amountMealPrice.round(); + } + } catch (e, stack) { + logger.e("结算订单失败error:|$e|stack:|$stack|"); + } + return null; + } +} diff --git a/lib/common/widgets/order/order_common.dart b/lib/common/widgets/order/order_common.dart new file mode 100644 index 0000000..64d63e0 --- /dev/null +++ b/lib/common/widgets/order/order_common.dart @@ -0,0 +1,149 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +/// 商品卡片 +Widget orderProductCard( + BuildContext context, { + String? image = "", + String name = "", + num price = 0.0, +}) { + final theme = FTheme.of(context); + return [ + appImg(image, width: double.infinity, height: 140.h), + Text(name, style: theme.typography.lg), + appPrice(context, price) + ] + .toColumn(crossAxisAlignment: CrossAxisAlignment.center) + .width(double.infinity) + .decorated( + border: Border.all( + color: theme.colorScheme.mutedForeground, + width: 1, + ), + borderRadius: theme.style.borderRadius / 3, + ); +} + +/// 菜品总价 +/// 额外支付 +/// 实际扣款 +Widget orderAllPrice( + BuildContext context, { + required num totalPrice, + required num amountMealPrice, +}) { + final theme = FTheme.of(context); + return [ + FDivider( + style: theme.dividerStyles.verticalStyle.copyWith( + color: theme.colorScheme.primary, + padding: EdgeInsets.zero, + ), + ), + [ + Text( + "菜品总价", + style: theme.typography.lg, + ), + appPrice(context, totalPrice), + ].toRow(), + [ + Text( + "额外支付", + style: theme.typography.lg, + ), + appPrice(context, amountMealPrice), + ].toRow(), + [ + Text( + "实际扣款", + style: theme.typography.lg, + ), + appPrice( + context, + totalPrice + amountMealPrice, + ), + ].toRow(), + FDivider( + style: theme.dividerStyles.verticalStyle.copyWith( + color: theme.colorScheme.primary, + padding: EdgeInsets.zero, + ), + ), + ].toColumn(); +} + +/// 商品选择面板 +Widget orderSelectPanel( + BuildContext context, { + required void Function(int, int) onChange, + String label = "馒头馒头馒头馒头馒头", + int total = 1, + num price = 0, + int index = 0, + double height = 50, +}) { + final theme = FTheme.of(context); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final maxWidth = constraints.maxWidth; + return [ + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.typography.lg, + ).width(maxWidth * 0.4), + [ + appPrice(context, price), + SizedBox(width: 10.w), + FIcon( + FAssets.icons.minus, + color: theme.colorScheme.secondary, + ) + .width(height.w) + .height(double.infinity) + .decorated(color: theme.colorScheme.primary) + .onTap(() { + if (total > 0) { + onChange(total - 1, index); + } + }), + Text( + total.toString(), + style: theme.typography.lg, + ).center().width(60.w).height(double.infinity).decorated( + color: theme.colorScheme.secondary, + borderRadius: theme.style.borderRadius / 2, + ), + FIcon( + FAssets.icons.plus, + color: theme.colorScheme.secondary, + ) + .width(height.w) + .height(double.infinity) + .decorated(color: theme.colorScheme.error) + .onTap(() { + if (total < 100) { + onChange(total + 1, index); + } + }), + ].toRow(mainAxisAlignment: MainAxisAlignment.end).expanded(), + ] + .toRow(mainAxisAlignment: MainAxisAlignment.center) + .height(height.h) + .padding(vertical: 5.h, horizontal: 5.w) + .decorated( + border: Border.all( + color: theme.colorScheme.border, + width: 1, + ), + borderRadius: theme.style.borderRadius / 2, + ); + }, + ); +} diff --git a/lib/common/widgets/order/select_pay_type_dialog.dart b/lib/common/widgets/order/select_pay_type_dialog.dart new file mode 100644 index 0000000..8a5e71a --- /dev/null +++ b/lib/common/widgets/order/select_pay_type_dialog.dart @@ -0,0 +1,542 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_subscreen_plugin/flutter_subscreen_plugin.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SelectPayTypeDialogController extends GetxController { + /// **************** start 回调函数 start **************** + + // |开始识别|的回调 + VoidCallback? onRecognition; + + // |确定|的回调 + Function( + ServiceUser, + OrderType, + OrderPayType, + OrderPayDisplayType, + ServiceQRCode?, + )? onSubmit; + + // |取消|的回调 + VoidCallback? onCancel; + + // 识别出用户的回调 + Function(ServiceUser?)? onDiscernUser; + + /// **************** end 回调函数 end **************** + + static const String _tag = "支付识别 Controller"; + + final app = Get.find(); + + Rxn payUser = Rxn(null); + + Rxn orderPayType = Rxn(OrderPayType.face); + + Rxn orderPayDisplayType = Rxn(OrderPayDisplayType.face); + + Rxn orderType = Rxn(OrderType.below); + + RxBool isRecognition = RxBool(false); + + StreamSubscription? faceDiscernListen; + + // 二维码识别结果 + ServiceQRCode? globalQrCode; + + // 语音锁 + bool _speechingLock = false; + + // 人脸识别回调锁 + bool _faceCallbackLock = false; + + _closeStatus() { + payUser.value = null; + isRecognition.value = false; + globalQrCode = null; + _speechingLock = false; + _faceCallbackLock = false; + } + + _closeFacility() { + app.scanCodeManage.close(); + app.serialCardManage.close(); + } + + _closeFaceDiscern() { + faceDiscernListen?.cancel(); + } + + // |开始识别|点击 + doRecognition() { + switch (orderPayDisplayType.value) { + case OrderPayDisplayType.qrCode: + _dispatchQrCode(); + break; + case OrderPayDisplayType.walletCard: + _dispatchWalletCard(); + break; + case OrderPayDisplayType.face: + _dispatchFace(); + break; + default: + } + onRecognition?.call(); + } + + // |确定|点击 + doSubmit() { + if (payUser.value == null) { + return; + } + onSubmit?.call( + payUser.value!, + orderType.value!, + orderPayType.value!, + orderPayDisplayType.value!, + globalQrCode, + ); + Navigator.of(Get.context!).pop(); + _closeStatus(); + _closeFacility(); + _closeFaceDiscern(); + } + + // |取消|点击 + doCancel() async { + if (isRecognition.value) { + final flag = await appMessageDialog(Get.context!, "是否取消识别"); + if (!flag) return; + _closeStatus(); + _closeFacility(); + _closeFaceDiscern(); + Navigator.of(Get.context!).pop(); + return; + } + _closeStatus(); + _closeFacility(); + _closeFaceDiscern(); + onCancel?.call(); + Navigator.of(Get.context!).pop(); + } + + // 二维码识别 + void _dispatchQrCode() async { + try { + if (isRecognition.value) { + Toast.error(msg: "正在识别中,请勿重复点击"); + return; + } + final isOpen = await app.scanCodeManage.open(); + if (!isOpen) { + logger.e("|$_tag|二维码识别,错误未打开设备"); + Toast.error(msg: "二维码扫码器错误"); + return; + } + app.scanCodeManage.listen((str) async { + // todo 这个地方以后只支持base64编码的字符串为了安全 + if (ObjectUtil.isEmpty(str)) return; + str = EncryptUtil.decodeBase64(str); + final qrCode = ServiceQRCode.fromJson(json.decode(str)); + if (ObjectUtil.isEmpty(qrCode)) { + Toast.error(msg: "识别错误,未识别到二维码"); + return; + } + if (qrCode.exceedTime == null) { + Toast.error(msg: "识别错误,二维码已过期"); + return; + } + if (qrCode.exceedTime != -1) { + int exceedTime = DateTime.fromMillisecondsSinceEpoch( + qrCode.exceedTime!, + ).millisecondsSinceEpoch; + int nowTime = DateTime.now().millisecondsSinceEpoch; + if (exceedTime <= nowTime) { + Toast.error(msg: "识别错误,二维码已过期"); + return; + } + } + globalQrCode = qrCode; + // 解析为enum 支付类型 + if (ObjectUtil.isNotEmpty(qrCode.payType)) { + final OrderPayType? type = + OrderPayType.values.fromCode(qrCode.payType!); + if (ObjectUtil.isNotEmpty(type)) { + orderPayType.value = type!; + } + } + // 解析为enum 订单类型 + if (ObjectUtil.isNotEmpty(qrCode.orderType)) { + final OrderType? type = OrderType.values.fromCode(qrCode.orderType!); + if (ObjectUtil.isNotEmpty(type)) { + orderType.value = type!; + } + } + if (orderPayType.value == null || orderType.value == null) { + logger.e("|$_tag|二维码识别,订单类型或者支付类型解析错误:|$str|"); + return; + } + if (!_validateQrCodeType(orderPayType.value!)) { + logger.e("|$_tag|二维码识别,无效的二维码json:|$str|"); + Toast.error(msg: "识别错误,无效的二维码"); + return; + } + logger.i("|$_tag|二维码识别,识别到的二维码json:|$str|"); + final isNetwork = AppNetworkTool.isAutoNetwork(); + final appSetting = await AppSettingTool.getSysSetting(); + if (isNetwork) { + final pa = { + "id": qrCode.userId, + "type": appSetting.sysType.name, + }; + final res = await HardApi.getUserInfoById(pa); + if (res.code != 0) { + throw Error(); + } + payUser.value = res.data; + if (payUser.value != null) { + await SyncUserTool.storageUser(user: payUser.value!); + } + logger.i("|$_tag|二维码识别|有网络|,用户信息:|${payUser.toJson()}|"); + } else { + if (ObjectUtil.isNotEmpty(qrCode.extra)) { + final extra = json.decode(qrCode.extra!); + final jsonPayUser = ServiceUser.fromJson(extra); + payUser.value = jsonPayUser; + logger.i("|$_tag|二维码识别|无网络|,用户信息:|$extra|"); + } + } + isRecognition.value = false; + await app.scanCodeManage.close(); + Toast.success(msg: "识别二维码成功"); + speeching("识别二维码成功"); + }); + isRecognition.value = true; + onDiscernUser?.call(payUser.value); + } catch (e) { + logger.e("|$_tag|二维码识别,失败error:|$e|"); + } + } + + // 饭卡识别 + void _dispatchWalletCard() async { + if (isRecognition.value) { + Toast.error(msg: "正在识别中,请勿重复点击"); + return; + } + try { + await app.serialCardManage.connect(); + if (!app.serialCardManage.isConnect) { + logger.e("|$_tag|饭卡识别,错误未打开设备"); + Toast.error(msg: "读卡器错误"); + return; + } + app.serialCardManage.callback = (type, status, card) async { + switch (status) { + case SerialCardResStatus.success: + if (ObjectUtil.isEmpty(card)) return; + speeching("识别饭卡成功"); + final isNetwork = AppNetworkTool.isAutoNetwork(); + if (isNetwork) { + final pa = {"card": card}; + final res = await HardApi.getUserByCard(pa); + if (res.code != 0 || res.data == null) { + logger.e("|$_tag|饭卡识别,无效的饭卡cardId:|$card|"); + isRecognition.value = false; + app.serialCardManage.close(); + Toast.error(msg: "查询不到此用户"); + return; + } + payUser.value = res.data; + if (payUser.value != null) { + await SyncUserTool.storageUser(user: payUser.value!); + } + } else { + final user = await SyncUserTool.storageUserByCarNumber(card); + if (user == null && + ObjectUtil.isNotEmpty(user?.walletCardNumber)) { + logger.e("|$_tag|,饭卡识别,离线获取本地用户失败"); + Toast.error(msg: "查询不到此用户"); + return; + } + payUser.value = SyncUserTool.parsePayUser(user: user!); + logger.i("|$_tag|,饭卡识别,离线获取本地用户成功:|${payUser.value!.toJson()}|"); + } + app.serialCardManage.close(); + isRecognition.value = false; + orderPayType.value = OrderPayType.walletCard; + orderType.value = OrderType.below; + break; + case SerialCardResStatus.warning: + break; + case SerialCardResStatus.error: + Toast.error(msg: "请在合适区域放置饭卡"); + isRecognition.value = false; + app.serialCardManage.close(); + logger.i("|$_tag|饭卡识别,连续一段时间未识别到饭卡"); + break; + default: + } + }; + app.serialCardManage.sendRead(); + isRecognition.value = true; + onDiscernUser?.call(payUser.value); + } catch (e) { + isRecognition.value = false; + app.serialCardManage.close(); + logger.e("|$_tag|饭卡识别,失败error:|$e|"); + } + } + + // 人脸支付 + void _dispatchFace() async { + try { + EasyLoading.dismiss(); + EasyLoading.show( + status: '人脸识别准备中...', + maskType: EasyLoadingMaskType.black, + ); + final faceDao = app.dataBase.getFaceDao; + final databaseFaceCount = await faceDao.findCount("-1"); + if (databaseFaceCount == null || databaseFaceCount == 0) { + Toast.error(msg: "人脸数据库为空,请前往'设置'->'人脸管理'点击同步人脸"); + throw Error(); + } + final isActive = await AppFaceTool.isActiveSdk(); + if (!isActive) { + Toast.error(msg: "请检查激人脸sdk激活码"); + throw Error(); + } + final faceCount = await FaceDiscernPlugs.getFaceCount(); + if (faceCount == 0) { + await AppFaceTool.faceRegister(); + } + final isJump = await AppFaceTool.jumpFacePage(); + if (!isJump) { + Toast.error(msg: "跳转到人脸检测页面失败"); + throw Error(); + } + _listenAndroidFaceBack(); + faceDiscernListen = FaceDiscernPlugs.faceStream.listen((event) { + if (event == null) return; + if (_faceCallbackLock) return; + _faceCallbackLock = true; + final userId = event.userId; + if (userId == null) { + Toast.error(msg: "人脸识别失败,请重试"); + return; + } + faceDiscernAfter(event); + SubScreenPlugin.doubleScreenShow(); + }); + } catch (e) { + EasyLoading.dismiss(); + } + } + + /// android人脸识别成功回调 + Future faceDiscernAfter(FaceStream event) async { + try { + EasyLoading.show( + status: '获取用户信息中...', + maskType: EasyLoadingMaskType.black, + ); + final isNetwork = AppNetworkTool.isAutoNetwork(); + final appSetting = await AppSettingTool.getSysSetting(); + if (isNetwork) { + final pa = { + "id": event.userId, + "type": appSetting.sysType.name, + }; + final res = await HardApi.getUserInfoById(pa); + if (res.code != 0) { + throw Error(); + } + payUser.value = res.data; + logger.i("|_tag|人脸识别|有网络|,用户信息:|${payUser.toJson()}|"); + if (payUser.value != null) { + await SyncUserTool.storageUser(user: payUser.value!); + } + } else { + // 没有网络去本地数据库查询 + final user = await SyncUserTool.storageUserByUserId(event.userId!); + if (user == null) { + logger.e("|_tag|,人脸识别,离线获取本地用户失败"); + Toast.error(msg: "查询不到此用户"); + return; + } + payUser.value = SyncUserTool.parsePayUser(user: user); + payUser.refresh(); + logger.i("|_tag|人脸识别|无网络|,用户信息:|${payUser.toJson()}|"); + } + orderPayType.value = OrderPayType.face; + orderType.value = OrderType.below; + onDiscernUser?.call(payUser.value); + } catch (e, task) { + logger.e("|_tag|人脸识别,意外的错误error:|$e|task:|$task |"); + Toast.error(msg: "识别人脸错误,请重试"); + } finally { + _faceCallbackLock = false; + EasyLoading.dismiss(); + } + } + + /// android 点击返回回调 + _listenAndroidFaceBack() { + final getChannel = FaceDiscernPlugs.getChannel; + getChannel.setMethodCallHandler((call) async { + if (call.method == "faceDiscernBack") { + EasyLoading.dismiss(); + SubScreenPlugin.doubleScreenShow(); + } + if (call.method == "faceDiscernPageInit") { + EasyLoading.dismiss(); + } + }); + } + + /// 校验二维码类型是否符合 + _validateQrCodeType(OrderPayType type) { + const success = [ + OrderPayType.walletQrCode, + OrderPayType.strideDeptQrCode, + OrderPayType.foreignTicketQrCode, + ]; + return success.contains(type); + } + + /// 语音 + Future speeching(String str) async { + if (_speechingLock) return; + try { + _speechingLock = true; + await Future.delayed(const Duration(seconds: 1)); + await LocalTtsPlugs.speeching(appId: Constants.ttsId, str: str); + await Future.delayed(const Duration(seconds: 2)); + } finally { + _speechingLock = false; + } + } +} + +Future selectPayTypeDialog(BuildContext context) async { + final payTypeC = Get.find(); + final payUser = payTypeC.payUser.value; + payTypeC.orderPayType.value = OrderPayType.face; + payTypeC.orderPayDisplayType.value = OrderPayDisplayType.face; + + final controller = FRadioSelectGroupController( + value: payTypeC.orderPayDisplayType.value, + ); + + final theme = FTheme.of(context); + final screenWidth = context.screenWidth; + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => FDialog( + direction: Axis.vertical, + title: Text("选择支付方式", style: theme.typography.lg), + body: [ + FSelectGroup( + controller: controller, + style: theme.selectGroupStyle.copyWith( + labelLayoutStyle: theme.selectGroupStyle.labelLayoutStyle.copyWith( + childPadding: EdgeInsets.symmetric( + vertical: 10.h, + ), + ), + radioStyle: theme.selectGroupStyle.radioStyle.copyWith( + padding: EdgeInsets.symmetric( + vertical: 10.h, + ), + ), + ), + items: OrderPayDisplayType.values + .map>( + (v) => FSelectGroupItem.radio( + value: v, + label: Text( + v.title, + style: theme.typography.xl, + ), + ), + ) + .toList(), + ), + SizedBox(height: 20.h), + Obx(() { + final payTypeCode = payTypeC.orderPayDisplayType.value?.code; + if (payTypeCode == null) return const SizedBox.shrink(); + const baseMsg = "请点击开始识别按钮"; + String msg = ""; + SvgAsset icon = FAssets.icons.bird; + if (payTypeCode == "1") { + msg = "$baseMsg,并让用户扫描二维码"; + icon = FAssets.icons.qrCode; + } + if (payTypeCode == "2") { + msg = "$baseMsg,并让用户刷饭卡"; + icon = FAssets.icons.idCard; + } + if (payTypeCode == "3") { + msg = "$baseMsg,并让用户进行刷脸"; + icon = FAssets.icons.scanFace; + } + return [ + FIcon( + icon, + size: theme.typography.xl4.fontSize, + ), + SizedBox(height: 10.h), + Text( + msg, + style: theme.typography.lg, + ), + ].toColumn().padding(vertical: 120.h); + }), + ].toColumn().width(screenWidth), + actions: [ + [ + FButton( + label: Text( + payUser == null ? "开始识别" : "确定支付", + style: theme.typography.xl.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () { + if (payUser == null) { + payTypeC.doRecognition(); + } else { + payTypeC.doSubmit(); + } + }, + ).flexible(flex: 1), + SizedBox(width: 20.w), + FButton( + style: FButtonStyle.destructive, + label: Text( + "取消", + style: theme.typography.xl.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () { + payTypeC.doCancel(); + }, + ).flexible(flex: 1), + ].toRow(), + ], + ), + ); +} diff --git a/lib/common/widgets/order/steelyard_container.dart b/lib/common/widgets/order/steelyard_container.dart new file mode 100644 index 0000000..0501c98 --- /dev/null +++ b/lib/common/widgets/order/steelyard_container.dart @@ -0,0 +1,142 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SteelyardController extends GetxController { + /// **************** start 回调函数 start **************** + + // |开始称重|的回调 + VoidCallback? onWeightStart; + + // |确定重量|的回调 + VoidCallback? onWeightSubmit; + + // |取消|的回调 + VoidCallback? onWeightCancel; + + /// **************** end 回调函数 end **************** + + static const _tag = "称重视图组件"; + + final app = Get.find(); + + // 称重商品 + Rxn _widgetProduct = Rxn(null); + + // 重量 + RxDouble widget = RxDouble(0); + + // 回调的重量 + RxDouble callbackWidget = RxDouble(0); + + // 价格 + RxInt price = RxInt(0); + + // 是否开始称重 + RxBool isWidget = RxBool(false); + + // 点击|开始称重| + void weightStartCallback() { + if (isWidget.value) { + Toast.error(msg: "正在称重中,请勿重复点击"); + return; + } + widget.value = 0; + callbackWidget.value = 0; + price.value = 0; + isWidget.value = true; + _listenSteelyard(); + onWeightStart?.call(); + } + + // 点击|确定重量| + void weightSubmitCallback() { + isWidget.value = false; + _closeSerial(); + onWeightSubmit?.call(); + } + + // 点击|取消| + void weightCancelCallback() { + isWidget.value = false; + _closeSerial(); + onWeightCancel?.call(); + } + + // 打开称重 + bool open(ProductEntity current) { + final isOpen = _openSerial(); + if (!isOpen) { + _closeStatus(); + return false; + } + _closeStatus(); + logger.i("|$_tag|称重,商品uniqueCode:|${current.serviceUniqueCode}|"); + _widgetProduct.value = current; + return true; + } + + void close() { + _closeStatus(); + _closeSerial(); + } + + // 清除状态 + _closeStatus() { + _widgetProduct.value = null; + widget.value = 0; + callbackWidget.value = 0; + price.value = 0; + isWidget.value = false; + } + + // 打开称串口 + bool _openSerial() { + if (app.srialSteelyardManage.isConnect) { + app.srialSteelyardManage.close(); + } + app.srialSteelyardManage.connect(); + if (app.srialSteelyardManage.isConnect) { + logger.i("|$_tag|打开称串口成功"); + return true; + } + logger.e("|$_tag|打开称串口失败"); + return false; + } + + // 关闭串口 + void _closeSerial() { + app.srialSteelyardManage.close(); + if (app.srialSteelyardManage.isConnect) { + logger.e("|$_tag|关闭串口失败"); + return; + } + logger.i("|$_tag|关闭串口成功"); + } + + // 监听串口 + void _listenSteelyard() { + app.srialSteelyardManage.readerClose(); + app.srialSteelyardManage.listen((event) { + final w = app.srialSteelyardManage.parseWidget(event); + if (ObjectUtil.isNotEmpty(w)) { + callbackWidget.value = w!; + } + }); + interval( + callbackWidget, + (callback) { + if (_widgetProduct.value != null) { + widget.value = callbackWidget.value; + final p = + callbackWidget.value * _widgetProduct.value!.price / 100 * 100; + price.value = int.parse(p.toStringAsFixed(2)); + logger.i( + "|$_tag|称重,重量:|${widget.value}|;价格:|${_widgetProduct.value!.price}|"); + } + }, + time: const Duration(seconds: 1), + ); + } +} diff --git a/lib/common/widgets/toast/toast_widget.dart b/lib/common/widgets/toast/toast_widget.dart new file mode 100644 index 0000000..db5bc9c --- /dev/null +++ b/lib/common/widgets/toast/toast_widget.dart @@ -0,0 +1,103 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +class Toast { + static Widget _successTotal({ + required BuildContext context, + String msg = "操作成功", + }) { + final theme = FTheme.of(context); + List widgets = [ + const Icon( + Icons.error, + size: 35, + color: Colors.black, + ), + SizedBox(width: 10.w), + Text(msg, style: theme.typography.xl.copyWith(color: Colors.black)), + ]; + return Container( + padding: EdgeInsets.symmetric(horizontal: 30.w, vertical: 15.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Colors.greenAccent, + ), + child: widgets.toRow( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + ), + ); + } + + static Widget _errorTotal({ + required BuildContext context, + String msg = "操作失败", + }) { + final theme = FTheme.of(context); + List widgets = [ + const Icon( + Icons.error, + size: 35, + color: Colors.white, + ), + SizedBox(width: 10.w), + Text(msg, style: theme.typography.xl.copyWith(color: Colors.white)), + ]; + return Container( + padding: EdgeInsets.symmetric(horizontal: 30.w, vertical: 15.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Colors.red, + ), + child: widgets.toRow( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + ), + ); + } + + static Future base({ + required String msg, + Color backgroundColor = Colors.black, + Color textColor = Colors.white, + ToastGravity gravity = ToastGravity.TOP, + }) { + return Fluttertoast.showToast( + msg: msg, + gravity: gravity, + timeInSecForIosWeb: 1, + backgroundColor: backgroundColor, + textColor: textColor, + fontSize: 16.sp, + ); + } + + static success({String msg = "操作成功"}) { + final context = Get.context!; + final fToast = FToast(); + fToast.init(context); + fToast.showToast( + gravity: ToastGravity.TOP, + child: _successTotal( + context: context, + msg: msg, + ), + ); + } + + static error({String msg = "操作失败"}) { + final context = Get.context!; + final fToast = FToast(); + fToast.init(context); + fToast.showToast( + gravity: ToastGravity.TOP, + child: _errorTotal( + context: context, + msg: msg, + ), + ); + } +} diff --git a/lib/common/widgets/user/user_info.dart b/lib/common/widgets/user/user_info.dart new file mode 100644 index 0000000..2f42552 --- /dev/null +++ b/lib/common/widgets/user/user_info.dart @@ -0,0 +1,39 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/forui.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +Widget userInfo( + BuildContext context, { + dynamic username = "未知", + dynamic balance = "未知", + String? avatar = "", +}) { + if (ObjectUtil.isEmpty(username)) { + username = "未知"; + } + if (ObjectUtil.isEmpty(balance)) { + balance = "未知"; + } + if (ObjectUtil.isEmpty(avatar)) { + avatar = ""; + } + final theme = FTheme.of(context); + final style = theme.typography.lg; + return [ + ObjectUtil.isNotEmpty(avatar) + ? appImg(avatar) + : Image.asset( + AssetsImages.defaultAvatar, + fit: BoxFit.cover, + width: 80.w, + height: 80.w, + ), + SizedBox(width: 10.w), + [ + Text("姓名:$username", style: style), + Text("饭卡余额:$balance", style: style), + ].toColumn(crossAxisAlignment: CrossAxisAlignment.start), + ].toRow(mainAxisAlignment: MainAxisAlignment.center).width(double.infinity); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..d01a630 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,34 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_subscreen_plugin/flutter_subscreen_plugin.dart'; +import 'package:get/get.dart'; +import 'package:pj_supermarket_cloud/app.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + await checkSubScreen(); +} + +Future checkSubScreen() async { + switch (window.defaultRouteName) { + case "/": + runApp(MainApp()); + final app = Get.put(AppControl()); + await app.initAsync(); + break; + case "subMain": + await Future(() async { + bool isMultipleScreen = await SubScreenPlugin.isMultipleScreen; + bool isOverlay = await SubScreenPlugin.checkOverlayPermission; + if (isMultipleScreen && isOverlay) { + SubScreenPlugin.doubleScreenShow(); + } + // runApp(SubApp()); + }); + break; + } +} diff --git a/lib/pages/canteen_product/controller.dart b/lib/pages/canteen_product/controller.dart new file mode 100644 index 0000000..5b2ccaa --- /dev/null +++ b/lib/pages/canteen_product/controller.dart @@ -0,0 +1,302 @@ +import 'dart:async'; + +import 'package:common_utils/common_utils.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_subscreen_plugin/flutter_subscreen_plugin.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class CanteenProductController extends GetxController with OrderMixin { + CanteenProductController(); + + static const _tag = "食堂购买页面"; + + final app = Get.find(); + + StreamSubscription? mqUpdateListen; + + _initData() { + update(["canteen_product"]); + } + + void onTap() {} + + @override + void onInit() async { + super.onInit(); + EasyLoading.dismiss(); + EasyLoading.show( + status: '初始化资源...', + maskType: EasyLoadingMaskType.black, + ); + + // 打开副屏 + SubScreenPlugin.doubleScreenShow(); + SubScreenPlugin.sendMsgToViceScreen(SubScreenAction.jumpSubPage.name); + + /// **************** start 组件初始化 start **************** + hourType.value = app.hourType.value; + sysType = SysType.canteen; + // 重置状态 + closeStatus(); + _listenPayTypeSelectCallback(); + + /// **************** end 组件初始化 end **************** + + /// **************** start 获取分类和商品 start **************** + await getClassify(); + await getProduct(); + + /// **************** end 获取分类和商品 end **************** + + EasyLoading.dismiss(); + } + + @override + void onReady() { + super.onReady(); + if (hourType.value != app.hourType.value) { + app.hourType.value = app.hourType.value; + } + _initData(); + } + + @override + void onClose() { + super.onClose(); + SubScreenPlugin.sendMsgToViceScreen(SubScreenAction.jumpSubBack.name); + // 读卡器 + app.serialCardManage.readerClose(); + app.serialCardManage.close(); + // 二维码读头 + app.scanCodeManage.close(); + // 路由监听 + routerBus?.cancel(); + // 重置状态 + closeStatus(); + mqUpdateListen?.cancel(); + } + + // |下单|点击识别按钮 + doRecognition() { + final payTypeC = Get.find(); + payTypeC.payUser.value = payUser.value; + payTypeC.orderPayDisplayType.value = orderPayDisplayType.value; + // payTypeSelectDialog(); + } + + // |下单|点击购买 + doPay() async { + final validShopCar = _validateShopCar(); + if (!validShopCar) return; + // 创建本地订单 + await createSqlOrder(); + final payTypeC = Get.find(); + payTypeC.payUser.value = payUser.value; + payTypeC.orderPayDisplayType.value = orderPayDisplayType.value; + selectPayTypeDialog(Get.context!); + } + + // |下单|点击取消下单 + Future doCancelPay({bool cancelShop = true}) async { + if (cancelShop) await _clearShopCard(); + sendSubScreen(); + } + + // |+|或者|-|点击更改商品数量 + void doComputedInputChange(int number, int index) async { + final current = shopCardList[index]; + current.productNumber = number; + current.totalPrice = _computedProductTotalPrice( + current.productPrice, + current.productNumber, + ); + if (current.productNumber == 0) { + shopCardList.removeWhere((e) { + return e.productServiceUniqueCode == current.productServiceUniqueCode; + }); + } + shopCardList.refresh(); + computedTotalPrice(); + await _computedAmountMealPrice(); + sendSubScreen(); + } + + // 点击商品 + void doProduct(ProductEntity current) async { + final target = shopCardList.firstWhereOrNull((e) { + return e.productServiceUniqueCode == current.serviceUniqueCode; + }); + if (ObjectUtil.isEmpty(target)) { + final data = ShopCardEntity( + orderId: -1, + productServiceUniqueCode: current.serviceUniqueCode, + productName: current.name, + productPic: current.image ?? "", + productPrice: current.price, + productNumber: 1, + productWidget: 0, + totalPrice: current.price, + ); + shopCardList.add(data); + } else { + // 旧商品 + target!.productNumber += 1; + target.totalPrice = _computedProductTotalPrice( + target.productPrice, + target.productNumber, + ); + } + shopCardList.refresh(); + computedTotalPrice(); + await _computedAmountMealPrice(); + sendSubScreen(); + } + + // |早餐、中餐、晚餐|点击确定 + hourTypeDoSubmit(ProductHourType hourType) async { + EasyLoading.show( + status: '切换中...', + maskType: EasyLoadingMaskType.black, + ); + this.hourType.value = hourType; + app.hourType.value = hourType; + await getClassify(); + await getProduct(); + await _clearShopCard(); + EasyLoading.dismiss(); + } + + void _listenPayTypeSelectCallback() { + final payTypeSelect = Get.find(); + // |支付方式|点击|开始识别| + payTypeSelect.onRecognition = () async { + // await createSqlOrder(); + }; + // 识别出用户的回调 + payTypeSelect.onDiscernUser = (payUser) { + if (payUser == null) return; + this.payUser.value = payUser; + sendSubScreen(); + }; + // |支付方式|点击|确定| + payTypeSelect.onSubmit = ( + user, + orderType, + orderPayType, + orderPayDisplayType, + qrCode, + ) async { + final log = + "|$_tag|回调,识别到用户:|${user.toJson()}|orderType:|$orderType|;orderPayType:|$orderPayType|;orderPayDisplayType|$orderPayDisplayType|;"; + logger.i(log); + this.orderType = orderType; + this.orderPayType.value = orderPayType; + this.orderPayDisplayType.value = orderPayDisplayType; + this.globalQrCode = qrCode; + payUser.value = user; + payUser.refresh(); + final validPayUser = _validatePayUser(); + if (!validPayUser) return; + final validShopCar = _validateShopCar(); + if (!validShopCar) return; + EasyLoading.show( + status: '支付中...', + maskType: EasyLoadingMaskType.black, + ); + // 最后计算一次额外的价格 + await _computedAmountMealPrice(); + await updateSqlOrder(); + await createApiOrder(); + _clearShopCard(); + putOffSendSubScreen(); + }; + // |支付方式|点击|取消| + payTypeSelect.onCancel = () {}; + } + + bool _validatePayUser() { + bool flag = true; + if (ObjectUtil.isEmpty(payUser.value?.walletBalance)) { + speeching("用户钱包不存在"); + Toast.error(msg: "校验购物车失败,用户钱包不存在"); + return false; + } + if (payUser.value!.walletBalance! < totalPrice.value) { + speeching("用户余额不足"); + Toast.error(msg: "校验购物车失败,用户余额不足"); + return false; + } + return flag; + } + + bool _validateShopCar() { + bool flag = false; + for (final v in shopCardList) { + if (v.productNumber > 0) { + flag = true; + break; + } + } + if (!flag) { + Toast.error(msg: "校验购物车失败,未选择饭菜"); + return false; + } + return flag; + } + + // 计算额外的价格 + Future _computedAmountMealPrice() async { + final price = await getAmountMealPrice( + price: totalPrice.value, + ); + if (ObjectUtil.isNotEmpty(price)) { + amountMealPrice.value = int.parse(price!.toString()); + sendSubScreen(); + } + } + + Future _clearShopCard() async { + shopCardList.clear(); + payUser.value = null; + totalPrice.value = 0; + amountMealPrice.value = 0; + await _getDefaultProduct(); + } + + // 获取默认商品 + Future _getDefaultProduct() async { + final appSetting = await AppSettingTool.getSysSetting(); + final sysType = appSetting.sysType.name; + final productDao = app.dataBase.getProductDao; + List list = await productDao.getDefault( + sysType, + hourType.value.code, + ); + if (ObjectUtil.isEmptyList(list)) return; + for (final ProductEntity v in list) { + final shopCard = shopCardList.firstWhereOrNull((i) { + return int.tryParse(i.productServiceUniqueCode) == v.id; + }); + if (shopCard != null) continue; + final data = ShopCardEntity( + orderId: -1, + productServiceUniqueCode: v.serviceUniqueCode, + productName: v.name, + productPic: v.image ?? "", + productPrice: v.price, + productNumber: 1, + productWidget: 0, + totalPrice: v.price, + ); + shopCardList.add(data); + } + computedTotalPrice(isSendSubScreen: false); + } + + // 计算单个的价格 + int _computedProductTotalPrice(int price, int number) { + return price * number; + } +} diff --git a/lib/pages/canteen_product/index.dart b/lib/pages/canteen_product/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/canteen_product/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/canteen_product/view.dart b/lib/pages/canteen_product/view.dart new file mode 100644 index 0000000..5730e69 --- /dev/null +++ b/lib/pages/canteen_product/view.dart @@ -0,0 +1,272 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +import 'index.dart'; + +class CanteenProductPage extends GetView { + const CanteenProductPage({super.key}); + + // 头部 + Widget _buildHeader(BuildContext context, double headerHeight) { + final theme = FTheme.of(context); + return [ + FIcon( + FAssets.icons.chevronLeft, + size: theme.typography.xl4.fontSize, + ).alignLeft().width(headerHeight).onTap(() => Get.back()), + Obx(() => controller.classifyList + .map((v) { + final currentIndex = controller.currentTabIndex.value; + final target = controller.classifyList[currentIndex]; + final active = target.id == v.id; + return FButton( + style: active ? FButtonStyle.primary : FButtonStyle.secondary, + label: Text( + v.name, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.typography.xl.copyWith( + color: active + ? theme.colorScheme.secondary + : theme.colorScheme.primary, + ), + ).center().width(120.w), + onPress: () => controller.doTab( + controller.classifyList.indexWhere((i) => i.id == v.id), + ), + ); + }) + .toList() + .toRowSpace(space: 20.w)) + .scrollable(scrollDirection: Axis.horizontal) + .expanded(), + appRadioHourTypeDialog( + context, + (hourType) => controller.hourTypeDoSubmit(hourType), + ).width(300.w), + ].toRow(); + } + + // 主体部分 + Widget _buildMain(BuildContext context) { + final width = context.width; + final theme = FTheme.of(context); + return [ + _buildProduct(context).expanded(), + SizedBox(width: 15.w), + _buildUser(context).width(width * 0.25).decorated( + border: Border.all( + color: theme.colorScheme.primary, + width: 1, + ), + borderRadius: theme.style.borderRadius, + ), + ].toRow(); + } + + // 商品 + Widget _buildProduct(BuildContext context) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + const spacing = 10; + const runSpacing = 10; + double maxHeight = constraints.maxHeight; + double maxWidth = constraints.maxWidth; + double itemWidth = (maxWidth - spacing * 5) / 6; + double itemHeight = (maxHeight - (runSpacing + 40) * 3) / 3; + return Obx( + () => ListView.builder( + padding: EdgeInsets.zero, + itemCount: controller.productClassifyList.length, + itemBuilder: (context, index) { + if (ObjectUtil.isEmpty(controller.productClassifyList) || + ObjectUtil.isEmpty(controller.productList)) { + return const SizedBox.shrink(); + } + final current = controller.productClassifyList[index]; + final currentStr = controller.classifyList + .firstWhereOrNull((v) => v.serviceUniqueCode == current); + final currentList = controller.productList + .where((i) => i.classifyServiceUniqueCode == current) + .toList(); + if (ObjectUtil.isEmpty(currentList)) { + return const SizedBox.shrink(); + } + return _buildProductItem( + context, + productList: currentList, + productSpacing: spacing.toDouble(), + productSize: Size(itemWidth, itemHeight), + title: currentStr?.name, + ); + }, + ), + ).height(maxHeight); + }, + ); + } + + /// 商品项 + Widget _buildProductItem( + BuildContext context, { + required List productList, + required double productSpacing, + required Size productSize, + String? title = "", + }) { + final theme = FTheme.of(context); + return [ + ObjectUtil.isNotEmpty(title) + ? Text( + title!, + textAlign: TextAlign.center, + style: theme.typography.lg.copyWith( + color: theme.colorScheme.secondary, + ), + ) + .center() + .width(double.infinity) + .padding(vertical: 1.h) + .decorated( + color: theme.colorScheme.mutedForeground, + borderRadius: theme.style.borderRadius / 2, + ) + .marginOnly(bottom: 20.h) + : const SizedBox.shrink(), + productList + .map( + (v) => orderProductCard( + context, + image: v.image, + name: v.name, + price: v.price, + ) + .width(productSize.width) + .height(productSize.height) + .onTap(() => controller.doProduct(v)), + ) + .toList() + .toWrap( + spacing: productSpacing, + runSpacing: productSpacing, + ) + .marginOnly(bottom: 20.h) + ].toColumn( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + ); + } + + // 用户信息 + Widget _buildUser(BuildContext context) { + final theme = FTheme.of(context); + return [ + [ + Obx(() { + final payUser = controller.payUser.value; + return userInfo( + context, + username: payUser?.username, + balance: payUser?.walletBalance, + avatar: payUser?.userAvatar, + ); + }).width(400.w), + ].toRow(mainAxisAlignment: MainAxisAlignment.center), + SizedBox(height: 10.h), + FDivider( + style: theme.dividerStyles.verticalStyle.copyWith( + color: theme.colorScheme.primary, + padding: EdgeInsets.zero, + ), + ), + Text( + "已点菜品", + style: theme.typography.xl2, + ).alignLeft(), + Obx( + () => ListView.builder( + padding: EdgeInsets.zero, + itemCount: controller.shopCardList.length, + itemBuilder: (context, index) { + final current = controller.shopCardList[index]; + return orderSelectPanel( + context, + label: current.productName, + price: current.productPrice, + total: current.productNumber, + index: index, + onChange: (number, i) => + controller.doComputedInputChange(number, i), + ).marginOnly(bottom: 10.h); + }, + ), + ).expanded(), + orderAllPrice( + context, + totalPrice: controller.totalPrice.value, + amountMealPrice: controller.amountMealPrice.value, + ), + [ + SizedBox(height: 20.h), + [ + FButton( + style: FButtonStyle.destructive, + label: Text( + "取消", + style: theme.typography.xl.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () => controller.doCancelPay(), + ).flexible(flex: 1), + SizedBox(width: 20.w), + FButton( + label: Text( + "支付", + style: theme.typography.xl.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () => controller.payUser.value?.userId == null + ? controller.doPay() + : controller.doRecognition(), + ).flexible(flex: 1), + ].toRow(mainAxisAlignment: MainAxisAlignment.spaceBetween), + ].toColumn(), + ].toColumn().padding(vertical: 10.h, horizontal: 10.w); + } + + // 主视图 + Widget _buildView(BuildContext context) { + return [ + _buildMain(context).marginOnly(bottom: 20.h).expanded(), + ].toColumn(); + } + + @override + Widget build(BuildContext context) { + final height = context.height; + final headerHeight = height * 0.08; + return GetBuilder( + init: CanteenProductController(), + id: "canteen_product", + builder: (_) { + return FScaffold( + header: _buildHeader( + context, + headerHeight, + ) + .height(headerHeight) + .padding(vertical: 30.h, horizontal: 10.w) + .decorated(), + content: _buildView(context), + ); + }, + ); + } +} diff --git a/lib/pages/card_active/controller.dart b/lib/pages/card_active/controller.dart new file mode 100644 index 0000000..4f5a310 --- /dev/null +++ b/lib/pages/card_active/controller.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +class CardActiveController extends GetxController { + CardActiveController(); + + _initData() { + update(["card_active"]); + } + + void onTap() {} + + // @override + // void onInit() { + // super.onInit(); + // } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + // @override + // void onClose() { + // super.onClose(); + // } +} diff --git a/lib/pages/card_active/index.dart b/lib/pages/card_active/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/card_active/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/card_active/view.dart b/lib/pages/card_active/view.dart new file mode 100644 index 0000000..e2b91f7 --- /dev/null +++ b/lib/pages/card_active/view.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'index.dart'; + +class CardActivePage extends GetView { + const CardActivePage({super.key}); + + // 主视图 + Widget _buildView() { + return const Center( + child: Text("CardActivePage"), + ); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: CardActiveController(), + id: "card_active", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/index.dart b/lib/pages/index.dart new file mode 100644 index 0000000..5b28622 --- /dev/null +++ b/lib/pages/index.dart @@ -0,0 +1,12 @@ +library; + +export './canteen_product/index.dart'; +export './card_active/index.dart'; +export './load/index.dart'; +export './login/index.dart'; +export './order_history/index.dart'; +export './order_history_detail/index.dart'; +export './panel/index.dart'; +export './setting/index.dart'; +export './supermarket_order/index.dart'; +export './take_food/index.dart'; diff --git a/lib/pages/load/controller.dart b/lib/pages/load/controller.dart new file mode 100644 index 0000000..f5d43e4 --- /dev/null +++ b/lib/pages/load/controller.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +class LoadController extends GetxController { + LoadController(); + + _initData() { + update(["load"]); + } + + void onTap() {} + + // @override + // void onInit() { + // super.onInit(); + // } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + // @override + // void onClose() { + // super.onClose(); + // } +} diff --git a/lib/pages/load/index.dart b/lib/pages/load/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/load/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/load/view.dart b/lib/pages/load/view.dart new file mode 100644 index 0000000..a004de4 --- /dev/null +++ b/lib/pages/load/view.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'index.dart'; + +class LoadPage extends GetView { + const LoadPage({super.key}); + + // 主视图 + Widget _buildView() { + return const Center( + child: Text("LoadPage"), + ); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: LoadController(), + id: "load", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart new file mode 100644 index 0000000..e416b6c --- /dev/null +++ b/lib/pages/login/controller.dart @@ -0,0 +1,122 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class LoginController extends GetxController { + static const String _tag = "登录页面 Controller"; + + // 用户名 + final usernameC = TextEditingController(); + // 密码 + final passwordC = TextEditingController(); + + _initData() { + update(["login"]); + } + + @override + void onInit() async { + super.onInit(); + await autoLogin(); + } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + Future doLogin(bool visibleMsg) async { + if (!_validateForm()) return; + SysSetting sysSetting = await AppSettingTool.getSysSetting(); + final pa = { + 'username': usernameC.value.text, + 'password': passwordC.value.text, + 'sysType': sysSetting.sysType.name, + "grant_type": 'password', + "scope": 'server', + }; + if (visibleMsg) { + EasyLoading.show( + status: '登录中...', + maskType: EasyLoadingMaskType.black, + ); + } + try { + final data = await UserApi.login(pa); + if (data == null) { + if (visibleMsg) EasyLoading.dismiss(); + return; + } + final userSetting = await AppSettingTool.getUserSetting(); + userSetting.userId = data['user_id']; + userSetting.accessToken = data['access_token']; + userSetting.refreshToken = data['refresh_token']; + userSetting.expiresTime = int.tryParse("${data['exp']}"); + userSetting.sysType = sysSetting.sysType; + userSetting.username = usernameC.value.text; + userSetting.password = passwordC.value.text; + await AppSettingTool.setUserSetting(userSetting); + if (visibleMsg) { + Toast.success(msg: "登录成功"); + EasyLoading.dismiss(); + } + Get.offAndToNamed(RouteNames.panel); + _clearForm(); + } catch (e) { + logger.d("|$_tag|登录错误error:|$e|"); + EasyLoading.dismiss(); + } + Get.offAllNamed(RouteNames.panel); + } + + Future autoLogin() async { + final userSetting = await AppSettingTool.getUserSetting(); + if (ObjectUtil.isEmpty(userSetting.username) || + ObjectUtil.isEmpty(userSetting.password) || + ObjectUtil.isEmpty(userSetting.sysType?.name)) { + return; + } + final sysSetting = await AppSettingTool.getSysSetting(); + usernameC.text = userSetting.username!; + passwordC.text = userSetting.password!; + // 无网络并且绑定过直接使用 + final isNetwork = AppNetworkTool.isAutoNetwork(); + final serviceId = sysSetting.serviceId; + if (!isNetwork && serviceId != null) { + Toast.success(msg: "登录成功"); + EasyLoading.dismiss(); + Get.offAndToNamed(RouteNames.panel); + _clearForm(); + return; + } + if (!isNetwork && serviceId == null) { + Toast.error(msg: "未绑定机器,不能开启离线模式"); + return; + } + await doLogin(true); + } + + bool _validateForm() { + final username = usernameC.value.text; + final password = passwordC.value.text; + if (ObjectUtil.isEmpty(username)) { + Toast.error(msg: "请输入账号"); + return false; + } + if (ObjectUtil.isEmpty(password)) { + Toast.error(msg: "请输入密码"); + return false; + } + return true; + } + + void _clearForm() { + usernameC.clear(); + passwordC.clear(); + } +} diff --git a/lib/pages/login/index.dart b/lib/pages/login/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/login/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart new file mode 100644 index 0000000..9e9c957 --- /dev/null +++ b/lib/pages/login/view.dart @@ -0,0 +1,67 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +import 'index.dart'; + +class LoginPage extends GetView { + const LoginPage({super.key}); + + // 主视图 + Widget _buildView() { + final theme = FTheme.of(Get.context!); + List widgets = [ + SizedBox(height: 30.h), + FTextField( + label: Text('用户名'), + controller: controller.usernameC, + hint: '请输入用户名', + ), + SizedBox(height: 30.h), + FTextField.password( + label: Text('密码'), + controller: controller.passwordC, + hint: '请输入密码', + ), + SizedBox(height: 50.h), + FButton( + label: Text( + '登录', + style: theme.typography.lg.copyWith(color: Colors.white), + ), + onPress: () => controller.doLogin(true), + ), + ]; + return SizedBox( + width: Get.context!.sizes.width * 0.3, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FCard( + title: appLogo(), + child: widgets.toColumn(), + ).center(), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: LoginController(), + id: "login", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/order_history/controller.dart b/lib/pages/order_history/controller.dart new file mode 100644 index 0000000..e221877 --- /dev/null +++ b/lib/pages/order_history/controller.dart @@ -0,0 +1,164 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class OrderHistoryController extends GetxController { + Rx hourType = Rx(ProductHourType.morning); + + DateTime startOrderCreateTime = DateTime.now().subtract( + const Duration(days: 7), + ); + + DateTime endOrderCreateTime = DateTime.now().add( + const Duration(days: 1), + ); + + RxInt currentTab = 0.obs; + + RxList tabList = [].obs; + + String orderStatus = "-1"; + + // table + RxInt currentPage = 1.obs; + + RxInt totalPage = 0.obs; + + RxInt pageSize = 10.obs; + + RxList orderList = RxList([]); + + _initData() { + update(["order_history"]); + } + + void onTap() {} + + @override + void onInit() async { + super.onInit(); + final app = Get.find(); + hourType.value = app.hourType.value; + // tabs + await getTableList(); + await getOrder(); + } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + @override + void onClose() { + super.onClose(); + } + + Future doPage({int? page}) async { + if (page != null) { + currentPage.value = page; + } + await getOrder(); + } + + // 订单列表 + Future getOrder() async { + final appSetting = await AppSettingTool.getSysSetting(); + final app = Get.find(); + final sysType = appSetting.sysType.name; + final orderDao = app.dataBase.getOrderDao; + final list = await orderDao.findAll( + orderStatus, + startOrderCreateTime, + endOrderCreateTime, + pageSize.value, + (currentPage.value - 1) * pageSize.value, + sysType, + hourType.value.code, + ); + final total = await orderDao.count( + orderStatus, + startOrderCreateTime, + endOrderCreateTime, + sysType, + hourType.value.code, + ); + if (total == null) { + totalPage.value = 1; + } else { + totalPage.value = (total / pageSize.value).ceil(); + } + orderList.value = list; + } + + Future hourTypeDoSubmit(ProductHourType hourType) async { + EasyLoading.show( + status: '切换中...', + maskType: EasyLoadingMaskType.black, + ); + this.hourType.value = hourType; + await getTableList(); + currentPage.value = 1; + await getOrder(); + EasyLoading.dismiss(); + } + + Future getTableList() async { + final total = await _getTotal(); + List tabs = OrderStatus.values + .map((v) => OrderHistoryTableItem(status: v)) + .toList(); + for (int i = 0; i < tabs.length; i++) { + final v = tabs[i]; + } + // final total = total[v.status.name]; + tabList.value = tabs; + } + +// 根据日期查询统计 + Future> _getTotal() async { + final appSetting = await AppSettingTool.getSysSetting(); + final app = Get.find(); + Map map = { + "PAY": 0, + "NONE_PAY": 0, + "OFFLINE_PAY": 0, + "REFUND": 0, + "TOTAL": 0, + }; + String totalSql = """ + SELECT + SUM(CASE WHEN status = 'PAY' THEN 1 ELSE 0 END) AS PAY, + SUM(CASE WHEN status = 'NONE_PAY' THEN 1 ELSE 0 END) AS NONE_PAY, + SUM(CASE WHEN status = 'OFFLINE_PAY' THEN 1 ELSE 0 END) AS OFFLINE_PAY, + SUM(CASE WHEN status = 'REFUND' THEN 1 ELSE 0 END) AS REFUND, + SUM(CASE WHEN 1 = 1 THEN 1 ELSE 0 END) AS TOTAL + FROM order_record + WHERE + sysType = ? + AND createTime BETWEEN ? AND ? + """; + final sysType = appSetting.sysType.name; + List? args = [ + sysType, + startOrderCreateTime.millisecondsSinceEpoch, + endOrderCreateTime.millisecondsSinceEpoch + ]; + // 组装where条件 + if (hourType.value.code != "-1") { + totalSql += " AND hourType = ?"; + args.add(hourType.value.code); + } + final list = await app.dataBase.database.rawQuery(totalSql, args); + if (!ObjectUtil.isEmptyList(list)) { + final map = list[0]; + if (ObjectUtil.isNotEmpty(map)) { + return map; + } + } + return map; + } +} diff --git a/lib/pages/order_history/index.dart b/lib/pages/order_history/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/order_history/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/order_history/view.dart b/lib/pages/order_history/view.dart new file mode 100644 index 0000000..4b31c96 --- /dev/null +++ b/lib/pages/order_history/view.dart @@ -0,0 +1,214 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; +import 'package:data_table_2/data_table_2.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +import 'index.dart'; + +class OrderHistoryPage extends GetView { + const OrderHistoryPage({super.key}); + + // 主视图 + Widget _buildView(BuildContext context) { + final height = context.height; + final headerHeight = height * 0.08; + return [ + _buildCondition(context, headerHeight), + SizedBox(height: 20.h), + _buildTable(context).center().expanded(), + appPage( + context, + pageNo: controller.currentPage.value, + pageSize: controller.pageSize.value, + total: controller.totalPage.value, + onPageChanged: (page) => controller.doPage(page: page), + ) + ].toColumn(); + } + + Widget _buildCondition(BuildContext context, double headerHeight) { + final theme = FTheme.of(context); + return [ + FIcon( + FAssets.icons.chevronLeft, + size: theme.typography.xl4.fontSize, + ).alignLeft().width(headerHeight).onTap(() => Get.back()), + Obx( + () => controller.tabList + .map((v) { + final active = false; + return FButton( + style: active ? FButtonStyle.primary : FButtonStyle.secondary, + label: Text( + "${v.status.title}(${v.total})", + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: theme.typography.lg.copyWith( + color: active + ? theme.colorScheme.secondary + : theme.colorScheme.primary, + ), + ).width(150.w), + onPress: () => {}, + ); + }) + .toList() + .toRowSpace(space: 20.w), + ).scrollable(scrollDirection: Axis.horizontal).expanded(), + appRadioHourTypeDialog( + context, + (hourType) => controller.hourTypeDoSubmit(hourType), + ).width(300.w), + ].toRow(); + } + + Widget _buildTable(BuildContext context) { + final theme = FTheme.of(context); + final titleTextStyle = theme.typography.lg; + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final height = constraints.maxHeight; + final hdHeight = (height - 50.h) / 10.5; + return DataTable2( + columnSpacing: 12.w, + horizontalMargin: 12.w, + minWidth: 600.w, + showBottomBorder: true, + headingTextStyle: theme.typography.lg, + dataTextStyle: theme.typography.base, + dataRowHeight: hdHeight, + columns: [ + DataColumn2( + label: Text("下单时间", textAlign: TextAlign.center), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 260.w, + ), + DataColumn2( + label: Text('订餐类型'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('支付类型'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('订单人', textAlign: TextAlign.center), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('菜品名称'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 220.w, + ), + DataColumn2( + label: Text('数量'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('菜品总价'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('额外支付'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('实际扣款'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + DataColumn2( + label: Text('操作'), + headingRowAlignment: MainAxisAlignment.center, + fixedWidth: 150.w, + ), + ], + rows: controller.orderList + .map( + (v) => DataRow(cells: [ + DataCell( + Text( + DateUtil.formatDate(v.createTime), + textAlign: TextAlign.center, + ).center(), + ), + DataCell( + Text( + v.type.title, + textAlign: TextAlign.center, + ).center(), + ), + DataCell( + Text( + v.payType.title, + textAlign: TextAlign.center, + ).center(), + ), + DataCell( + Text( + v.username ?? "未知用户", + textAlign: TextAlign.center, + ).center(), + ), + DataCell( + Text( + v.productDesc ?? "未知商品", + textAlign: TextAlign.center, + ).center(), + ), + DataCell( + Text( + "${v.count}", + textAlign: TextAlign.center, + ).center(), + ), + DataCell( + appPrice(context, v.price).center(), + ), + DataCell( + appPrice(context, v.extraPrice).center(), + ), + DataCell( + appPrice(context, v.payPrice).center(), + ), + DataCell( + Text( + v.payType.title, + textAlign: TextAlign.center, + ).center(), + ), + ]), + ) + .toList(), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: OrderHistoryController(), + id: "order_history", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(context), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/order_history_detail/controller.dart b/lib/pages/order_history_detail/controller.dart new file mode 100644 index 0000000..8623e05 --- /dev/null +++ b/lib/pages/order_history_detail/controller.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +class OrderHistoryDetailController extends GetxController { + OrderHistoryDetailController(); + + _initData() { + update(["order_history_detail"]); + } + + void onTap() {} + + // @override + // void onInit() { + // super.onInit(); + // } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + // @override + // void onClose() { + // super.onClose(); + // } +} diff --git a/lib/pages/order_history_detail/index.dart b/lib/pages/order_history_detail/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/order_history_detail/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/order_history_detail/view.dart b/lib/pages/order_history_detail/view.dart new file mode 100644 index 0000000..7050e39 --- /dev/null +++ b/lib/pages/order_history_detail/view.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'index.dart'; + +class OrderHistoryDetailPage extends GetView { + const OrderHistoryDetailPage({super.key}); + + // 主视图 + Widget _buildView() { + return const Center( + child: Text("OrderHistoryDetailPage"), + ); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: OrderHistoryDetailController(), + id: "order_history_detail", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/panel/controller.dart b/lib/pages/panel/controller.dart new file mode 100644 index 0000000..214633d --- /dev/null +++ b/lib/pages/panel/controller.dart @@ -0,0 +1,82 @@ +import 'package:get/get.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +class PanelController extends GetxController { + PanelController(); + + static const String _tag = "菜单页面 Controller"; + + RxList panelBtnList = [ + PanelItem(title: "购买", visible: false, type: PanelType.supermarketOrder), + PanelItem(title: "点餐", visible: false, type: PanelType.canteenOrder), + PanelItem(title: "取餐", visible: true, type: PanelType.takeFood), + PanelItem(title: "交易查询", visible: true, type: PanelType.orderHistory), + PanelItem(title: "开卡管理", visible: true, type: PanelType.cardActive), + PanelItem(title: "设置", visible: true, type: PanelType.setting), + PanelItem(title: "退出登录", visible: true, type: PanelType.logout), + ].obs; + + final canteenMenus = [ + PanelType.canteenOrder, + ]; + + final supermarketMenus = [ + PanelType.supermarketOrder, + ]; + + _initData() async { + await _updateMenus(); + update(["panel"]); + } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + void doBtn(PanelType type) { + switch (type) { + case PanelType.supermarketOrder: + Get.toNamed(RouteNames.supermarketProduct); + break; + case PanelType.canteenOrder: + Get.toNamed(RouteNames.canteenProduct); + break; + case PanelType.takeFood: + Get.toNamed(RouteNames.takeFood); + break; + case PanelType.orderHistory: + Get.toNamed(RouteNames.orderHistory); + break; + case PanelType.setting: + Get.toNamed(RouteNames.setting); + break; + case PanelType.cardActive: + Get.toNamed(RouteNames.cardActive); + break; + case PanelType.logout: + Get.offAndToNamed(RouteNames.login); + break; + default: + break; + } + } + + Future _updateMenus() async { + final sysSetting = await AppSettingTool.getSysSetting(); + final sysType = sysSetting.sysType; + for (final v in panelBtnList) { + if (sysType == SysType.canteen) { + if (canteenMenus.contains(v.type)) { + v.visible = true; + } + } else if (sysType == SysType.supermarket) { + if (supermarketMenus.contains(v.type)) { + v.visible = true; + } + } + } + panelBtnList.refresh(); + } +} diff --git a/lib/pages/panel/index.dart b/lib/pages/panel/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/panel/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/panel/view.dart b/lib/pages/panel/view.dart new file mode 100644 index 0000000..717c62d --- /dev/null +++ b/lib/pages/panel/view.dart @@ -0,0 +1,62 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +import 'index.dart'; + +class PanelPage extends GetView { + const PanelPage({super.key}); + + // 主视图 + Widget _buildView() { + final theme = FTheme.of(Get.context!); + return SizedBox( + width: Get.context!.sizes.width * 0.2, + height: double.infinity, + child: [ + appLogo(), + Obx( + () => controller.panelBtnList + .map((v) { + if (!v.visible) return SizedBox.shrink(); + return FButton( + label: Text( + v.title, + style: theme.typography.lg.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () => controller.doBtn(v.type), + ).marginOnly(bottom: 20.h); + }) + .toList() + .toColumn(), + ), + ].toColumnSpace( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + space: 20.h, + ), + ).center(); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: PanelController(), + id: "panel", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart new file mode 100644 index 0000000..af98dd7 --- /dev/null +++ b/lib/pages/setting/controller.dart @@ -0,0 +1,61 @@ +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class SettingController extends GetxController { + _initData() { + update(["setting"]); + } + + RxList menuList = [ + SettingMenu( + prefixIcon: FAssets.icons.bookCopy, + title: "基本信息", + type: SettingMenuType.baseInfo, + ), + SettingMenu( + prefixIcon: FAssets.icons.refreshCw, + title: "同步设置", + type: SettingMenuType.syncSetting, + ), + SettingMenu( + prefixIcon: FAssets.icons.japaneseYen, + title: "支付设置", + type: SettingMenuType.paySetting, + ), + SettingMenu( + prefixIcon: FAssets.icons.databaseBackup, + title: "资源备份", + type: SettingMenuType.backup, + ), + SettingMenu( + prefixIcon: FAssets.icons.scanFace, + title: "人脸管理", + type: SettingMenuType.faceSetting, + ), + SettingMenu( + prefixIcon: FAssets.icons.shoppingCart, + title: "商品管理", + type: SettingMenuType.productSetting, + ), + ].obs; + + Rx cuurentMenuType = SettingMenuType.baseInfo.obs; + + void onTap() {} + + @override + void onReady() { + super.onReady(); + _initData(); + } + + void doMenu(SettingMenu v) { + if (v.type == cuurentMenuType.value) { + return; + } + cuurentMenuType.value = v.type; + menuList.refresh(); + } +} diff --git a/lib/pages/setting/index.dart b/lib/pages/setting/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/setting/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart new file mode 100644 index 0000000..b5d56c4 --- /dev/null +++ b/lib/pages/setting/view.dart @@ -0,0 +1,120 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/models/index.dart'; +import 'package:pj_supermarket_cloud/pages/setting/widgets/index.dart'; + +import 'index.dart'; + +class SettingPage extends GetView { + const SettingPage({super.key}); + + // 主视图 + Widget _buildView() { + final context = Get.context!; + final width = Get.context!.sizes.width; + final theme = FTheme.of(context); + return [ + _buildMenu().width(width * 0.2).height(double.infinity), + FDivider( + axis: Axis.vertical, + style: theme.dividerStyles.verticalStyle.copyWith( + color: theme.colorScheme.primary, + ), + ), + Obx(() => _buildDetail(context, controller.cuurentMenuType.value)) + .marginOnly(left: 20.w) + .expanded(), + ].toRow().height(double.infinity); + } + + Widget _buildMenu() { + final theme = FTheme.of(Get.context!); + return Obx( + () => controller.menuList + .map((v) { + final isSelect = controller.cuurentMenuType.value == v.type; + return [ + FIcon( + v.prefixIcon, + color: isSelect + ? theme.colorScheme.secondary + : theme.colorScheme.primary, + ), + Text( + v.title, + style: theme.typography.xl.copyWith( + color: isSelect + ? theme.colorScheme.secondary + : theme.colorScheme.primary, + ), + ).paddingHorizontal(20.w), + isSelect + ? FIcon( + FAssets.icons.check, + color: theme.colorScheme.secondary, + ) + : SizedBox.shrink(), + ] + .toRow( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + ) + .decorated( + color: isSelect + ? theme.colorScheme.primary + : theme.colorScheme.secondary, + border: Border.all( + color: theme.colorScheme.primary, + width: 1, + ), + borderRadius: theme.style.borderRadius, + ) + .onTap(() => controller.doMenu(v)); + }) + .toList() + .toColumnSpace( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + space: 10.h, + ), + ); + } + + Widget _buildDetail(BuildContext context, SettingMenuType type) { + switch (type) { + case SettingMenuType.baseInfo: + return settingBaseInfo(context); + case SettingMenuType.syncSetting: + return settingSync(context); + case SettingMenuType.paySetting: + return settingPay(context); + case SettingMenuType.backup: + return settingBackup(context); + case SettingMenuType.faceSetting: + return settingFace(context); + case SettingMenuType.productSetting: + return settingProduct(context); + default: + return SizedBox.shrink(); + } + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: SettingController(), + id: "setting", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/setting/widgets/index.dart b/lib/pages/setting/widgets/index.dart new file mode 100644 index 0000000..d631498 --- /dev/null +++ b/lib/pages/setting/widgets/index.dart @@ -0,0 +1,8 @@ +library; + +export './setting_backup.dart'; +export './setting_baseInfo.dart'; +export './setting_face.dart'; +export './setting_pay.dart'; +export './setting_product.dart'; +export './setting_sync.dart'; diff --git a/lib/pages/setting/widgets/setting_backup.dart b/lib/pages/setting/widgets/setting_backup.dart new file mode 100644 index 0000000..5ecda61 --- /dev/null +++ b/lib/pages/setting/widgets/setting_backup.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:pj_supermarket_cloud/common/index.dart'; + +Widget settingBackup(BuildContext context) { + return FutureBuilder( + future: AppSettingTool.getSysSetting(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.shrink(); + } + final sys = snapshot.data!; + return [ + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.user), + suffixIcon: appSuffixIcon(context, FAssets.icons.files), + title: appPrefixTitle(context, "备份路径"), + details: appSuffixTitle(context, sys.externalPath), + onPress: () => KeywordTool.copy(sys.sysType.cnStr), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.user), + suffixIcon: appSuffixIcon(context, FAssets.icons.folderSync), + title: appPrefixTitle(context, "sql备份"), + onPress: () => KeywordTool.copy(sys.sysType.cnStr), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.user), + suffixIcon: appSuffixIcon(context, FAssets.icons.folderSync), + title: appPrefixTitle(context, "log备份"), + onPress: () => KeywordTool.copy(sys.sysType.cnStr), + ), + ].toColumnSpace(space: 20.h); + }); +} diff --git a/lib/pages/setting/widgets/setting_baseInfo.dart b/lib/pages/setting/widgets/setting_baseInfo.dart new file mode 100644 index 0000000..80aae17 --- /dev/null +++ b/lib/pages/setting/widgets/setting_baseInfo.dart @@ -0,0 +1,140 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:forui/forui.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +Future _bindServiceDialog(BuildContext context, SysSetting sys) async { + final theme = FTheme.of(context); + TextEditingController uniqueCodeC = TextEditingController(); + if (ObjectUtil.isNotEmpty(sys.serviceId)) { + uniqueCodeC.text = sys.serviceId ?? ""; + } else if (AppSysTool.isDev()) { + uniqueCodeC.text = "1896157295523049473"; + } + await showAdaptiveDialog( + context: context, + builder: (context) => FDialog( + direction: Axis.horizontal, + title: Text('绑定唯一编号', style: theme.typography.xl2), + body: FTextField( + controller: uniqueCodeC, + hint: "请输入设备唯一编号", + maxLines: 1, + ).width(context.width * 0.5).marginSymmetric(vertical: 20.h), + actions: [ + FButton( + style: FButtonStyle.outline, + label: Text('取消', style: theme.typography.base), + onPress: () => Get.back(), + ), + FButton( + label: Text( + '绑定', + style: theme.typography.base.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () async { + final code = uniqueCodeC.text; + if (ObjectUtil.isEmpty(code)) { + Toast.error(msg: "请输入设备唯一编号"); + return; + } + int? serverId = int.tryParse(code); + if (ObjectUtil.isEmpty(serverId)) { + Toast.error(msg: "服务id格式错误"); + return; + } + final isNetwork = AppNetworkTool.isAutoNetworkMsg(); + if (!isNetwork) return; + EasyLoading.show( + status: '绑定中...', + maskType: EasyLoadingMaskType.black, + ); + try { + final flag = await AppSettingTool.bindSys(serverId!); + EasyLoading.dismiss(); + if (flag) { + // 连接mq + await AppMqTool.mqRefresh(); + Toast.success(msg: "绑定成功"); + } + } catch (e, stack) { + logger.e("|绑定服务端设备信息error:|$e|stack:|$stack|"); + EasyLoading.dismiss(); + } + Get.back(); + }, + ) + ], + ), + ); +} + +Widget settingBaseInfo(BuildContext context) { + return FutureBuilder( + future: AppSettingTool.getSysSetting(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.shrink(); + } + final sys = snapshot.data!; + return [ + appDividerTitle(context, "基本信息"), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.files), + title: appPrefixTitle(context, "设备类型"), + details: appSuffixTitle(context, sys.sysType.cnStr), + onPress: () => KeywordTool.copy(sys.sysType.cnStr), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.files), + title: appPrefixTitle(context, "设备名称"), + details: appSuffixTitle(context, sys.serviceName), + onPress: () => KeywordTool.copy(sys.serviceName), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.files), + title: appPrefixTitle(context, "android id"), + details: appSuffixTitle(context, sys.androidId), + onPress: () => KeywordTool.copy(sys.androidId), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.audioLines), + title: appPrefixTitle(context, "唯一编号"), + details: appSuffixTitle(context, sys.serviceId), + onPress: () => _bindServiceDialog(context, sys), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.files), + title: appPrefixTitle(context, "商户编号"), + details: appSuffixTitle(context, sys.serviceMerchantId), + onPress: () => KeywordTool.copy(sys.serviceMerchantId), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.files), + title: appPrefixTitle(context, "商户部门编号"), + details: appSuffixTitle(context, sys.merchantDeptId), + onPress: () => KeywordTool.copy(sys.merchantDeptId), + ), + appDividerTitle(context, "主题样式"), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.audioLines), + title: appPrefixTitle(context, "字体大小"), + details: appSuffixTitle(context, sys.merchantDeptId), + onPress: () => KeywordTool.copy(sys.merchantDeptId), + ), + ].toColumnSpace(space: 20.h); + }); +} diff --git a/lib/pages/setting/widgets/setting_face.dart b/lib/pages/setting/widgets/setting_face.dart new file mode 100644 index 0000000..b535c46 --- /dev/null +++ b/lib/pages/setting/widgets/setting_face.dart @@ -0,0 +1,229 @@ +import 'dart:async'; + +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class FaceManageController extends GetxController { + static const String _tag = "人脸管理 Controller"; + + RxList faceList = RxList(); + + RxInt currentPage = RxInt(1); + + RxInt totalPage = RxInt(0); + + RxInt pageSize = RxInt(15); + + final TextEditingController usernameC = TextEditingController(); + + late StreamSubscription mqUpdateListen; + + @override + void onInit() async { + super.onInit(); + getFaceList(); + _listenMqCallback(); + } + + @override + void onClose() { + mqUpdateListen.cancel(); + usernameC.dispose(); + } + + Future getFaceList({bool isLoading = false}) async { + if (isLoading) { + EasyLoading.show( + status: '查询中...', + maskType: EasyLoadingMaskType.black, + ); + } + try { + final app = Get.find(); + final faceDao = app.dataBase.getFaceDao; + String userName = usernameC.text; + if (ObjectUtil.isEmpty(userName)) userName = "-1"; + final findCount = await faceDao.findCount(userName); + final list = await faceDao.findAll( + userName, + pageSize.value, + (currentPage.value - 1) * pageSize.value, + ); + if (findCount != null) { + totalPage.value = (findCount / pageSize.value).ceil(); + } + faceList.value = list; + } finally { + if (isLoading) { + Future.delayed(const Duration(seconds: 1), () { + EasyLoading.dismiss(); + }); + } + } + } + + // 同步人脸 + Future asyncFace({bool isAll = false}) async { + final isNetwork = AppNetworkTool.isAutoNetworkMsg(); + if (!isNetwork) return; + final setting = await AppSettingTool.getSysSetting(); + final serviceMerchantId = setting.serviceMerchantId; + if (ObjectUtil.isEmpty(serviceMerchantId)) { + Toast.error(msg: "请先绑定设备"); + return; + } + final isActive = await AppFaceTool.isActiveSdk(); + if (!isActive) { + Toast.error(msg: "人脸sdk激活失败"); + return; + } + final isSure = await appMessageDialog( + Get.context!, + "是否同步所有人脸,这个过程肯能比较长", + ); + if (!isSure) return; + EasyLoading.show( + status: '同步中...', + maskType: EasyLoadingMaskType.black, + ); + try { + // await SyncFaceTool.syncFace(isAll: isAll); + } finally { + currentPage.value = 1; + getFaceList(); + EasyLoading.dismiss(); + } + } + + Future delAllFace() async { + final isSure = await appMessageDialog( + Get.context!, + "是否删除全部人脸", + ); + if (!isSure) return; + EasyLoading.show( + status: '删除中...', + maskType: EasyLoadingMaskType.black, + ); + try { + final app = Get.find(); + final faceDao = app.dataBase.getFaceDao; + await faceDao.delAll(); + final bool = await AppFaceTool.removeFace(sampleId: "-1"); + logger.i(bool); + } finally { + Future.delayed(const Duration(seconds: 1), () { + getFaceList(); + EasyLoading.dismiss(); + }); + } + } + + _listenMqCallback() { + final app = Get.find(); + mqUpdateListen = + app.mqUpdateEventBus.on().listen((event) async { + switch (event) { + case MqtActionEnum.face: + getFaceList(isLoading: true); + break; + default: + } + }); + } + + Future doPage({int? page}) async { + if (page != null) currentPage.value = page; + await getFaceList(isLoading: true); + } +} + +/// 搜索 +Widget buildFaceSearch(BuildContext context) { + final faceC = Get.find(); + final theme = FTheme.of(context); + return [ + [ + FTextField( + controller: faceC.usernameC, + hint: '请输入姓名', + maxLines: 1, + ).width(300.w), + FButton( + label: Text( + '搜索', + style: theme.typography.base.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () => faceC.doPage(), + ), + ].toRowSpace(space: 10.w).expanded(), + [ + FButton( + label: Text( + '删除全部人脸', + style: theme.typography.base.copyWith( + color: theme.colorScheme.secondary, + ), + ), + style: FButtonStyle.destructive, + onPress: () => faceC.delAllFace(), + ), + FButton( + label: Text( + '同步本部门人脸', + style: theme.typography.base.copyWith( + color: theme.colorScheme.primary, + ), + ), + style: FButtonStyle.outline, + onPress: () => faceC.asyncFace(isAll: false), + ), + FButton( + label: Text( + '同步全部人脸', + style: theme.typography.base.copyWith( + color: theme.colorScheme.primary, + ), + ), + style: FButtonStyle.outline, + onPress: () => faceC.asyncFace(isAll: true), + ), + ].toRowSpace(space: 10.w), + ].toRow(); +} + +/// 列表 +Widget buildFaceList(context) { + return [].toRow(); +} + +/// 分页 +Widget buildFacePage(context) { + final faceC = Get.find(); + return appPage( + context, + pageNo: faceC.currentPage.value, + pageSize: faceC.pageSize.value, + total: faceC.totalPage.value * faceC.pageSize.value, + onPageChanged: (pageNo) => faceC.doPage(page: pageNo), + ); +} + +Widget settingFace(BuildContext context) { + Get.put( + FaceManageController(), + ); + return [ + buildFaceSearch(context), + buildFaceList(context).expanded(), + buildFacePage(context), + ].toColumn(); +} diff --git a/lib/pages/setting/widgets/setting_pay.dart b/lib/pages/setting/widgets/setting_pay.dart new file mode 100644 index 0000000..fd60489 --- /dev/null +++ b/lib/pages/setting/widgets/setting_pay.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +Widget settingPay(BuildContext context) { + return SizedBox.shrink(); +} diff --git a/lib/pages/setting/widgets/setting_product.dart b/lib/pages/setting/widgets/setting_product.dart new file mode 100644 index 0000000..1952477 --- /dev/null +++ b/lib/pages/setting/widgets/setting_product.dart @@ -0,0 +1,170 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:forui/forui.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:get/get.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +class ProductManageController extends GetxController { + static const _tag = "商品管理 Controller"; + + RxList productList = [].obs; + + ProductHourType hourType = ProductHourType.morning; + + RxInt currentPage = RxInt(1); + + RxInt totalPage = RxInt(0); + + RxInt pageSize = RxInt(24); + + @override + void onInit() async { + await initData(); + super.onInit(); + } + + Future initData() async { + await getProductList(); + if (ObjectUtil.isEmptyList(productList)) { + await SyncProductTool.updateDatabase(); + getProductList(); + } + } + + Future getProductList({bool isLoading = false}) async { + if (isLoading) { + EasyLoading.show( + status: '查询中...', + maskType: EasyLoadingMaskType.black, + ); + } + try { + await SyncProductTool.updateDatabase(); + final app = Get.find(); + final sysSetting = await AppSettingTool.getSysSetting(); + final productDao = app.dataBase.getProductDao; + final currnetHourType = hourType.code; + String sysType = sysSetting.sysType.name; + final findCount = await productDao.getCount( + sysType, + currnetHourType, + ); + if (findCount != null) { + totalPage.value = (findCount / pageSize.value).ceil(); + } + final list = await productDao.getAllPage( + "-1", + currnetHourType, + sysType, + pageSize.value, + (currentPage.value - 1) * pageSize.value, + ); + productList.value = list; + } finally { + if (isLoading) { + Future.delayed(const Duration(seconds: 1), () { + EasyLoading.dismiss(); + }); + } + } + } + + /// 搜索 + Future doPage({int? page}) async { + if (page != null) currentPage.value = page; + await getProductList(isLoading: true); + } + + Future doHourType(ProductHourType type) async { + if (hourType.code == type.code) return; + hourType = type; + await doPage(page: 1); + } +} + +/// 搜索条件 +Widget buildSearch(BuildContext context) { + final theme = FTheme.of(context); + final productC = Get.find(); + return [ + [ + appRadioHourTypeDialog( + context, + (v) => productC.doHourType(v), + hourType: productC.hourType, + ).width(300.w), + ].toRow().expanded(), + FButton( + label: Text( + '搜索', + style: theme.typography.base.copyWith( + color: theme.colorScheme.secondary, + ), + ), + onPress: () => productC.doPage(), + ), + ].toRow(); +} + +/// 列表 +Widget buildList(BuildContext context) { + final theme = FTheme.of(context); + final productC = Get.find(); + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, + mainAxisSpacing: 10.w, + crossAxisSpacing: 10.w, + childAspectRatio: 0.7, + ), + itemCount: productC.productList.length, + itemBuilder: (BuildContext context, int index) { + final v = productC.productList[index]; + return FCard( + style: theme.cardStyle.copyWith( + contentStyle: theme.cardStyle.contentStyle.copyWith( + padding: EdgeInsets.all(0), + ), + ), + image: appImg(v.image, width: double.infinity, height: 120.h), + title: Text(v.name, maxLines: 2, style: theme.typography.base).center(), + subtitle: appPrice(context, v.price.toString()) + .center() + .paddingOnly(top: 10.h), + child: [ + Text("是否默认", style: theme.typography.sm), + FSwitch( + value: true, + onChange: (value) {}, + ) + ].toRow(mainAxisAlignment: MainAxisAlignment.center), + ); + }, + ); +} + +/// 分页 +Widget buildPage(BuildContext context) { + final productC = Get.find(); + return appPage( + context, + pageNo: productC.currentPage.value, + pageSize: productC.pageSize.value, + total: productC.totalPage.value * productC.pageSize.value, + onPageChanged: (pageNo) => productC.doPage(page: pageNo), + ); +} + +Widget settingProduct(BuildContext context) { + Get.put( + ProductManageController(), + ); + return [ + buildSearch(context), + buildList(context).expanded(), + buildPage(context), + ].toColumnSpace(space: 10.h); +} diff --git a/lib/pages/setting/widgets/setting_sync.dart b/lib/pages/setting/widgets/setting_sync.dart new file mode 100644 index 0000000..f0dc783 --- /dev/null +++ b/lib/pages/setting/widgets/setting_sync.dart @@ -0,0 +1,84 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +Widget settingSync(BuildContext context) { + String dateFormat(DateTime? dataTime) { + if (dataTime == null) return ""; + return DateUtil.formatDate(dataTime); + } + + return FutureBuilder( + future: AppSettingTool.getSysSetting(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.shrink(); + } + final sys = snapshot.data!; + String getSyncClassifyTime() { + if (sys.sysType == SysType.canteen) { + return dateFormat(sys.canteenClassifySyncTime); + } else if (sys.sysType == SysType.supermarket) { + return dateFormat(sys.supermarketClassifySyncTime); + } else { + return "未同步"; + } + } + + String getSyncProductTime() { + if (sys.sysType == SysType.canteen) { + return dateFormat(sys.canteenProductSyncTime); + } else if (sys.sysType == SysType.supermarket) { + return dateFormat(sys.supermarketProductSyncTime); + } else { + return "未同步"; + } + } + + return [ + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.refreshCw), + title: appPrefixTitle(context, "同步设备信息"), + details: appSuffixTitle( + context, + "最后同步时间:${dateFormat(sys.syncTime)}", + ), + onPress: () => KeywordTool.copy(dateFormat(sys.syncTime)), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.refreshCw), + title: appPrefixTitle(context, "同步长链接"), + details: appSuffixTitle( + context, + "最后同步时间:${dateFormat(sys.mqSyncTime)}", + ), + onPress: () => KeywordTool.copy(dateFormat(sys.syncTime)), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.refreshCw), + title: appPrefixTitle(context, "同步分类"), + details: appSuffixTitle( + context, + "最后同步时间:${getSyncClassifyTime()}", + ), + onPress: () => KeywordTool.copy(dateFormat(sys.syncTime)), + ), + FTile( + prefixIcon: appPrefixIcon(context, FAssets.icons.settings), + suffixIcon: appSuffixIcon(context, FAssets.icons.refreshCw), + title: appPrefixTitle(context, "同步商品"), + details: appSuffixTitle( + context, + "最后同步时间:${getSyncProductTime()}", + ), + onPress: () => KeywordTool.copy(dateFormat(sys.syncTime)), + ), + ].toColumnSpace(space: 20.h); + }); +} diff --git a/lib/pages/supermarket_order/controller.dart b/lib/pages/supermarket_order/controller.dart new file mode 100644 index 0000000..275834e --- /dev/null +++ b/lib/pages/supermarket_order/controller.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +class SupermarketOrderController extends GetxController { + SupermarketOrderController(); + + _initData() { + update(["supermarket_order"]); + } + + void onTap() {} + + // @override + // void onInit() { + // super.onInit(); + // } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + // @override + // void onClose() { + // super.onClose(); + // } +} diff --git a/lib/pages/supermarket_order/index.dart b/lib/pages/supermarket_order/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/supermarket_order/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/supermarket_order/view.dart b/lib/pages/supermarket_order/view.dart new file mode 100644 index 0000000..ee51dcc --- /dev/null +++ b/lib/pages/supermarket_order/view.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'index.dart'; + +class SupermarketOrderPage extends GetView { + const SupermarketOrderPage({super.key}); + + // 主视图 + Widget _buildView() { + return const Center( + child: Text("SupermarketOrderPage"), + ); + } + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: SupermarketOrderController(), + id: "supermarket_order", + builder: (_) { + return FScaffold( + header: FHeader(title: SizedBox.shrink()), + content: _buildView(), + contentPad: true, + resizeToAvoidBottomInset: true, + ); + }, + ); + } +} diff --git a/lib/pages/take_food/controller.dart b/lib/pages/take_food/controller.dart new file mode 100644 index 0000000..c928cfb --- /dev/null +++ b/lib/pages/take_food/controller.dart @@ -0,0 +1,27 @@ +import 'package:get/get.dart'; + +class TakeFoodController extends GetxController { + TakeFoodController(); + + _initData() { + update(["take_food"]); + } + + void onTap() {} + + // @override + // void onInit() { + // super.onInit(); + // } + + @override + void onReady() { + super.onReady(); + _initData(); + } + + // @override + // void onClose() { + // super.onClose(); + // } +} diff --git a/lib/pages/take_food/index.dart b/lib/pages/take_food/index.dart new file mode 100644 index 0000000..b16b2f8 --- /dev/null +++ b/lib/pages/take_food/index.dart @@ -0,0 +1,4 @@ +library; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/take_food/view.dart b/lib/pages/take_food/view.dart new file mode 100644 index 0000000..25312a5 --- /dev/null +++ b/lib/pages/take_food/view.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; +import 'package:get/get.dart'; + +import 'index.dart'; + +class TakeFoodPage extends GetView { + const TakeFoodPage({super.key}); + + // 主视图 + Widget _buildView() { + return const Center( + child: Text("TakeFoodPage"), + ); + } + + @override + Widget build(BuildContext context) { + final theme = FTheme.of(context); + return GetBuilder( + init: TakeFoodController(), + id: "take_food", + builder: (_) { + return FScaffold( + header: FHeader.nested( + title: Text("取餐", style: theme.typography.xl3), + prefixActions: [ + FHeaderAction.back(onPress: () {}), + ], + ), + content: _buildView(), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..fdf364c --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1276 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.11.0" + android_id: + dependency: "direct main" + description: + name: android_id + sha256: "748ba5f93dd5c497e675d8eaa1404346ce4d1794464ea654576ff192d153b92a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.4" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.12" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.14" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.9.4" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.0" + common_utils: + dependency: "direct main" + description: + name: common_utils + sha256: c26884339b13ff99b0739e56f4b02090c84054ed9dd3a045435cd24e7b99c2c1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.3" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.8" + data_table_2: + dependency: "direct main" + description: + name: data_table_2 + sha256: "5a540a7b64809eb46b3809c67fc6faa29ca217864341294df66b1f0eb040c20e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.18" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.11" + decimal: + dependency: transitive + description: + name: decimal + sha256: "4140a688f9e443e2f4de3a1162387bf25e1ac6d51e24c9da263f245210f41440" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + dio: + dependency: "direct main" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + drago_pos_printer: + dependency: "direct main" + description: + name: drago_pos_printer + sha256: "3ad14d15872204541a964005005a535ecf78f052a9d295b1703fddebc3e50def" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + drago_usb_printer: + dependency: transitive + description: + name: drago_usb_printer + sha256: "08272fe48797fe513cbdd57747dfe2edb2814cc438f9623311ec5e2601fcd1ab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + ducafe_ui_core: + dependency: "direct main" + description: + name: ducafe_ui_core + sha256: "92ff9b05654e1b5ae2adbeac7227c669655da73f134bede75ad9aa1c505af904" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.6" + dylib: + dependency: transitive + description: + name: dylib + sha256: bf609b3eb6492a3309b3d1dbe8f83a4031de5535dd7686be33487051cc760bb0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.3" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.7" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + external_path: + dependency: "direct main" + description: + name: external_path + sha256: e6d70bd47ee25d164287ecab65426684825439c6a39ee1895c6193d65576ee13 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + floor: + dependency: "direct main" + description: + name: floor + sha256: "52a8eac2c8d274e7c0c54251226f59786bb5b749365a2d8537d8095aa5132d92" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + floor_annotation: + dependency: transitive + description: + name: floor_annotation + sha256: a40949580a7ab0eee572686e2d3b1638fd6bd6a753e661d792ab4236b365b23b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + floor_generator: + dependency: "direct dev" + description: + name: floor_generator + sha256: "1499b3ab878a807e6fbe6f140dc014124845cd1df3090a113aae5fa7577a1e77" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_easyloading: + dependency: "direct main" + description: + name: flutter_easyloading + sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.5" + flutter_libserialport: + dependency: "direct main" + description: + name: flutter_libserialport + sha256: d193b5ac819db6540d20cc8d20d9f5ec9e3396edb4d2c4a23c97863fa9132336 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" + flutter_subscreen_plugin: + dependency: "direct main" + description: + name: flutter_subscreen_plugin + sha256: b89bc8ba87956d441726557ece140ccea418ca4569e8f06e028e1017740ff6f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2+5" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.17" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.2.12" + forui: + dependency: "direct main" + description: + name: forui + sha256: "3ebbf39d57f0667afc47cbab86b0afe679bba2173a51afc21855053663dc5062" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.1+1" + forui_assets: + dependency: "direct main" + description: + name: forui_assets + sha256: df0f24b47ff0c3e16811bb4ad597b426b8e1a62fcf2fea7d95923ef4d6e8aa2c + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + gbk_codec: + dependency: transitive + description: + name: gbk_codec + sha256: "3af5311fc9393115e3650ae6023862adf998051a804a08fb804f042724999f61" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.0" + get: + dependency: "direct main" + description: + name: get + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.7.2" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + google_fonts: + dependency: transitive + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + html: + dependency: transitive + description: + name: html + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.5" + http: + dependency: "direct main" + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + icons_launcher: + dependency: "direct dev" + description: + name: icons_launcher + sha256: a7c83fbc837dc6f81944ef35c3756f533bb2aba32fcca5cbcdb2dbcd877d5ae9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + image: + dependency: "direct main" + description: + name: image + sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.3" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.1" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + libserialport: + dependency: transitive + description: + name: libserialport + sha256: "392e1592def65282429832ec66fa25e9e163d3b37716b97691482e2406720727" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0+1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3-main.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + mqtt_client: + dependency: "direct main" + description: + name: mqtt_client + sha256: "37aae360fac0b3322cb37267696c1535e1ad2984a13064d877273ffe4b16ca7a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.5.1" + network_info_plus: + dependency: transitive + description: + name: network_info_plus + sha256: "08f4166bbb77da9e407edef6322a33f87b18c0ca46483fb25606cb3d2bfcdd2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.3" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + nitrogen_flutter_svg: + dependency: transitive + description: + name: nitrogen_flutter_svg + sha256: "103313861b182fd4069bac896519b1ee64cbe1af0023d818f6c300b81c16853c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.0" + nitrogen_types: + dependency: transitive + description: + name: nitrogen_types + sha256: "5d4ec453aea14e34607bc6021ff1381192e8fcd8541bf103638baa4d013e668f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.0+1" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + number_pagination: + dependency: "direct main" + description: + name: number_pagination + sha256: "75d3a28616196e7c8df431d0fb7c48e811e462155f4cf3b5b4167b3408421327" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.6" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + pdf: + dependency: transitive + description: + name: pdf + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.11.3" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.1" + printing: + dependency: "direct main" + description: + name: printing + sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.14.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + simple_sparse_list: + dependency: transitive + description: + name: simple_sparse_list + sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4+6" + sqflite_common_ffi: + dependency: transitive + description: + name: sqflite_common_ffi + sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.4+4" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.4" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "91f47610aa54d8abf9d795a7b4e49b2a788f65d7493d5a68fbf180c3cbcc6f38" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.27.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + strings: + dependency: transitive + description: + name: strings + sha256: "482f1511d2cd8ab9f2c4cc148e6837be8a00c03b9a8eaedbc981a84d9c6305d8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + sugar: + dependency: transitive + description: + name: sugar + sha256: a0a97f1d3552a3ef4cbec7c6e62c7b52c151cd5c11e984e8311700a918f73dc5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.0+3" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.3" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0d99edbd2e74726bed2e4989713c8bec02e5581628e334d8c88c0271593fb402" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.8" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.16" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.3.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + win32: + dependency: transitive + description: + name: win32 + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.10.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..51ffed1 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,80 @@ +name: pj_supermarket_cloud +description: "A new Flutter project." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.6.0 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + # http请求 + dio: 5.7.0 + http: ^1.3.0 + # mq + mqtt_client: ^10.0.2 + + # ui框架 + ducafe_ui_core: ^1.0.6 + forui: ^0.9.1+1 + forui_assets: ^0.9.1 + # 吐司弹框 + fluttertoast: ^8.2.5 + # loading + flutter_easyloading: ^3.0.5 + # 图片处理 + image: ^4.3.0 + # 表格 + data_table_2: ^2.5.18 + # 分页 + number_pagination: ^1.1.6 + + + # 串口 + flutter_libserialport: 0.5.0 + # 状态管理、路由 + get: ^4.7.2 + # 双屏 + flutter_subscreen_plugin: 1.3.2+5 + # 数据库 + sqflite: 2.3.2 + floor: 1.4.2 + # json序列化 + json_annotation: ^4.9.0 + # 外部路径获取 + external_path: ^2.0.1 + # 获取唯一id + android_id: ^0.4.0 + # 监听网络状态 + connectivity_plus: ^6.0.3 + # 日志 + logger: ^2.3.0 + # 工具类 + common_utils: ^2.1.0 + # 判断两个对象相等 + equatable: ^2.0.7 + # 打印机 + drago_pos_printer: ^1.0.5 + printing: ^5.13.4 +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + # logo + icons_launcher: 3.0.0 + # orm + floor_generator: ^1.5.0 + # 自动生成代码 + build_runner: 2.4.14 + # json + json_serializable: 6.8.0 + +flutter: + + uses-material-design: true + + assets: + - assets/images/ + - assets/crt/ \ No newline at end of file