소스 검색

第一次提交

JaycePC 3 주 전
커밋
2c0e858432
100개의 변경된 파일6204개의 추가작업 그리고 0개의 파일을 삭제
  1. 11 0
      .gitignore
  2. 3 0
      app/.gitignore
  3. 123 0
      app/build.gradle
  4. BIN
      app/libs/HatomPlayerCore.jar
  5. BIN
      app/libs/anrwatchdog-1.4.0.jar
  6. BIN
      app/libs/cardlibrary-release.aar
  7. BIN
      app/libs/facedetector-1.0.0.aar
  8. BIN
      app/libs/flowlayout-1.0.0.aar
  9. BIN
      app/libs/lib-eyecoolfinger-v2.3.0.jar
  10. BIN
      app/libs/tbs_sdk_v4.3.0.165_20210628_103707.jar
  11. 21 0
      app/proguard-rules.pro
  12. 26 0
      app/src/androidTest/java/xn/xxp/ExampleInstrumentedTest.java
  13. 104 0
      app/src/main/AndroidManifest.xml
  14. 1 0
      app/src/main/assets/face_loading.json
  15. 12 0
      app/src/main/assets/simple_table.css
  16. BIN
      app/src/main/assets/songti.zip
  17. BIN
      app/src/main/ic_launcher-playstore.png
  18. 8 0
      app/src/main/java/core/event/RefreshEvent.kt
  19. 71 0
      app/src/main/java/core/ui/ActivityCollector.kt
  20. 95 0
      app/src/main/java/core/ui/activity/BaseActivity.java
  21. 122 0
      app/src/main/java/core/ui/activity/BaseCountDownActivity.java
  22. 342 0
      app/src/main/java/core/ui/activity/BaseSignActivity.kt
  23. 37 0
      app/src/main/java/core/ui/common/AbsUIDelegate.kt
  24. 26 0
      app/src/main/java/core/ui/common/IUIListener.kt
  25. 86 0
      app/src/main/java/core/ui/common/UIDelegateImpl.kt
  26. 121 0
      app/src/main/java/core/ui/dialog/RcBaseDialog.kt
  27. 87 0
      app/src/main/java/core/ui/fragment/RcBaseFragment.kt
  28. 37 0
      app/src/main/java/core/ui/fragment/RcLazyFragment.kt
  29. 169 0
      app/src/main/java/core/ui/fragment/RcRefreshFragment.kt
  30. 27 0
      app/src/main/java/core/ui/widget/NoScrollGridLayoutManager.kt
  31. 60 0
      app/src/main/java/core/ui/widget/SwipeTouchListener.kt
  32. 33 0
      app/src/main/java/core/ui/widget/SwipeViewPager.kt
  33. 115 0
      app/src/main/java/core/ui/widget/decoration/GridSpacingItemDecoration.java
  34. 148 0
      app/src/main/java/core/ui/widget/decoration/NoLastLineItemDecoration.java
  35. 68 0
      app/src/main/java/core/util/EscapeUnescape.java
  36. 29 0
      app/src/main/java/core/util/Extension.kt
  37. 39 0
      app/src/main/java/core/util/FastClickDelegate.kt
  38. 58 0
      app/src/main/java/core/util/Format.kt
  39. 166 0
      app/src/main/java/core/util/MediaUtils.kt
  40. 130 0
      app/src/main/java/core/util/ScreenAdapter.kt
  41. 50 0
      app/src/main/java/core/util/VideoFullScreenWebChromeClient.kt
  42. 27 0
      app/src/main/java/core/util/Weak.kt
  43. 26 0
      app/src/main/java/core/util/WeakHandler.kt
  44. 155 0
      app/src/main/java/core/util/WebViewHelper.kt
  45. 26 0
      app/src/main/java/core/util/net/NetChangeListener.kt
  46. 20 0
      app/src/main/java/core/util/net/NetConnectedListener.kt
  47. 143 0
      app/src/main/java/core/util/net/NetWatchdog.kt
  48. 49 0
      app/src/main/java/http/HttpClient.java
  49. 39 0
      app/src/main/java/http/OkHttpUtils.java
  50. 623 0
      app/src/main/java/http/client/ApiRepository.kt
  51. 128 0
      app/src/main/java/http/client/HttpTool.java
  52. 305 0
      app/src/main/java/http/client/LabClient.kt
  53. 9 0
      app/src/main/java/http/client/factory/ClientFactory.kt
  54. 9 0
      app/src/main/java/http/client/factory/RetrofitFactory.kt
  55. 400 0
      app/src/main/java/http/client/retrofit/ApiService.java
  56. 596 0
      app/src/main/java/http/client/retrofit/LabRetrofit.kt
  57. 27 0
      app/src/main/java/http/config/ConfigCore.kt
  58. 27 0
      app/src/main/java/http/config/ConfigFactory.kt
  59. 22 0
      app/src/main/java/http/converter/NullOnEmptyConverterFactory.kt
  60. 16 0
      app/src/main/java/http/exception/AICheckException.kt
  61. 9 0
      app/src/main/java/http/exception/NetException.kt
  62. 92 0
      app/src/main/java/http/interceptor/TokenHeaderInterceptor.kt
  63. 14 0
      app/src/main/java/http/net/DownloadListener.kt
  64. 156 0
      app/src/main/java/http/net/DownloadTask.kt
  65. 5 0
      app/src/main/java/http/vo/CommonDataResponse.kt
  66. 17 0
      app/src/main/java/http/vo/CommonResponse.kt
  67. 11 0
      app/src/main/java/http/vo/CommonRowsResponse.kt
  68. 28 0
      app/src/main/java/http/vo/request/AccessTokenReq.java
  69. 22 0
      app/src/main/java/http/vo/request/ApkInfoReq.java
  70. 28 0
      app/src/main/java/http/vo/request/AuthFaceReq.java
  71. 24 0
      app/src/main/java/http/vo/request/AuthPwdReq.java
  72. 20 0
      app/src/main/java/http/vo/request/BannerImageReq.java
  73. 26 0
      app/src/main/java/http/vo/request/CheckInAllReq.java
  74. 22 0
      app/src/main/java/http/vo/request/CommonSignInReq.java
  75. 24 0
      app/src/main/java/http/vo/request/ControllerCMD.java
  76. 22 0
      app/src/main/java/http/vo/request/FaceCompareReq.java
  77. 24 0
      app/src/main/java/http/vo/request/HazardReq.java
  78. 18 0
      app/src/main/java/http/vo/request/LotDeviceReq.java
  79. 28 0
      app/src/main/java/http/vo/request/MonitorReq.java
  80. 62 0
      app/src/main/java/http/vo/request/NoticeReq.java
  81. 22 0
      app/src/main/java/http/vo/request/OnLineUserReq.java
  82. 22 0
      app/src/main/java/http/vo/request/PatrolSignInReq.java
  83. 20 0
      app/src/main/java/http/vo/request/PatrolSignOutReq.java
  84. 16 0
      app/src/main/java/http/vo/request/SafetyListReq.java
  85. 20 0
      app/src/main/java/http/vo/request/SignInReq.java
  86. 18 0
      app/src/main/java/http/vo/request/SubIdReq.java
  87. 34 0
      app/src/main/java/http/vo/request/TerminalAuthReq.java
  88. 14 0
      app/src/main/java/http/vo/response/AccessTokenResp.java
  89. 70 0
      app/src/main/java/http/vo/response/ApkInfoResp.java
  90. 12 0
      app/src/main/java/http/vo/response/BannerImageBean.java
  91. 31 0
      app/src/main/java/http/vo/response/CheckMachineVo.java
  92. 14 0
      app/src/main/java/http/vo/response/ContentMachineVo.java
  93. 20 0
      app/src/main/java/http/vo/response/DutyPersonVo.java
  94. 36 0
      app/src/main/java/http/vo/response/HazardBook.java
  95. 35 0
      app/src/main/java/http/vo/response/HomeMiddleResp.java
  96. 16 0
      app/src/main/java/http/vo/response/HomeRightResp.java
  97. 15 0
      app/src/main/java/http/vo/response/HomeTopResp.java
  98. 47 0
      app/src/main/java/http/vo/response/LabBulletinBoardVo.java
  99. 18 0
      app/src/main/java/http/vo/response/LabHazardVo.java
  100. 0 0
      app/src/main/java/http/vo/response/LabPersonVo.java

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.kotlin
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 3 - 0
app/.gitignore

@@ -0,0 +1,3 @@
+/build
+/release
+/schemas

+ 123 - 0
app/build.gradle

@@ -0,0 +1,123 @@
+plugins {
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.ksp)
+    alias(libs.plugins.room)
+}
+
+android {
+    namespace 'xn.xxp'
+    compileSdk 35
+
+    defaultConfig {
+        applicationId "xn.xxp"
+        minSdk 31
+        targetSdk 35
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        room {
+            schemaDirectory("$projectDir/schemas")
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+    }
+
+    buildFeatures {
+        viewBinding true
+    }
+
+    kotlinOptions {
+        jvmTarget = '17'
+    }
+
+    applicationVariants.configureEach { variant ->
+        variant.outputs.configureEach { output ->
+            def formattedDate = new Date().format('yyyyMMddHHmm')
+            output.outputFileName = "xn_xxp_${variant.versionName}_${formattedDate}.apk"
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation fileTree(dir: "libs", include: ["*.aar"])
+
+    implementation libs.appcompat
+    implementation libs.material
+    implementation libs.activity
+    implementation libs.constraintlayout
+    testImplementation libs.junit
+    androidTestImplementation libs.ext.junit
+    androidTestImplementation libs.espresso.core
+
+    // androidx
+    implementation libs.androidx.core.ktx
+    implementation libs.androidx.annotation
+    implementation libs.androidx.lifecycle.extensions
+    implementation libs.androidx.lifecycle.livedata.ktx
+    implementation libs.androidx.lifecycle.viewmodel.ktx
+    implementation libs.androidx.work.runtime
+    //room
+    implementation libs.androidx.room.runtime
+    ksp libs.androidx.room.compiler
+    // Retrofit
+    implementation libs.retrofit
+    implementation libs.converter.gson
+    implementation libs.converter.scalars
+    implementation libs.adapter.rxjava3
+    // OkHttp
+    implementation libs.okhttp
+    implementation libs.logging.interceptor
+    // RxJava
+    implementation libs.rxjava
+    // RxAndroid
+    implementation libs.rxandroid
+    // gson
+    implementation libs.gson
+    // auc
+    implementation libs.utilcodex
+    // swiperefreshlayout
+    implementation libs.androidx.swiperefreshlayout
+    //noinspection UseTomlInstead 3.0其实已经过时了 但是项目中依赖过多 懒得弄
+    implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.14"
+    // eventbus
+    implementation libs.eventbus
+    // dialogX
+    implementation libs.dialogx
+    // glide
+    implementation libs.glide
+    // xxPermissions
+    implementation libs.xxpermissions
+    // flexbox
+    implementation libs.flexbox
+    // lottie
+    implementation libs.lottie
+    // mqtt
+    implementation libs.org.eclipse.paho.client.mqttv3
+    // refresh
+    implementation libs.refresh.layout.kernel
+    implementation libs.refresh.footer.classics
+    implementation libs.refresh.header.classics
+    // zxing
+    implementation libs.zxing.core
+    implementation libs.zxing.android.core
+    // coil
+    implementation libs.coil
+    implementation libs.coil.svg
+    // fotoapparat
+    implementation libs.fotoapparat
+
+
+}

BIN
app/libs/HatomPlayerCore.jar


BIN
app/libs/anrwatchdog-1.4.0.jar


BIN
app/libs/cardlibrary-release.aar


BIN
app/libs/facedetector-1.0.0.aar


BIN
app/libs/flowlayout-1.0.0.aar


BIN
app/libs/lib-eyecoolfinger-v2.3.0.jar


BIN
app/libs/tbs_sdk_v4.3.0.165_20210628_103707.jar


+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# 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

+ 26 - 0
app/src/androidTest/java/xn/xxp/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package xn.xxp;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("xn.xxp", appContext.getPackageName());
+    }
+}

+ 104 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission
+        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-feature android:name="android.hardware.camera" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+    <uses-feature android:name="android.hardware.camera.any" />
+
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
+
+    <application
+        android:name=".app.LabApp"
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:requestLegacyExternalStorage="true"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.西农电子信息牌"
+        android:usesCleartextTraffic="true">
+        <activity
+            android:name=".main.msds.HtmlFullScreenActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".main.msds.InstructionActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".main.things.ThingsActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".main.risk.RiskActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".main.person.LaboratoryPersonActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".main.monitor.MonitorListActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".home.lab_info.LabDetailActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".main.MainActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".home.auth.ChoiceAuthActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".home.notice.NoticeListActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".home.lab_info.LabDescActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".HomeActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".app.InitActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".app.SettingActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="false" />
+        <activity
+            android:name=".StartActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:exported="true"
+            android:launchMode="singleTop">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
app/src/main/assets/face_loading.json


+ 12 - 0
app/src/main/assets/simple_table.css

@@ -0,0 +1,12 @@
+@charset "utf-8";
+
+table {
+  border-top: 1px solid #000;
+  border-left: 1px solid #000;
+  border-spacing: 0;
+}
+
+table td {
+  border-bottom: 1px solid #000;
+  border-right: 1px solid #000;
+}

BIN
app/src/main/assets/songti.zip


BIN
app/src/main/ic_launcher-playstore.png


+ 8 - 0
app/src/main/java/core/event/RefreshEvent.kt

@@ -0,0 +1,8 @@
+package core.event
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+data class RefreshEvent(var refresh: Boolean = true, var flag: String)

+ 71 - 0
app/src/main/java/core/ui/ActivityCollector.kt

@@ -0,0 +1,71 @@
+//package core.ui
+//
+//import android.app.Activity
+//import java.util.ArrayList
+//
+///**
+// * info
+// *
+// * @author ReiChin_
+// */
+//object ActivityCollector {
+//
+//    /**
+//     * 存放应用已开启的所有Activity.
+//     */
+//    private val mActivities by lazy { ArrayList<Activity>() }
+//
+//    /**
+//     * 添加一个Activity到Activity列表中.
+//     *
+//     * @param activity
+//     * : Activity实例.
+//     */
+//    fun addActivity(activity: Activity) {
+//        mActivities.add(activity)
+//    }
+//
+//    /**
+//     * 在Activity列表中,从顶部删除一定数量的Activity.
+//     * -1表示删除所有的Activity, 如果count大于Activity列表的size, 将删除所有的Activity.
+//     *
+//     * @param count
+//     * : 要删的Activity数量.
+//     */
+//    fun removeActivity(count: Int) {
+//        val listSize = mActivities.size
+//        val delCount = if (-1 == count || count > listSize) listSize else count
+//        for (i in 0 until delCount) {
+//            val activity = mActivities[listSize - i - 1]
+//            if (!activity.isFinishing) {
+//                activity.finish()
+//            }
+//            mActivities.remove(activity)
+//        }
+//    }
+//
+//    /**
+//     * 从Activity列表中删除指定的Activity.
+//     *
+//     * @param activity
+//     * : 要删的Activity.
+//     */
+//    fun removeActivity(activity: Activity) {
+//        if (mActivities.contains(activity)) {
+//            mActivities.remove(activity)
+//        }
+//    }
+//
+//    /**
+//     * 关闭所有的Activity, 退出程序.
+//     */
+//    fun finishAll() {
+//        for (activity in mActivities) {
+//            if (!activity.isFinishing) {
+//                activity.finish()
+//            }
+//        }
+//        mActivities.clear()
+//    }
+//
+//}

+ 95 - 0
app/src/main/java/core/ui/activity/BaseActivity.java

@@ -0,0 +1,95 @@
+package core.ui.activity;
+
+import android.content.pm.ActivityInfo;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.viewbinding.ViewBinding;
+
+import com.blankj.utilcode.util.LogUtils;
+
+import core.ui.common.AbsUIDelegate;
+import core.ui.common.UIDelegateImpl;
+import core.util.ScreenAdapter;
+import io.reactivex.rxjava3.disposables.Disposable;
+
+public abstract class BaseActivity<VB extends ViewBinding> extends AppCompatActivity {
+
+    private UIDelegateImpl uiDelegate;
+    public VB binding;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+        ScreenAdapter.INSTANCE.setCustomDensity(this);
+        uiDelegate = AbsUIDelegate.Companion.create();
+        configImmersiveMode();
+        binding = createViewBinding();
+        setContentView(binding.getRoot());
+        initViews(savedInstanceState);
+        initListener();
+        initData();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        ScreenAdapter.INSTANCE.setCustomDensity(this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        uiDelegate.clearDisposable();
+        dismissLoading();
+    }
+
+    private void configImmersiveMode() {
+        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+        getWindow().setStatusBarColor(Color.WHITE);
+    }
+
+    protected abstract VB createViewBinding();
+
+    protected void initViews(Bundle savedInstanceState) {
+
+    }
+
+    protected void initListener() {
+
+    }
+
+    protected void initData() {
+
+    }
+
+
+    public void showLoading(String message) {
+        uiDelegate.showLoading(this, message, false);
+    }
+
+    public void dismissLoading() {
+        uiDelegate.dismissLoading();
+    }
+
+    public void showToast(String message) {
+        uiDelegate.showToast(this, message);
+    }
+
+    public void showNetError(Throwable throwable) {
+        uiDelegate.showNetError(this, throwable);
+    }
+
+    public void addDisposable(Disposable disposable) {
+        uiDelegate.addDisposable(disposable);
+    }
+
+    public void removeDisposable(Disposable disposable) {
+        uiDelegate.removeDisposable(disposable);
+    }
+
+}

+ 122 - 0
app/src/main/java/core/ui/activity/BaseCountDownActivity.java

@@ -0,0 +1,122 @@
+package core.ui.activity;
+
+import android.os.Bundle;
+import android.os.CountDownTimer;
+
+import androidx.viewbinding.ViewBinding;
+
+import com.blankj.utilcode.util.ActivityUtils;
+import com.blankj.utilcode.util.LogUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import xn.xxp.HomeActivity;
+import xn.xxp.home.notice.NoticeListActivity;
+import xn.xxp.app.LabApp;
+import xn.xxp.room.RoomTool;
+import xn.xxp.room.bean.NoticeSummary;
+import xn.xxp.room.dao.NoticeSummaryDao;
+import xn.xxp.widget.ITitleBar;
+import xn.xxp.widget.NavViewCompat;
+
+public abstract class BaseCountDownActivity<VB extends ViewBinding> extends BaseActivity<VB> {
+    private final int MAX_TIME = 120;
+
+    private int finishCd = 0;
+
+    private CountDownTimer countDownTimer;
+
+    @Override
+    protected void initViews(Bundle savedInstanceState) {
+        super.initViews(savedInstanceState);
+        ITitleBar iTitleBar = getMTitleBar();
+        if (null != iTitleBar) {
+            iTitleBar.setTitleInfoFromSp();
+            NoticeSummaryDao noticeSummaryDao = RoomTool.getInstance().noticeSummaryDao();
+            List<NoticeSummary> noticeSummaryList = noticeSummaryDao.getAll();
+            if (null == noticeSummaryList) {
+                noticeSummaryList = new ArrayList<>();
+            }
+            iTitleBar.updateNotice(noticeSummaryList);
+            iTitleBar.setTitleListener(() -> ActivityUtils.startActivity(NoticeListActivity.class));
+        }
+        NavViewCompat navViewCompat = getMNavView();
+        if (null != navViewCompat) {
+            navViewCompat.setNavListener(new NavViewCompat.NavListener() {
+                @Override
+                public void onHomeViewClicked() {
+                    logoutCountDownFinish();
+                }
+
+                @Override
+                public void onBackViewClicked() {
+                    finish();
+                }
+
+                @Override
+                public void onFingerEnrollViewClicked() {
+
+                }
+            });
+        }
+
+
+        if (enabledBackCountDown()) {
+            countDownTimer = new CountDownTimer(1000, 1000) {
+                @Override
+                public void onTick(long millisUntilFinished) {
+
+                }
+
+                @Override
+                public void onFinish() {
+                    if (finishCd >= MAX_TIME) {
+                        logoutCountDownFinish();
+                    } else {
+                        finishCd++;
+                        countDownTimer.start();
+                    }
+                }
+            };
+        }
+    }
+
+    protected void logoutCountDownFinish() {
+        LabApp.userVo = null;
+        ActivityUtils.finishToActivity(HomeActivity.class, false);
+        LogUtils.d(ActivityUtils.getActivityList());
+    }
+
+    public abstract ITitleBar getMTitleBar();
+
+    public abstract NavViewCompat getMNavView();
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (null != countDownTimer) {
+            finishCd = 0;
+            countDownTimer.start();
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if (null != countDownTimer) {
+            countDownTimer.cancel();
+        }
+    }
+
+    @Override
+    public void onUserInteraction() {
+        super.onUserInteraction();
+        finishCd = 0;
+    }
+
+    protected boolean enabledBackCountDown() {
+        return false;
+    }
+}

+ 342 - 0
app/src/main/java/core/ui/activity/BaseSignActivity.kt

@@ -0,0 +1,342 @@
+package core.ui.activity
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.viewbinding.ViewBinding
+import core.util.ifNullOrEmpty
+import http.client.ApiRepository
+import http.exception.NetException
+import http.vo.request.CommonSignInReq
+import http.vo.response.SignFaceVo
+import http.vo.response.SignInCheckResp
+import org.greenrobot.eventbus.EventBus
+import xn.xxp.app.LabApp
+import xn.xxp.home.auth.PickSignTypeDialog
+import xn.xxp.home.auth.ResultEnum
+import xn.xxp.home.auth.SafetyCheckResultDialog
+import xn.xxp.home.auth.SignType
+import xn.xxp.home.leave.LeaveActivity
+import xn.xxp.home.sign.SignInActivity
+import xn.xxp.mqtt.event.OnlineUserEvent
+import xn.xxp.room.RoomTool
+import xn.xxp.room.bean.DeviceConfig
+import xn.xxp.room.bean.LabConfig
+import xn.xxp.utils.Tool
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class BaseSignActivity<VB : ViewBinding> : BaseCountDownActivity<VB>() {
+    private lateinit var labConfig: LabConfig
+    protected lateinit var deviceConfig: DeviceConfig
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        labConfig = RoomTool.getInstance().labConfigDao().labConfig
+        deviceConfig = RoomTool.getInstance().deviceConfigDao().deviceConfig
+    }
+
+    protected fun dispatchSignIn() {
+        LabApp.userVo ?: return
+
+        if ("1" == LabApp.userVo?.pageType) {
+            // 一类人员:巡查签到
+            dispatchPatrolSign()
+        } else {
+            if ("1" == LabApp.userVo?.isDutyUser) {
+                // 值班人员:选择巡查签到/准入签到
+                PickSignTypeDialog(this) { type ->
+                    if (SignType.PATROL.code == type) {
+                        dispatchPatrolSign()
+                    } else {
+                        dispatchAccessSign()
+                    }
+                }.show()
+            } else {
+                // 其它人员:准入签到
+                dispatchAccessSign()
+            }
+        }
+    }
+
+    protected fun dispatchSignOut() {
+        LabApp.userVo ?: return
+
+        if ("1" == LabApp.userVo?.pageType) {
+            // 一类人员:巡查签退
+            callLeaveApi(SignType.PATROL)
+        } else {
+            if ("1" == LabApp.userVo?.isDutyUser) {
+                // 值班人员:选择巡查签退/准入签退
+                queryLeaveType {
+                    // 准入签退
+                    dispatchAccessLeave()
+                }
+            } else {
+                // 其它人员:准入签退
+                dispatchAccessLeave()
+            }
+        }
+    }
+
+    protected open fun onSignInFinish(completed: Boolean) {
+    }
+
+    protected open fun onSignOutFinish(completed: Boolean) {
+    }
+
+    private fun dispatchAccessSign() {
+        val param = CommonSignInReq().apply {
+            labId = labConfig.labId.toString()
+            userId = LabApp.userVo.userId
+        }
+        if (false == mustAccessInCheck()) {
+            // 直接签到
+            callSignApi(SignType.ACCESS, param)
+        } else {
+            accessSignCheck(param) { result ->
+                val intent = Intent(this, SignInActivity::class.java)
+                intent.putExtra("sign_type", SignType.ACCESS.code)
+                intent.putExtra("sign_face", result)
+                startActivity(intent)
+                onSignInFinish(false)
+            }
+        }
+    }
+
+    // 准入签到检测
+    private fun accessSignCheck(param: CommonSignInReq, callback: (result: SignFaceVo) -> Unit) {
+        showLoading("请稍等...")
+        val disposable = ApiRepository.accessSignCheck(param)
+            .subscribe({
+                dismissLoading()
+                callback.invoke(it)
+            }, { throwable ->
+                dismissLoading()
+                if (!pretreatmentError(throwable, "签到核验")) {
+                    throwable.printStackTrace()
+                    showNetError(throwable)
+                }
+            })
+        addDisposable(disposable)
+    }
+
+    private fun dispatchPatrolSign() {
+        val param = CommonSignInReq().apply {
+            num = deviceConfig.devId
+            labId = labConfig.labId.toString()
+            userId = LabApp.userVo.userId
+        }
+        if (false == mustPatrolAiCheck()) {
+            // 直接签到
+            callSignApi(SignType.PATROL, param)
+        } else {
+            patrolSignCheck(param) {
+                val intent = Intent(this, SignInActivity::class.java)
+                intent.putExtra("sign_type", SignType.PATROL.code)
+                startActivity(intent)
+                onSignInFinish(false)
+            }
+        }
+    }
+
+    // 巡查签到检测
+    private fun patrolSignCheck(param: CommonSignInReq, callback: () -> Unit) {
+        showLoading("请稍等...")
+        val disposable = ApiRepository.patrolSignCheck(param)
+            .subscribe({
+                dismissLoading()
+                callback.invoke()
+            }, { throwable ->
+                dismissLoading()
+                if (!pretreatmentError(throwable, "签到核验")) {
+                    throwable.printStackTrace()
+                    showNetError(throwable)
+                }
+            })
+        addDisposable(disposable)
+    }
+
+    private fun callSignApi(signType: SignType, param: CommonSignInReq) {
+        showLoading("签到中...")
+        val disposable = ApiRepository.commonSignIn(signType.code, param)
+            .subscribe({
+                dismissLoading()
+                EventBus.getDefault().post(OnlineUserEvent())
+                SafetyCheckResultDialog(
+                    this,
+                    ResultEnum.SUCCESS,
+                    "签到成功",
+                    3000
+                ).apply {
+                    setCancelable(false)
+                    setOnDismissListener { onSignInFinish(true) }
+                    show()
+                }
+            }, { throwable ->
+                dismissLoading()
+                if (!pretreatmentError(throwable, "签到")) {
+                    throwable.printStackTrace()
+                    showNetError(throwable)
+                }
+            })
+        addDisposable(disposable)
+    }
+
+    private fun pretreatmentError(throwable: Throwable, text: String): Boolean {
+        if (throwable is NetException) {
+            val message = throwable.message.ifNullOrEmpty("${text}失败,请联系实验室管理员。")
+            //AudioPlayerManager.speakVoice(R.raw.audio_failed_sign_admin)
+            SafetyCheckResultDialog(this, ResultEnum.FAIL, message, 3000).apply {
+                setCancelable(false)
+                show()
+            }
+            return true
+        }
+        return false
+    }
+
+    private fun dispatchAccessLeave() {
+        if (false == mustAccessOutCheck()) {
+            // 直接准入签退
+            callLeaveApi(SignType.ACCESS)
+        } else {
+            accessLeaveCheck { resp ->
+                val intent = Intent(this, LeaveActivity::class.java)
+                intent.putExtra("data", resp)
+                startActivity(intent)
+                onSignOutFinish(false)
+            }
+        }
+    }
+
+    private fun accessLeaveCheck(callback: (resp: SignInCheckResp) -> Unit) {
+        if (null == labConfig || null == LabApp.userVo) {
+            showToast("暂无实验室信息,即将重新获取")
+            Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
+            return
+        }
+
+        showLoading("请稍等...")
+        val disposable =
+            ApiRepository.signOutCheck(true, labConfig.labId.toString(), LabApp.userVo!!.userId)
+                .subscribe({ data ->
+                    dismissLoading()
+                    if (data.state) {
+                        callback.invoke(data)
+                    } else {
+                        val message =
+                            if (data.message.isNullOrEmpty()) "核验失败,请联系管理员!" else data.message[0]
+                        SafetyCheckResultDialog(this, ResultEnum.FAIL, message, 3000).apply {
+                            setCancelable(false)
+                            show()
+                        }
+                    }
+                }, { throwable ->
+                    dismissLoading()
+                    if (!pretreatmentError(throwable, "签退核验")) {
+                        throwable.printStackTrace()
+                        showNetError(throwable)
+                    }
+                })
+        addDisposable(disposable)
+    }
+
+    private fun queryLeaveType(callback: () -> Unit) {
+        if (null == labConfig || null == LabApp.userVo) {
+            showToast("暂无实验室信息,即将重新获取")
+            Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
+            return
+        }
+
+        showLoading("请稍等...")
+        val disposable =
+            ApiRepository.isSignInType(labConfig.labId.toString(), LabApp.userVo!!.userId)
+                .subscribe({ patrol ->
+                    dismissLoading()
+                    if (patrol) {
+                        callLeaveApi(SignType.PATROL)
+                    } else {
+                        callback.invoke()
+                    }
+                }, { throwable ->
+                    dismissLoading()
+                    if (!pretreatmentError(throwable, "签退")) {
+                        throwable.printStackTrace()
+                        showNetError(throwable)
+                    }
+                })
+        addDisposable(disposable)
+    }
+
+    private fun callLeaveApi(signType: SignType) {
+        if (null == labConfig || null == LabApp.userVo) {
+            showToast("暂无实验室信息,即将重新获取")
+            Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
+            return
+        }
+
+        showLoading("签退中...")
+        val disposable = ApiRepository.commonLeave(
+            signType.code,
+            labConfig.labId.toString(),
+            LabApp.userVo.userId
+        )
+            .subscribe({
+                dismissLoading()
+                EventBus.getDefault().post(OnlineUserEvent())
+                SafetyCheckResultDialog(
+                    this,
+                    ResultEnum.SUCCESS,
+                    "离开成功",
+                    3000
+                ).apply {
+                    setCancelable(false)
+                    setOnDismissListener { onSignOutFinish(true) }
+                    show()
+                }
+            }, { throwable ->
+                dismissLoading()
+                if (!pretreatmentError(throwable, "离开")) {
+                    throwable.printStackTrace()
+                    showNetError(throwable)
+                }
+            })
+        addDisposable(disposable)
+    }
+
+    // 是否配置了离开检查项
+    private fun mustAccessOutCheck(): Boolean? {
+        val laboratoryVo = LabApp.laboratory
+        if (null == laboratoryVo || null == labConfig) {
+            showToast("暂无实验室信息,即将重新获取")
+            Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
+            return null
+        }
+        return !laboratoryVo.outCheck.isNullOrEmpty()
+    }
+
+    // 是否需要准入AI检测
+    private fun mustAccessInCheck(): Boolean? {
+        val laboratoryVo = LabApp.laboratory
+        if (null == laboratoryVo || null == labConfig) {
+            showToast("暂无实验室信息,即将重新获取")
+            Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
+            return null
+        }
+        return !laboratoryVo.inCheck.isNullOrEmpty()
+    }
+
+    // 是否需要巡查AI检测
+    private fun mustPatrolAiCheck(): Boolean? {
+        val laboratoryVo = LabApp.laboratory
+        if (null == laboratoryVo || null == labConfig) {
+            showToast("暂无实验室信息,即将重新获取")
+            Tool.INSTANCE.reStartApp("暂无实验室信息,即将重新获取")
+            return null
+        }
+        return !laboratoryVo.inspectInCheck.isNullOrEmpty()
+    }
+
+}

+ 37 - 0
app/src/main/java/core/ui/common/AbsUIDelegate.kt

@@ -0,0 +1,37 @@
+package core.ui.common
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import io.reactivex.rxjava3.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class AbsUIDelegate {
+
+    companion object {
+        fun create() = UIDelegateImpl()
+    }
+
+    abstract fun showLoading(
+        context: Context,
+        message: String? = null,
+        cancelable: Boolean = false
+    )
+
+    abstract fun dismissLoading()
+
+    abstract fun showToast(context: Context?, message: String)
+
+    abstract fun showNetError(context: Context?, throwable: Throwable)
+
+    abstract fun addDisposable(disposable: Disposable)
+
+    abstract fun removeDisposable(disposable: Disposable)
+
+    abstract fun clearDisposable()
+
+    abstract fun createItemDecoration(context: Context?): RecyclerView.ItemDecoration?
+}

+ 26 - 0
app/src/main/java/core/ui/common/IUIListener.kt

@@ -0,0 +1,26 @@
+package core.ui.common
+
+import androidx.recyclerview.widget.RecyclerView
+import io.reactivex.rxjava3.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+interface IUIListener {
+
+    fun showLoading(message: String? = null, cancelable: Boolean = false)
+
+    fun dismissLoading()
+
+    fun showToast(message: String)
+
+    fun showNetError(throwable: Throwable)
+
+    fun addDisposable(disposable: Disposable)
+
+    fun removeDisposable(disposable: Disposable)
+
+    fun createItemDecoration(): RecyclerView.ItemDecoration?
+}

+ 86 - 0
app/src/main/java/core/ui/common/UIDelegateImpl.kt

@@ -0,0 +1,86 @@
+package core.ui.common
+
+import android.content.Context
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.RecyclerView
+import com.blankj.utilcode.util.ActivityUtils
+import com.blankj.utilcode.util.ThreadUtils
+import com.kongzue.dialogx.dialogs.WaitDialog
+import http.exception.NetException
+import core.ui.widget.decoration.NoLastLineItemDecoration
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.disposables.Disposable
+import retrofit2.HttpException
+import xn.xxp.R
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class UIDelegateImpl : AbsUIDelegate() {
+
+    override fun showLoading(context: Context, message: String?, cancelable: Boolean) {
+        WaitDialog.show(message).setCancelable(cancelable).setEnterAnimDuration(0)
+    }
+
+    override fun dismissLoading() {
+        WaitDialog.dismiss()
+    }
+
+    override fun showToast(context: Context?, message: String) {
+        ThreadUtils.runOnUiThread {
+            Toast.makeText(ActivityUtils.getTopActivity(), message, Toast.LENGTH_SHORT).show()
+        }
+    }
+
+    override fun showNetError(context: Context?, throwable: Throwable) {
+        when (throwable) {
+            is NetException -> {
+                if (throwable.message.isNullOrEmpty()) {
+                    "接口请求失败(${throwable.code})"
+                } else {
+                    throwable.message!!
+                }
+            }
+
+            is SocketTimeoutException -> "请求超时,请稍后重试"
+            is ConnectException -> "无法连接服务器,请检查网络"
+            is HttpException -> "服务器繁忙,请稍后重试"
+            else -> null
+        }?.let { showToast(context, it) }
+    }
+
+    private var mCompositeDisposable: CompositeDisposable? = null
+
+    override fun addDisposable(disposable: Disposable) {
+        mCompositeDisposable = (mCompositeDisposable ?: CompositeDisposable()).apply {
+            if (!isDisposed) add(disposable)
+        }
+    }
+
+    override fun removeDisposable(disposable: Disposable) {
+        mCompositeDisposable = (mCompositeDisposable ?: CompositeDisposable()).apply {
+            remove(disposable)
+        }
+    }
+
+    override fun clearDisposable() {
+        mCompositeDisposable = mCompositeDisposable?.let {
+            it.clear()
+            null
+        }
+    }
+
+    override fun createItemDecoration(context: Context?): RecyclerView.ItemDecoration? {
+        return context?.let {
+            NoLastLineItemDecoration(it, DividerItemDecoration.VERTICAL).apply {
+                setDrawable(ContextCompat.getDrawable(it, R.drawable.shape_item_divider)!!)
+            }
+        }
+    }
+}

+ 121 - 0
app/src/main/java/core/ui/dialog/RcBaseDialog.kt

@@ -0,0 +1,121 @@
+package core.ui.dialog
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.*
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import xn.xxp.R
+import core.ui.common.AbsUIDelegate
+import core.ui.common.IUIListener
+import io.reactivex.rxjava3.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcBaseDialog<VB : ViewBinding> :
+    DialogFragment(), IUIListener {
+
+    private lateinit var _viewBinding: VB
+
+    protected val viewBinding get() = _viewBinding
+
+    private lateinit var mUIDelegate: AbsUIDelegate
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mUIDelegate = AbsUIDelegate.create()
+
+        setStyle(STYLE_NO_TITLE, R.style.LoadingDialog)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        initWindowConfig()
+        _viewBinding = createViewBinding(inflater, container)
+        return _viewBinding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initViews(savedInstanceState)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        initDialogSize()
+    }
+
+    protected open fun initDialogSize() {
+        dialog?.window?.apply {
+            attributes.width = WindowManager.LayoutParams.MATCH_PARENT
+            attributes.height = WindowManager.LayoutParams.WRAP_CONTENT
+            setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
+        }
+    }
+
+    protected open fun initWindowConfig() {
+//        dialog?.window?.apply {
+//            setWindowAnimations(R.style.BottomDialog_Animation)
+//            setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+//        }
+//
+//        dialog?.window?.apply {
+//            attributes.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
+//            attributes.windowAnimations = R.style.BottomDialog_Animation
+//            requestFeature(Window.FEATURE_NO_TITLE)
+//            setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+//        }
+//        dialog?.setCanceledOnTouchOutside(true)
+    }
+
+    protected abstract fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): VB
+
+    protected abstract fun initViews(savedInstanceState: Bundle?)
+
+    override fun showLoading(message: String?, cancelable: Boolean) {
+        fragmentManager?.let {
+            mUIDelegate.showLoading(requireActivity(), message, cancelable)
+        }
+    }
+
+    override fun dismissLoading() {
+        mUIDelegate.dismissLoading()
+    }
+
+    override fun showToast(message: String) {
+        mUIDelegate.showToast(context, message)
+    }
+
+    override fun showNetError(throwable: Throwable) {
+        mUIDelegate.showNetError(context, throwable)
+    }
+
+    override fun addDisposable(disposable: Disposable) {
+        mUIDelegate.addDisposable(disposable)
+    }
+
+    override fun removeDisposable(disposable: Disposable) {
+        mUIDelegate.removeDisposable(disposable)
+    }
+
+    override fun onDestroyView() {
+        mUIDelegate.clearDisposable()
+        super.onDestroyView()
+    }
+
+    override fun createItemDecoration(): RecyclerView.ItemDecoration? {
+        return mUIDelegate.createItemDecoration(context)
+    }
+
+}

+ 87 - 0
app/src/main/java/core/ui/fragment/RcBaseFragment.kt

@@ -0,0 +1,87 @@
+package core.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import core.ui.common.AbsUIDelegate
+import core.ui.common.IUIListener
+import io.reactivex.rxjava3.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcBaseFragment<VB : ViewBinding> :
+    Fragment(), IUIListener {
+
+    private lateinit var _viewBinding: VB
+
+    protected val viewBinding get() = _viewBinding
+
+    private lateinit var mUIDelegate: AbsUIDelegate
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mUIDelegate = AbsUIDelegate.create()
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        _viewBinding = createViewBinding(inflater, container)
+        return _viewBinding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initViews(savedInstanceState)
+    }
+
+    protected abstract fun createViewBinding(
+        inflater: LayoutInflater,
+        container: ViewGroup?
+    ): VB
+
+    protected abstract fun initViews(savedInstanceState: Bundle?)
+
+    override fun showLoading(message: String?, cancelable: Boolean) {
+        context?.let { mUIDelegate.showLoading(it, message, cancelable) }
+    }
+
+    override fun dismissLoading() {
+        mUIDelegate.dismissLoading()
+    }
+
+    override fun showToast(message: String) {
+        mUIDelegate.showToast(context, message)
+    }
+
+    override fun showNetError(throwable: Throwable) {
+        mUIDelegate.showNetError(context, throwable)
+    }
+
+    override fun addDisposable(disposable: Disposable) {
+        mUIDelegate.addDisposable(disposable)
+    }
+
+    override fun removeDisposable(disposable: Disposable) {
+        mUIDelegate.removeDisposable(disposable)
+    }
+
+    override fun onDestroyView() {
+        mUIDelegate.clearDisposable()
+        super.onDestroyView()
+    }
+
+    override fun createItemDecoration(): RecyclerView.ItemDecoration? {
+        return mUIDelegate.createItemDecoration(context)
+    }
+
+}

+ 37 - 0
app/src/main/java/core/ui/fragment/RcLazyFragment.kt

@@ -0,0 +1,37 @@
+package core.ui.fragment
+
+import androidx.viewbinding.ViewBinding
+
+/**
+ * 懒加载
+ *
+ * @author ReiChin_
+ */
+abstract class RcLazyFragment<VB : ViewBinding> : RcBaseFragment<VB>() {
+
+    override fun onResume() {
+        super.onResume()
+        lazyLoad()
+    }
+
+    private fun lazyLoad() {
+        if (!loadDateCompleted()) {
+            onLoadData()
+        }
+    }
+
+    /**
+     * 加载数据
+     */
+    protected abstract fun onLoadData()
+
+    /**
+     * 数据是否加载完毕,子类可覆写此方法,根据需要返回。
+     * 1) false,每次画面可见时,都会回调[.onLoadData]。
+     * 2) true,每次画面可见时,不会回调[.onLoadData]。
+     *
+     * @return 数据是否加载完成。
+     */
+    protected open fun loadDateCompleted() = false
+
+}

+ 169 - 0
app/src/main/java/core/ui/fragment/RcRefreshFragment.kt

@@ -0,0 +1,169 @@
+package core.ui.fragment
+
+import android.os.Bundle
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import androidx.viewbinding.ViewBinding
+import com.chad.library.adapter.base.BaseQuickAdapter
+import com.chad.library.adapter.base.listener.OnItemChildClickListener
+import com.chad.library.adapter.base.listener.OnItemClickListener
+import com.chad.library.adapter.base.listener.OnLoadMoreListener
+import com.chad.library.adapter.base.viewholder.BaseViewHolder
+import xn.xxp.R
+import io.reactivex.rxjava3.disposables.Disposable
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+abstract class RcRefreshFragment<T, VB : ViewBinding> :
+    RcLazyFragment<VB>(),
+    OnLoadMoreListener,
+    OnItemClickListener,
+    OnItemChildClickListener {
+
+    companion object {
+        const val FIRST_PAGE = 1
+        const val PAGE_SIZE = 15
+    }
+
+    protected abstract val mSrlRefresh: SwipeRefreshLayout
+    protected abstract val mRvContent: RecyclerView
+    protected abstract val mAdapter: BaseQuickAdapter<T, BaseViewHolder>
+
+    protected var mCurrentPage = FIRST_PAGE
+    protected var mRefresh = false
+
+    override fun initViews(savedInstanceState: Bundle?) {
+        initSwipeRecyclerView()
+    }
+
+    private fun initSwipeRecyclerView() {
+        mSrlRefresh.setOnRefreshListener { loadData(true) }
+
+        mAdapter.setOnItemClickListener(this)
+        mAdapter.setOnItemChildClickListener(this)
+
+        mRvContent.layoutManager = createLayoutManager()
+//        mAdapter.loadMoreModule.loadMoreView = ItLoadMoreView()
+        mAdapter.loadMoreModule.setOnLoadMoreListener(this)
+        mAdapter.loadMoreModule.isAutoLoadMore = true
+        // 当自动加载开启,同时数据不满一屏时,是否继续执行自动加载更多(默认为true)
+        mAdapter.loadMoreModule.isEnableLoadMoreIfNotFullPage = true
+        mAdapter.animationEnable = true
+        mAdapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.SlideInBottom)
+
+        createItemDecoration()?.let {
+            mRvContent.addItemDecoration(it)
+        }
+
+        mRvContent.adapter = mAdapter
+    }
+
+    protected open fun createLayoutManager(): RecyclerView.LayoutManager {
+        return LinearLayoutManager(requireActivity())
+    }
+
+    override fun onLoadData() {
+        mSrlRefresh.isRefreshing = true
+        loadData(true)
+    }
+
+    override fun loadDateCompleted() = mAdapter.data.isNotEmpty()
+
+    override fun onLoadMore() {
+        val dataSize = mAdapter.data.size
+        if (dataSize < PAGE_SIZE) {
+            mAdapter.loadMoreModule.loadMoreEnd(false)
+        } else {
+            if (dataSize % PAGE_SIZE != 0) {
+                mAdapter.loadMoreModule.loadMoreEnd(false)
+            } else {
+                mCurrentPage++
+                loadData(false)
+            }
+        }
+    }
+
+    protected fun loadData(refresh: Boolean) {
+        if (refresh) {
+            mCurrentPage = FIRST_PAGE
+            // 这里的作用是防止下拉刷新的时候还可以上拉加载
+            mAdapter.loadMoreModule.isEnableLoadMore = false
+        }
+        mRefresh = refresh
+        showLoadingView()
+        queryData()?.let { addDisposable(it) }
+    }
+
+    private fun showLoadingView() {
+//        showLoading()
+    }
+
+    /**
+     * 数据加载成功的处理逻辑
+     *
+     * @param data 从服务端查询的数据
+     */
+    protected open fun dispatchLoadDataSuccess(data: List<T>?) {
+//        dismissLoading()
+        if (isDetached) return
+
+        if (mRefresh) {
+            // 停止刷新
+            mSrlRefresh.isRefreshing = false
+
+            mAdapter.setNewInstance(data?.toMutableList())
+            mAdapter.loadMoreModule.loadMoreComplete()
+            if (data.isNullOrEmpty()) {
+                mAdapter.setEmptyView(R.layout.view_list_empty)
+            }
+            if (null == data || data.size < PAGE_SIZE) {
+                mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+            }
+        } else {
+            if (null == data || data.size < PAGE_SIZE) {
+                data?.let { mAdapter.addData(it) }
+                mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+            } else {
+                mAdapter.addData(data)
+                mAdapter.loadMoreModule.loadMoreComplete()
+            }
+        }
+    }
+
+    /**
+     * 数据加载失败的处理逻辑
+     */
+    protected open fun dispatchLoadDataFailure(throwable: Throwable) {
+//        dismissLoading()
+        if (isDetached) return
+
+        if (mRefresh) {
+            mSrlRefresh.isRefreshing = false
+            mAdapter.setEmptyView(R.layout.view_list_empty)
+        } else {
+            mCurrentPage = if (mCurrentPage-- < FIRST_PAGE) FIRST_PAGE else mCurrentPage
+            mAdapter.loadMoreModule.loadMoreEnd(goneLoadMoreView())
+        }
+        throwable.printStackTrace()
+        showNetError(throwable)
+    }
+
+    protected open fun goneLoadMoreView() = false
+
+    /**
+     * 查询数据
+     */
+    abstract fun queryData(): Disposable?
+
+    override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
+    }
+
+    override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
+    }
+
+}

+ 27 - 0
app/src/main/java/core/ui/widget/NoScrollGridLayoutManager.kt

@@ -0,0 +1,27 @@
+package core.ui.widget
+
+import android.content.Context
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class NoScrollGridLayoutManager(
+    context: Context,
+    spanCount: Int,
+    @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
+    reverseLayout: Boolean = false
+) : GridLayoutManager(context, spanCount, orientation, reverseLayout) {
+
+    override fun canScrollHorizontally(): Boolean {
+        return false
+    }
+
+    override fun canScrollVertically(): Boolean {
+        return false
+    }
+
+}

+ 60 - 0
app/src/main/java/core/ui/widget/SwipeTouchListener.kt

@@ -0,0 +1,60 @@
+package core.ui.widget
+
+import android.content.Context
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import kotlin.math.abs
+import kotlin.properties.Delegates
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class SwipeTouchListener(
+    context: Context,
+    private val callback: (action: SwipeAction) -> Unit
+) : View.OnTouchListener {
+
+    private var mLastMotionX: Float = 0f
+    private var mLastMotionY: Float = 0f
+
+    private var mActivePointerId: Int = -1
+
+    private var mTouchSlop by Delegates.notNull<Int>()
+
+    init {
+        mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop
+    }
+
+    override fun onTouch(v: View?, ev: MotionEvent?): Boolean {
+        when (ev?.action?.and(MotionEvent.ACTION_MASK)) {
+            MotionEvent.ACTION_DOWN -> {
+                mLastMotionX = ev.x
+                mLastMotionY = ev.y
+                mActivePointerId = ev.getPointerId(0)
+            }
+            MotionEvent.ACTION_UP -> {
+                val pointerIndex = ev.findPointerIndex(mActivePointerId)
+                if (pointerIndex != -1) {
+                    val x = ev.getX(pointerIndex)
+                    val xDiff = abs(x - mLastMotionX)
+                    val y = ev.getY(pointerIndex)
+                    val yDiff = abs(y - mLastMotionY)
+                    if (xDiff > mTouchSlop && xDiff > yDiff) {
+                        val action = if (x > mLastMotionX) SwipeAction.PREV else SwipeAction.NEXT
+                        callback.invoke(action)
+                    }
+                }
+            }
+        }
+
+        return true
+    }
+
+}
+
+enum class SwipeAction {
+    PREV, NEXT
+}

+ 33 - 0
app/src/main/java/core/ui/widget/SwipeViewPager.kt

@@ -0,0 +1,33 @@
+package core.ui.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import androidx.viewpager.widget.ViewPager
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class SwipeViewPager @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) :
+    ViewPager(context, attrs) {
+
+    private var swipeEnabled: Boolean = false
+
+    fun setSwipeEnabled(enabled: Boolean) {
+        this.swipeEnabled = enabled
+    }
+
+    override fun onTouchEvent(ev: MotionEvent?): Boolean {
+        return swipeEnabled && super.onTouchEvent(ev)
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+        return swipeEnabled && super.onInterceptTouchEvent(ev)
+    }
+
+}

+ 115 - 0
app/src/main/java/core/ui/widget/decoration/GridSpacingItemDecoration.java

@@ -0,0 +1,115 @@
+package core.ui.widget.decoration;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * FileName: GridSpacingItemDecoration
+ * Description:
+ */
+public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final int spanCount; //列数
+    private final int horizontalSpacing; // 水平间隔
+    private final int verticalSpacing; // 垂直间隔
+    private final boolean includeEdge; //是否包含边缘
+
+    private Drawable mDivider;
+
+    public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
+        this(spanCount, spacing, spacing, includeEdge);
+    }
+
+    public GridSpacingItemDecoration(int spanCount, int horizontalSpacing, int verticalSpacing, boolean includeEdge) {
+        this.spanCount = spanCount;
+        this.horizontalSpacing = horizontalSpacing;
+        this.verticalSpacing = verticalSpacing;
+        this.includeEdge = includeEdge;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+
+        //这里是关键,需要根据你有几列来判断
+        int position = parent.getChildAdapterPosition(view); // item position
+        int column = position % spanCount; // item column
+
+        if (includeEdge) {
+            outRect.left = horizontalSpacing - column * horizontalSpacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
+            outRect.right = (column + 1) * horizontalSpacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
+
+            if (position < spanCount) { // top edge
+                outRect.top = verticalSpacing;
+            }
+            outRect.bottom = verticalSpacing; // item bottom
+        } else {
+            outRect.left = column * horizontalSpacing / spanCount; // column * ((1f / spanCount) * spacing)
+            outRect.right = horizontalSpacing - (column + 1) * horizontalSpacing / spanCount; // spacing - (column + 1) * ((1f /    spanCount) * spacing)
+            if (position >= spanCount) {
+                outRect.top = verticalSpacing; // item top
+            }
+        }
+    }
+
+    public void setDrawable(@NotNull Drawable drawable) {
+        this.mDivider = drawable;
+    }
+
+    @Override
+    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+        if (parent.getLayoutManager() == null || mDivider == null) return;
+
+        drawVertical(c, parent);
+        drawHorizontal(c, parent);
+    }
+
+    /**
+     * 绘制垂直方向的分割线
+     *
+     * @param c
+     * @param parent
+     */
+    private void drawVertical(Canvas c, RecyclerView parent) {
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View childView = parent.getChildAt(i);
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
+            int top = childView.getTop() - params.topMargin;
+            int bottom = childView.getBottom() + params.bottomMargin;
+            int left = childView.getRight() + params.rightMargin;
+            int right = left + mDivider.getIntrinsicWidth();
+            // 计算水平分割线的位置
+            mDivider.setBounds(left, top, right, bottom);
+            mDivider.draw(c);
+        }
+    }
+
+    /**
+     * 绘制水平方向的分割线
+     *
+     * @param c
+     * @param parent
+     */
+    private void drawHorizontal(Canvas c, RecyclerView parent) {
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View childView = parent.getChildAt(i);
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
+            int left = childView.getLeft() - params.leftMargin;
+            int right = childView.getRight() + mDivider.getIntrinsicWidth() + params.rightMargin;
+            int top = childView.getBottom() + params.bottomMargin;
+            int bottom = top + mDivider.getIntrinsicHeight();
+            // 计算水平分割线的位置
+            mDivider.setBounds(left, top, right, bottom);
+            mDivider.draw(c);
+        }
+    }
+
+}

+ 148 - 0
app/src/main/java/core/ui/widget/decoration/NoLastLineItemDecoration.java

@@ -0,0 +1,148 @@
+package core.ui.widget.decoration;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class NoLastLineItemDecoration extends RecyclerView.ItemDecoration {
+    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+    public static final int VERTICAL = LinearLayout.VERTICAL;
+
+    private static final String TAG = "DividerItem";
+    private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
+
+    private Drawable mDivider;
+
+    /**
+     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
+     */
+    private int mOrientation;
+
+    private final Rect mBounds = new Rect();
+
+    public NoLastLineItemDecoration(Context context, int orientation) {
+        final TypedArray a = context.obtainStyledAttributes(ATTRS);
+        mDivider = a.getDrawable(0);
+        if (mDivider == null) {
+            Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+                    + "DividerItemDecoration. Please set that attribute all call setDrawable()");
+        }
+        a.recycle();
+        setOrientation(orientation);
+    }
+
+    /**
+     * Sets the orientation for this divider. This should be called if
+     * {@link RecyclerView.LayoutManager} changes orientation.
+     *
+     * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+     */
+    public void setOrientation(int orientation) {
+        if (orientation != HORIZONTAL && orientation != VERTICAL) {
+            throw new IllegalArgumentException(
+                    "Invalid orientation. It should be either HORIZONTAL or VERTICAL");
+        }
+        mOrientation = orientation;
+    }
+
+    /**
+     * Sets the {@link Drawable} for this divider.
+     *
+     * @param drawable Drawable that should be used as a divider.
+     */
+    public void setDrawable(@NonNull Drawable drawable) {
+        if (drawable == null) {
+            throw new IllegalArgumentException("Drawable cannot be null.");
+        }
+        mDivider = drawable;
+    }
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        if (parent.getLayoutManager() == null || mDivider == null) {
+            return;
+        }
+        if (mOrientation == VERTICAL) {
+            drawVertical(c, parent);
+        } else {
+            drawHorizontal(c, parent);
+        }
+    }
+
+    private void drawVertical(Canvas canvas, RecyclerView parent) {
+        canvas.save();
+        final int left;
+        final int right;
+        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
+        if (parent.getClipToPadding()) {
+            left = parent.getPaddingLeft();
+            right = parent.getWidth() - parent.getPaddingRight();
+            canvas.clipRect(left, parent.getPaddingTop(), right,
+                    parent.getHeight() - parent.getPaddingBottom());
+        } else {
+            left = 0;
+            right = parent.getWidth();
+        }
+
+        final int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            final View child = parent.getChildAt(i);
+            parent.getDecoratedBoundsWithMargins(child, mBounds);
+            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
+            final int top = bottom - mDivider.getIntrinsicHeight();
+            mDivider.setBounds(left, top, right, bottom);
+            mDivider.draw(canvas);
+        }
+        canvas.restore();
+    }
+
+    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
+        canvas.save();
+        final int top;
+        final int bottom;
+        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
+        if (parent.getClipToPadding()) {
+            top = parent.getPaddingTop();
+            bottom = parent.getHeight() - parent.getPaddingBottom();
+            canvas.clipRect(parent.getPaddingLeft(), top,
+                    parent.getWidth() - parent.getPaddingRight(), bottom);
+        } else {
+            top = 0;
+            bottom = parent.getHeight();
+        }
+
+        final int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            final View child = parent.getChildAt(i);
+            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
+            final int right = mBounds.right + Math.round(child.getTranslationX());
+            final int left = right - mDivider.getIntrinsicWidth();
+            mDivider.setBounds(left, top, right, bottom);
+            mDivider.draw(canvas);
+        }
+        canvas.restore();
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                               RecyclerView.State state) {
+        if (mDivider == null) {
+            outRect.set(0, 0, 0, 0);
+            return;
+        }
+        if (mOrientation == VERTICAL) {
+            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
+        } else {
+            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
+        }
+    }
+}
+

+ 68 - 0
app/src/main/java/core/util/EscapeUnescape.java

@@ -0,0 +1,68 @@
+package core.util;
+
+public class EscapeUnescape {
+
+    public static String escape(String src) {
+        int i;
+        char j;
+        StringBuffer tmp = new StringBuffer();
+        tmp.ensureCapacity(src.length() * 6);
+
+        for (i = 0; i < src.length(); i++) {
+
+            j = src.charAt(i);
+
+            if (Character.isDigit(j) || Character.isLowerCase(j) || Character.isUpperCase(j))
+                tmp.append(j);
+            else if (j < 256) {
+                tmp.append("%");
+                if (j < 16)
+                    tmp.append("0");
+                tmp.append(Integer.toString(j, 16));
+            } else {
+                tmp.append("%u");
+                tmp.append(Integer.toString(j, 16));
+            }
+        }
+        return tmp.toString();
+    }
+
+    public static String unescape(String src) {
+        StringBuffer tmp = new StringBuffer();
+        tmp.ensureCapacity(src.length());
+        int lastPos = 0, pos = 0;
+        char ch;
+        while (lastPos < src.length()) {
+            pos = src.indexOf("%", lastPos);
+            if (pos == lastPos) {
+                if (src.charAt(pos + 1) == 'u') {
+                    ch = (char) Integer.parseInt(src.substring(pos + 2, pos + 6), 16);
+                    tmp.append(ch);
+                    lastPos = pos + 6;
+                } else {
+                    ch = (char) Integer.parseInt(src.substring(pos + 1, pos + 3), 16);
+                    tmp.append(ch);
+                    lastPos = pos + 3;
+                }
+            } else {
+                if (pos == -1) {
+                    tmp.append(src.substring(lastPos));
+                    lastPos = src.length();
+                } else {
+                    tmp.append(src.substring(lastPos, pos));
+                    lastPos = pos;
+                }
+            }
+        }
+        return tmp.toString();
+    }
+
+//    public static void main(String[] args) {
+//        String tmp = "~!@#$%^&*()_+|\\=-,./?><;'][{}\"";
+//        System.out.println("testing escape : " + tmp);
+//        tmp = escape(tmp);
+//        System.out.println(tmp);
+//        System.out.println("testing unescape :" + tmp);
+//        System.out.println(unescape(tmp));
+//    }
+}

+ 29 - 0
app/src/main/java/core/util/Extension.kt

@@ -0,0 +1,29 @@
+package core.util
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.text.Html
+import android.text.Spanned
+
+fun String.fromHtml(): Spanned {
+    return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
+        Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
+    } else {
+        Html.fromHtml(this)
+    }
+}
+
+fun CharSequence?.ifNullOrEmpty(defaultValue: CharSequence = ""): CharSequence {
+    return if (isNullOrEmpty()) defaultValue else this
+}
+
+fun Context.isPkgInstalled(packageName: String?): Boolean {
+    if (packageName.isNullOrEmpty()) return false
+
+    return try {
+        null != packageManager.getPackageInfo(packageName, 0)
+    } catch (e: PackageManager.NameNotFoundException) {
+        false
+    }
+}

+ 39 - 0
app/src/main/java/core/util/FastClickDelegate.kt

@@ -0,0 +1,39 @@
+package core.util
+
+import android.view.View
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class FastClickDelegate(private val listener: ((v: View?) -> Unit)?) :
+    View.OnClickListener {
+
+    companion object {
+        private const val FAST_CLICK_DELAY_TIME = 1000
+    }
+
+    private var mLastClickTime = 1L
+
+    override fun onClick(v: View?) {
+        dispatchFastClick(v)
+    }
+
+    fun dispatchFastClick(v: View? = null) {
+        if (!isFastClick()) {
+            listener?.invoke(v)
+        }
+    }
+
+    private fun isFastClick(): Boolean {
+        var isFastClick = true
+        val currentTimeMillis = System.currentTimeMillis()
+        if (currentTimeMillis - mLastClickTime > FAST_CLICK_DELAY_TIME) {
+            isFastClick = false
+        }
+        mLastClickTime = currentTimeMillis
+        return isFastClick
+    }
+
+}

+ 58 - 0
app/src/main/java/core/util/Format.kt

@@ -0,0 +1,58 @@
+package core.util
+
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object Format {
+
+    /**
+     * 将时区时间格式化成日期字符串
+     *
+     * @param zonedDateTime 2021-03-30T14:18:41.000+08:00
+     */
+    fun formatZonedDateTime(zonedDateTime: String?, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
+        val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.getDefault())
+        val data: Date? = try {
+            dateFormat.parse(zonedDateTime)
+        } catch (e: Exception) {
+            null
+        }
+
+        return formatDate(data, pattern)
+    }
+
+    /**
+     * 将时间戳格式化成yyyy-MM-dd HH:mm:ss格式的日期字符串
+     *
+     * @param dateLong
+     * @return
+     */
+    fun formatTimestamp(dateLong: String?, pattern: String = "yyyy-MM-dd HH:mm:ss"): String {
+        val date: Date? = try {
+            dateLong?.let { Date(it.toLong()) }
+        } catch (e: Exception) {
+            null
+        }
+
+        return formatDate(date, pattern)
+    }
+
+    /**
+     * 按给定的格式格式化日期.
+     *
+     * @param date
+     * @param pattern : 格式化格式
+     * @return
+     */
+    fun formatDate(date: Date?, pattern: String = "yyyy-MM-dd"): String {
+        return date?.let {
+            SimpleDateFormat(pattern, Locale.getDefault()).format(it)
+        } ?: ""
+    }
+
+}

+ 166 - 0
app/src/main/java/core/util/MediaUtils.kt

@@ -0,0 +1,166 @@
+package core.util
+
+import android.content.ContentValues
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import java.io.File
+import java.util.*
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+object MediaUtils {
+
+    /**
+     * 创建一条图片地址uri,用于保存拍照后的照片
+     *
+     * @param context
+     * @param cameraFileName
+     * @param mimeType
+     * @return 图片的uri
+     */
+    fun createImageUri(
+        context: Context,
+        cameraFileName: String,
+        mimeType: String? = "image/jpeg"
+    ): Uri? {
+        // ContentValues是我们希望这条记录被创建时包含的数据信息
+        val values = ContentValues()
+
+        val suffix = cameraFileName.substring(cameraFileName.lastIndexOf("."))
+        val fileName = cameraFileName.replace(suffix, "")
+        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            values.put(MediaStore.Images.Media.DATE_TAKEN, "${System.currentTimeMillis()}")
+        }
+        values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
+
+        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
+        return if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                values.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/Camera")
+            }
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
+        } else {
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values)
+        }
+    }
+
+    /**
+     * 创建一条视频地址uri,用于保存录制的视频
+     *
+     * @param context
+     * @param cameraFileName
+     * @param mimeType
+     * @return 视频的uri
+     */
+    fun createVideoUri(
+        context: Context,
+        cameraFileName: String,
+        mimeType: String? = "video/mp4"
+    ): Uri? {
+        // ContentValues是我们希望这条记录被创建时包含的数据信息
+        val values = ContentValues()
+
+        val suffix = cameraFileName.substring(cameraFileName.lastIndexOf("."))
+        val fileName = cameraFileName.replace(suffix, "")
+        values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            values.put(MediaStore.Video.Media.DATE_TAKEN, "${System.currentTimeMillis()}")
+        }
+        values.put(MediaStore.Video.Media.MIME_TYPE, mimeType)
+
+        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
+        return if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES)
+            }
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)
+        } else {
+            context.applicationContext.contentResolver
+                .insert(MediaStore.Video.Media.INTERNAL_CONTENT_URI, values)
+        }
+    }
+
+    /**
+     * delete camera PATH
+     *
+     * @param context Context
+     * @param cameraPath Camera url
+     */
+    fun deleteCamera(context: Context, cameraPath: String?) {
+        try {
+            if (null != cameraPath && cameraPath.startsWith("content://")) {
+                context.contentResolver.delete(Uri.parse(cameraPath), null, null)
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    fun deleteFile(filepath: String?) {
+        if (!filepath.isNullOrEmpty()) {
+            deleteFile(File(filepath))
+        }
+    }
+
+    fun deleteFile(file: File) {
+        try {
+            file.delete()
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    /**
+     * 创建文件
+     *
+     * @param context 上下文
+     * @param fileName 文件名
+     * @param outCameraDirectory 输出目录
+     * @return
+     */
+    fun createCameraFile(
+        context: Context,
+        fileName: String,
+        outCameraDirectory: String? = null
+    ): File {
+        val folderDir: File
+        if (outCameraDirectory.isNullOrEmpty()) {
+            // 外部没有自定义拍照存储路径使用默认
+            val rootDir: File
+            if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
+                rootDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+                folderDir = File(rootDir.absolutePath + File.separator + "Camera" + File.separator)
+            } else {
+                rootDir =
+                    context.applicationContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
+                folderDir = File(rootDir.absolutePath + File.separator)
+            }
+            if (!rootDir.exists()) {
+                rootDir.mkdirs()
+            }
+        } else {
+            // 自定义存储路径
+            folderDir = File(outCameraDirectory)
+            if (!Objects.requireNonNull(folderDir.parentFile).exists()) {
+                folderDir.parentFile?.mkdirs()
+            }
+        }
+        if (!folderDir.exists()) {
+            folderDir.mkdirs()
+        }
+
+        return File(folderDir, fileName)
+    }
+}

+ 130 - 0
app/src/main/java/core/util/ScreenAdapter.kt

@@ -0,0 +1,130 @@
+package core.util
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Application
+import android.content.ComponentCallbacks
+import android.content.Context
+import android.content.res.Configuration
+import android.util.DisplayMetrics
+
+/**
+ * 屏幕适配
+ *
+ * @author ReiChin_
+ */
+object ScreenAdapter {
+
+    private const val DEFAULT_WIDTH = 1080 // 默认宽度
+
+    private var appDensity = 0f
+
+    /**
+     * 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值
+     */
+    private var appScaledDensity = 0f
+
+    /**
+     * 状态栏高度
+     */
+    private var barHeight: Int = 0
+    private var appDisplayMetrics: DisplayMetrics? = null
+    private var densityScale = 1.0f
+
+    /**
+     * application 层调用,存储默认屏幕密度
+     */
+    fun initAppDensity(application: Application) {
+        // 获取application的DisplayMetrics
+        appDisplayMetrics = application.resources.displayMetrics
+        // 获取状态栏高度
+        barHeight = getStatusBarHeight(application)
+        if (appDensity == 0f) {
+            // 初始化的时候赋值
+            appDensity = appDisplayMetrics!!.density
+            appScaledDensity = appDisplayMetrics!!.scaledDensity
+            // 添加字体变化的监听
+            application.registerComponentCallbacks(object : ComponentCallbacks {
+
+                override fun onConfigurationChanged(newConfig: Configuration) {
+                    // 字体改变后,将appScaledDensity重新赋值
+                    if (newConfig.fontScale > 0) {
+                        appScaledDensity = application.resources.displayMetrics.scaledDensity
+                    }
+                }
+
+                override fun onLowMemory() {
+                }
+            })
+        }
+    }
+
+    /**
+     * 设置自定义的屏幕密度
+     */
+    fun setCustomDensity(activity: Activity) {
+        val targetDensity = appDisplayMetrics!!.widthPixels * 1.0f / DEFAULT_WIDTH
+
+        // 最后在这里将修改过后的值赋给系统参数,只修改Activity的density值
+        val activityDisplayMetrics = activity.resources.displayMetrics.apply {
+            density = targetDensity
+            scaledDensity = targetDensity * (appScaledDensity / appDensity)
+            densityDpi = (160 * targetDensity).toInt()
+        }
+
+        densityScale = appDensity / targetDensity
+        setBitmapDefaultDensity(activityDisplayMetrics.densityDpi)
+    }
+
+    /**
+     * 重置屏幕密度
+     */
+    fun resetAppDensity(activity: Activity) {
+        val activityDisplayMetrics = activity.resources.displayMetrics
+        activityDisplayMetrics.density = appDensity
+        activityDisplayMetrics.scaledDensity = appScaledDensity
+        activityDisplayMetrics.densityDpi = (appDensity * 160).toInt()
+
+        densityScale = 1.0f
+        setBitmapDefaultDensity(activityDisplayMetrics.densityDpi)
+    }
+
+    /**
+     * 获取状态栏高度
+     *
+     * @param context context
+     * @return 状态栏高度
+     */
+    private fun getStatusBarHeight(context: Context): Int {
+        val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
+        return if (resourceId > 0) {
+            context.resources.getDimensionPixelSize(resourceId)
+        } else 0
+    }
+
+    /**
+     * 设置 Bitmap 的默认屏幕密度
+     * 由于 Bitmap 的屏幕密度是读取配置的,导致修改未被启用
+     * 所有,放射方式强行修改
+     * @param defaultDensity 屏幕密度
+     */
+    @SuppressLint("SoonBlockedPrivateApi")
+    private fun setBitmapDefaultDensity(defaultDensity: Int) {
+        try {
+            // 获取单个变量的值
+            val clazz = Class.forName("android.graphics.Bitmap")
+            val field = clazz.getDeclaredField("sDefaultDensity")
+            field.isAccessible = true
+            field.set(null, defaultDensity)
+            field.isAccessible = false
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    /**
+     * 屏幕密度缩放系数
+     */
+    fun getDensityScale() = densityScale
+
+}

+ 50 - 0
app/src/main/java/core/util/VideoFullScreenWebChromeClient.kt

@@ -0,0 +1,50 @@
+package core.util
+
+import android.view.View
+import android.widget.FrameLayout
+import com.tencent.smtt.export.external.interfaces.IX5WebChromeClient
+import com.tencent.smtt.sdk.WebChromeClient
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class VideoFullScreenWebChromeClient(
+    private val fullScreen: FrameLayout
+) : WebChromeClient() {
+
+    // 用于保存系统提供的全屏视频所在的view
+    // 可将view添加到我们自己的视频播放锚点布局中
+    private var mCustomView: View? = null
+    private var mCustomViewCallback: IX5WebChromeClient.CustomViewCallback? = null
+
+    override fun onShowCustomView(view: View?, callback: IX5WebChromeClient.CustomViewCallback?) {
+        super.onShowCustomView(view, callback)
+        // view为全屏时,系统提供的视频展示窗口
+        // 如果view已经存在,则隐藏
+        if (mCustomView != null) {
+            callback?.onCustomViewHidden()
+            return
+        }
+        mCustomView = view
+        mCustomView?.visibility = View.VISIBLE
+        mCustomViewCallback = callback
+        fullScreen.addView(mCustomView)
+        // 仅显示视频锚点布局
+        fullScreen.visibility = View.VISIBLE
+    }
+
+    override fun onHideCustomView() {
+        super.onHideCustomView()
+        if (mCustomView == null) {
+            return
+        }
+        mCustomView?.visibility = View.GONE
+        fullScreen.removeView(mCustomView)
+        mCustomView = null
+
+        mCustomViewCallback?.onCustomViewHidden()
+    }
+
+}

+ 27 - 0
app/src/main/java/core/util/Weak.kt

@@ -0,0 +1,27 @@
+package core.util
+
+import java.lang.ref.WeakReference
+import kotlin.reflect.KProperty
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class Weak<T : Any>(initializer: () -> T?) {
+
+    var weakReference = WeakReference<T?>(initializer())
+
+    constructor() : this({
+        null
+    })
+
+    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+        return weakReference.get()
+    }
+
+    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
+        weakReference = WeakReference(value)
+    }
+
+}

+ 26 - 0
app/src/main/java/core/util/WeakHandler.kt

@@ -0,0 +1,26 @@
+package core.util
+
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class WeakHandler(target: OnWeakListener) : Handler(Looper.getMainLooper()) {
+
+    private val mTarget: OnWeakListener? by Weak { target }
+
+    override fun handleMessage(msg: Message) {
+        mTarget?.onWeakHandleMessage(msg)
+    }
+
+}
+
+interface OnWeakListener {
+
+    fun onWeakHandleMessage(msg: Message)
+
+}

+ 155 - 0
app/src/main/java/core/util/WebViewHelper.kt

@@ -0,0 +1,155 @@
+package core.util
+
+import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Build
+import android.view.View
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.widget.ProgressBar
+import com.tencent.smtt.export.external.interfaces.IX5WebChromeClient
+import com.tencent.smtt.export.external.interfaces.SslError
+import com.tencent.smtt.export.external.interfaces.SslErrorHandler
+import com.tencent.smtt.export.external.interfaces.WebResourceRequest
+import com.tencent.smtt.sdk.*
+
+/**
+ * WebView + ProgressBar帮助类
+ *
+ * @author ReiChin_
+ */
+class WebViewHelper(private val webView: WebView,
+                    private val progressBar: ProgressBar) {
+
+    private val mProgressHelper by lazy { ProgressHelper(progressBar) }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    fun initWebView(webViewClientProxy: WebViewClient? = null,
+                    webChromeClientProxy: WebChromeClient? = null) {
+        webView.settings.apply {
+            // 为拍照而加
+            allowContentAccess = true
+            allowFileAccess = true
+            setAllowFileAccessFromFileURLs(true)
+            setAllowUniversalAccessFromFileURLs(true)
+            loadWithOverviewMode = true
+
+            defaultTextEncodingName = "utf-8"
+
+            setSupportZoom(true)
+
+            // 设置是否支持执行JS,如果设置为true会存在XSS攻击风险
+            javaScriptEnabled = true
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                safeBrowsingEnabled = false
+            }
+
+            useWideViewPort = true
+            // 安全考虑,防止密码泄漏,尤其是root过的手机
+            savePassword = false
+            userAgentString = "${userAgentString}; MYAPP"
+            layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL
+            // 最重要的方法,一定要设置,这就是出不来的主要原因
+            domStorageEnabled = true
+
+//            databaseEnabled = true
+//            setGeolocationEnabled(true)
+//            javaScriptCanOpenWindowsAutomatically = true
+            cacheMode = WebSettings.LOAD_NO_CACHE
+        }
+
+        webView.apply {
+            scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
+            // 水平不显示
+            isHorizontalScrollBarEnabled = false
+            // 垂直不显示
+            isVerticalScrollBarEnabled = false
+            webViewClient = MyWebViewClient(progressBar, webView, webViewClientProxy)
+            webChromeClient = MyWebChromeClient(mProgressHelper, webChromeClientProxy)
+        }
+    }
+
+    private class ProgressHelper(val progressBar: ProgressBar) {
+
+        private val animator: ValueAnimator = ValueAnimator()
+
+        init {
+            animator.duration = 500
+            animator.interpolator = AccelerateDecelerateInterpolator()
+            animator.addUpdateListener { animation ->
+                val value = animation.animatedValue as Int
+                progressBar.progress = value
+                if (value >= 99)
+                    progressBar.visibility = View.GONE
+                else
+                    progressBar.visibility = View.VISIBLE
+            }
+        }
+
+        fun setProgress(progress: Int) {
+            if (progressBar.progress > progress) {
+                animator.cancel()
+                progressBar.progress = 0
+            }
+            if (!animator.isRunning) {
+                animator.setIntValues(progressBar.progress, progress)
+                animator.start()
+            } else {
+                val value = animator.animatedValue as Int
+                animator.cancel()
+                animator.setIntValues(value, progress)
+                animator.start()
+            }
+        }
+    }
+
+    private class MyWebViewClient(val progressBar: ProgressBar,
+                                  val webView: WebView,
+                                  val proxy: WebViewClient? = null) : WebViewClient() {
+
+        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+            super.onPageStarted(view, url, favicon)
+            progressBar.progress = 0
+        }
+
+        override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
+            return proxy?.shouldOverrideUrlLoading(view, request?.url.toString())
+                    ?: super.shouldOverrideUrlLoading(view, request)
+        }
+
+        override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
+            handler?.proceed()
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                webView.settings.mixedContentMode = 0/*android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW*/
+            }
+        }
+
+    }
+
+    private class MyWebChromeClient(val progressHelper: ProgressHelper,
+                                    val proxy: WebChromeClient? = null) : WebChromeClient() {
+
+        override fun onProgressChanged(view: WebView?, newProgress: Int) {
+            progressHelper.setProgress(newProgress)
+        }
+
+        override fun onShowCustomView(view: View?, callback: IX5WebChromeClient.CustomViewCallback?) {
+            super.onShowCustomView(view, callback)
+            proxy?.onShowCustomView(view, callback)
+        }
+
+        override fun onHideCustomView() {
+            super.onHideCustomView()
+            proxy?.onHideCustomView()
+        }
+
+        override fun onShowFileChooser(webView: WebView?,
+                                       filePathCallback: ValueCallback<Array<Uri>>?,
+                                       fileChooserParams: FileChooserParams?): Boolean {
+            return proxy?.onShowFileChooser(webView, filePathCallback, fileChooserParams)
+                    ?: super.onShowFileChooser(webView, filePathCallback, fileChooserParams)
+        }
+    }
+
+}

+ 26 - 0
app/src/main/java/core/util/net/NetChangeListener.kt

@@ -0,0 +1,26 @@
+package core.util.net
+
+/**
+ * Function : 网络变化监听事件
+ * Date:  2020-10-15 10:29
+ * Author: ReiChin_
+ * Version:
+ */
+interface NetChangeListener {
+
+    /**
+     * wifi变为4G
+     */
+    fun onWifiTo4G()
+
+    /**
+     * 4G变为wifi
+     */
+    fun on4GToWifi()
+
+    /**
+     * 网络断开
+     */
+    fun onNetDisconnected()
+
+}

+ 20 - 0
app/src/main/java/core/util/net/NetConnectedListener.kt

@@ -0,0 +1,20 @@
+package core.util.net
+
+/**
+ * Function : 判断是否有网络的监听
+ * Date:  2020-10-15 10:30
+ * Author: ReiChin_
+ * Version:
+ */
+interface NetConnectedListener {
+
+    /**
+     * 网络已连接
+     */
+    fun onReNetConnected(isReconnect: Boolean)
+
+    /**
+     * 网络未连接
+     */
+    fun onNetUnConnected()
+}

+ 143 - 0
app/src/main/java/core/util/net/NetWatchdog.kt

@@ -0,0 +1,143 @@
+package core.util.net
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.NetworkInfo
+import java.lang.Exception
+
+/**
+ * Function :
+ * Date:  2020-10-15 10:02
+ * Author: ReiChin_
+ * Version:
+ */
+class NetWatchdog(context: Context) {
+
+    private val mAppContext = context.applicationContext
+
+    private var isReconnect = false
+
+    // 广播过滤器,监听网络变化
+    private val mNetIntentFilter = IntentFilter()
+
+    init {
+        mNetIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
+    }
+
+    private val mReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            // 获取手机的连接服务管理器,这里是连接管理器类
+            val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+            val wifiNetworkInfo: NetworkInfo? = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
+            val mobileNetworkInfo: NetworkInfo? = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
+            val activeNetworkInfo: NetworkInfo? = cm.getActiveNetworkInfo()
+
+            val wifiState = wifiNetworkInfo?.state ?: NetworkInfo.State.UNKNOWN
+            val mobileState = mobileNetworkInfo?.state ?: NetworkInfo.State.UNKNOWN
+
+            if (activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting) {
+                mNetConnectedListener?.run {
+                    isReconnect = false
+                    onReNetConnected(isReconnect)
+                }
+            } else if (activeNetworkInfo == null){
+                mNetConnectedListener?.run {
+                    isReconnect = true
+                    onNetUnConnected()
+                }
+            }
+
+            if (NetworkInfo.State.CONNECTED != wifiState && NetworkInfo.State.CONNECTED == mobileState) {
+                mNetChangeListener?.onWifiTo4G()
+            } else if (NetworkInfo.State.CONNECTED == wifiState && NetworkInfo.State.CONNECTED != mobileState) {
+                mNetChangeListener?.on4GToWifi()
+            } else if (NetworkInfo.State.CONNECTED != wifiState && NetworkInfo.State.CONNECTED != mobileState) {
+                mNetChangeListener?.onNetDisconnected()
+            }
+        }
+    }
+
+    private var mNetChangeListener: NetChangeListener? = null
+
+    fun setNetChangeListener(listener: NetChangeListener) {
+        mNetChangeListener = listener
+    }
+
+    private var mNetConnectedListener: NetConnectedListener? = null
+
+    fun setNetConnectedListener(listener: NetConnectedListener) {
+        mNetConnectedListener = listener
+    }
+
+    /**
+     * 开始监听
+     */
+    fun startWatch() {
+        try {
+            mAppContext.registerReceiver(mReceiver, mNetIntentFilter)
+        } catch (e: Exception) {
+        }
+    }
+
+    /**
+     * 结束监听
+     */
+    fun stopWatch() {
+        try {
+            mAppContext.unregisterReceiver(mReceiver)
+        } catch (e: Exception) {
+        }
+    }
+
+    companion object {
+        /**
+         * 静态方法获取是否有网络连接
+         *
+         * @param context 上下文
+         * @return 是否连接
+         */
+        fun hasNet(context: Context): Boolean {
+            // 获取手机的连接服务管理器,这里是连接管理器类
+            val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+            val wifiNetworkInfo: NetworkInfo? = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
+            val mobileNetworkInfo: NetworkInfo? = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
+            val activeNetworkInfo: NetworkInfo? = cm.activeNetworkInfo
+
+            val wifiState = wifiNetworkInfo?.state ?: NetworkInfo.State.UNKNOWN
+            val mobileState = mobileNetworkInfo?.state ?: NetworkInfo.State.UNKNOWN
+
+            if (NetworkInfo.State.CONNECTED != wifiState && NetworkInfo.State.CONNECTED != mobileState) {
+                return false
+            }
+            if (activeNetworkInfo == null || !activeNetworkInfo.isConnectedOrConnecting) {
+                return false
+            }
+
+            return true
+        }
+
+        /**
+         * 静态判断是不是4G网络
+         *
+         * @param context 上下文
+         * @return 是否是4G
+         */
+        fun is4GConnected(context: Context): Boolean {
+            // 获取手机的连接服务管理器,这里是连接管理器类
+            val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+            val mobileNetworkInfo: NetworkInfo ? = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
+
+            val mobileState = mobileNetworkInfo?.state ?: NetworkInfo.State.UNKNOWN
+
+            return NetworkInfo.State.CONNECTED == mobileState
+        }
+    }
+
+
+}

+ 49 - 0
app/src/main/java/http/HttpClient.java

@@ -0,0 +1,49 @@
+package http;
+
+import java.util.Objects;
+
+import http.client.retrofit.ApiService;
+import http.converter.NullOnEmptyConverterFactory;
+import http.interceptor.TokenHeaderInterceptor;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
+import retrofit2.converter.gson.GsonConverterFactory;
+import xn.xxp.app.LabApp;
+import xn.xxp.room.RoomTool;
+import xn.xxp.room.bean.DeviceConfig;
+
+public class HttpClient {
+    private static okhttp3.OkHttpClient okHttpClient;
+    private static Retrofit retrofit;
+    private static ApiService apiService;
+
+    public static ApiService createRetrofitApi(Class<ApiService> apiServiceClass) {
+        return Objects.requireNonNullElseGet(apiService, () -> apiService = buildRetrofit().create(apiServiceClass));
+    }
+
+    public static Retrofit buildRetrofit() {
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        if (null != retrofit) {
+            return retrofit;
+        } else {
+            Retrofit.Builder builder = new Retrofit.Builder();
+            builder.client(buildHttpClient());
+            builder.baseUrl(deviceConfig.getBaseUrl());
+            builder.addCallAdapterFactory(RxJava3CallAdapterFactory.create());
+            builder.addConverterFactory(new NullOnEmptyConverterFactory());
+            builder.addConverterFactory(GsonConverterFactory.create(LabApp.gson));
+            return retrofit = builder.build();
+        }
+    }
+
+    public static okhttp3.OkHttpClient buildHttpClient() {
+        if (null != okHttpClient) {
+            return okHttpClient;
+        } else {
+            okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder();
+            builder.addNetworkInterceptor(new TokenHeaderInterceptor());
+            return okHttpClient = builder.build();
+        }
+    }
+
+}

+ 39 - 0
app/src/main/java/http/OkHttpUtils.java

@@ -0,0 +1,39 @@
+package http;
+
+import java.io.IOException;
+
+import okhttp3.Call;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class OkHttpUtils {
+
+    private static final OkHttpClient client = HttpClient.buildHttpClient();
+    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+    private OkHttpUtils() {
+    }
+
+    // 同步 GET 请求
+    public static Response getSync(String url) throws IOException {
+        Request request = new Request.Builder()
+                .url(url)
+                .build();
+        Call call = client.newCall(request);
+        return call.execute();
+    }
+
+    // 同步 POST 请求
+    public static Response postSync(String url, String json) throws IOException {
+        RequestBody body = RequestBody.create(JSON, json);
+        Request request = new Request.Builder()
+                .url(url)
+                .post(body)
+                .build();
+        Call call = client.newCall(request);
+        return call.execute();
+    }
+}

+ 623 - 0
app/src/main/java/http/client/ApiRepository.kt

@@ -0,0 +1,623 @@
+package http.client
+
+import http.client.factory.RetrofitFactory
+import http.exception.NetException
+import http.vo.request.*
+import http.vo.response.*
+import io.reactivex.rxjava3.core.Observable
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import xn.xxp.room.bean.LabConfig
+import xn.xxp.room.bean.NoticeSummary
+import java.io.File
+import java.lang.StringBuilder
+
+object ApiRepository {
+
+    private val mLibClient by lazy { RetrofitFactory().createLabClient()
+    }
+
+    /**
+     * 登录获取token
+     */
+//    fun authOneLogin(param: AccessTokenReq): Observable<Boolean> {
+//        return mLibClient.authOneLogin(param).schedulers()
+//    }
+
+    /**
+     * 查询APK版本
+     *
+     * @param param 设备版本信息
+     */
+    fun apkVersion(param: ApkInfoReq): Observable<ApkInfoResp> {
+        return mLibClient.apkVersion(param).schedulers()
+    }
+
+    /**
+     * 实验室信息
+     *
+     * @param id 设备唯一编码
+     */
+    fun laboratoryInfo(id: String): Observable<LaboratoryVo> {
+        return mLibClient.laboratoryInfo(id).schedulers()
+    }
+
+    /**
+     * 危险源信息
+     *
+     * @param param 实验室Id、分页信息
+     */
+    fun hazardlist(param: HazardReq): Observable<List<LabHazardVo>> {
+        return mLibClient.hazardlist(param).schedulers()
+    }
+
+    /**
+     * 签到验证(进入)
+     *
+     * @param eBoard 是否为电子信息牌
+     * @param subId 实验室id
+     * @param username 学生卡编号/人员id
+     */
+    fun signInCheck(eBoard: Boolean, subId: String, username: String): Observable<SignInCheckResp> {
+        return mLibClient.signInCheck(eBoard, subId, username).schedulers()
+    }
+
+    /**
+     * 签到提交-人脸验证
+     */
+    fun signInFace(code: String, faceFeature: SignInReq? = null): Observable<String> {
+        return mLibClient.signInFace(code, faceFeature).schedulers()
+    }
+
+    /**
+     * 签到-安全准入检测三合一
+     *
+     * @param patrolSign 是否为巡查签到
+     */
+    fun checkInAll(patrolSign: Boolean, param: CheckInAllReq): Observable<Boolean> {
+        return mLibClient.checkInAll(patrolSign, param).schedulers()
+    }
+
+    /**
+     * 签到提交
+     */
+    fun signIn(id: String): Observable<Boolean> {
+        return mLibClient.signIn(id).schedulers()
+    }
+
+    /**
+     * 签到提交-有跳过安全准入检测时使用
+     */
+    fun signInJump(id: String, code: String): Observable<Boolean> {
+        return mLibClient.signInJump(id, code).schedulers()
+    }
+
+    /**
+     * 签到提交
+     * @param jumpSafetyCheck 是否跳过安全检查
+     * @param code 查询人员状态返回的code
+     * @param id 人脸比对成功后返回的id
+     */
+    fun signIn(jumpSafetyCheck: Boolean, code: String, id: String): Observable<Boolean> {
+        return if (jumpSafetyCheck) {
+            signInJump(id, code)
+        } else {
+            signIn(id)
+        }
+    }
+
+    /**
+     * 签到验证(离开)
+     *
+     * @param eBoard 是否为电子信息牌
+     * @param subId 实验室id
+     * @param username 学生卡编号/人员id
+     */
+    fun signOutCheck(
+        eBoard: Boolean,
+        subId: String,
+        username: String
+    ): Observable<SignInCheckResp> {
+        return mLibClient.signOutCheck(eBoard, subId, username).schedulers()
+    }
+
+    /**
+     * 离开提交
+     */
+    fun signOut(code: String): Observable<Boolean> {
+        return mLibClient.signOut(code).schedulers()
+    }
+
+    /**
+     * 获取实验室一体机可控制设备
+     *
+     * @param subId 实验室id
+     */
+    fun controllerList(subId: String): Observable<List<LotDeviceVo>> {
+        return mLibClient.controllerList(subId).schedulers()
+    }
+
+    /**
+     * 控制设备
+     *
+     * @param param 设备编号、命令
+     */
+    fun sendControllerCMD(param: ControllerCMD): Observable<Boolean> {
+        return mLibClient.sendControllerCMD(param).schedulers()
+    }
+
+    /**
+     * 实验室测点功能列表(首页-左侧看板)
+     *
+     * @param subId 实验室id
+     */
+    fun functionList(subId: String): Observable<List<LabBulletinBoardVo>> {
+        return mLibClient.functionList(subId).schedulers()
+    }
+
+    /**
+     * 实验室预警测点
+     *
+     * @param subId 实验室id
+     */
+    fun warnList(subId: String): Observable<List<LabWarnVo>> {
+        return mLibClient.warnList(subId).schedulers()
+    }
+
+    /**
+     * 人脸比对
+     */
+    fun faceCompare(param: FaceCompareReq): Observable<Boolean> {
+        return mLibClient.faceCompare(param).schedulers()
+    }
+
+    /**
+     * 心跳
+     *
+     * @param num 设备唯一编码
+     */
+    fun heartbeat(num: String): Observable<Boolean> {
+        return mLibClient.heartbeat(num).schedulers()
+    }
+
+    /**
+     * 查询实验室安全制度列表
+     *
+     * @param type 1:学校制度 2:学院制度 3: 中心制度
+     */
+    fun safeBookList(safetyListReq: SafetyListReq): Observable<List<SafeBook>> {
+        return mLibClient.safeBookList(safetyListReq).schedulers()
+    }
+
+    /**
+     * 获取实验室安全制度详细信息
+     */
+    fun safeBookDetail(id: String): Observable<SafeBook> {
+        return mLibClient.safeBookDetail(id).schedulers()
+    }
+
+    /**
+     * 一体机查询危化品
+     */
+    fun hazardBookList(): Observable<List<HazardBook>> {
+        return mLibClient.hazardBookList().schedulers()
+    }
+
+    /**
+     * 获取危化品安全技术说明书详细信息
+     */
+    fun hazardBookDetail(id: String): Observable<HazardBook> {
+        return mLibClient.hazardBookDetail(id).schedulers()
+    }
+
+
+    /**
+     * 实验室文化图
+     */
+    fun bannerImages(param: BannerImageReq): Observable<List<BannerImageBean>> {
+        return mLibClient.bannerImages(param).schedulers()
+    }
+
+    /**
+     * 文字转语音
+     */
+    fun textParseVideo(text: String): Observable<SpeakInfo> {
+        return mLibClient.textParseVideo(text).schedulers()
+    }
+
+    /**
+     * 物联控制权限验证
+     */
+    fun lotInCheck(subId: String, username: String): Observable<String> {
+        return mLibClient.lotInCheck(subId, username).schedulers()
+    }
+
+    /**
+     * 查询实验室配置
+     */
+    fun queryLabConfig(deviceNum: String): Observable<LabConfig> {
+        return mLibClient.queryLabConfig(deviceNum).schedulers()
+    }
+
+    fun laboratoryInfo(openEBoard: Boolean, id: String): Observable<LaboratoryVo> {
+        return if (openEBoard) laboratoryInfoEBoard(id) else laboratoryInfo(id)
+    }
+
+    fun laboratoryInfoEBoard(labId: String): Observable<LaboratoryVo> {
+        val observable1 = mLibClient.homeTopInfo(labId)
+        val observable2 = mLibClient.homeMiddleInfo(labId)
+
+        return Observable.zip(observable1, observable2, { top, middle ->
+            LaboratoryVo().apply {
+                subId = labId
+                rectangleLogo = top.circularLogo
+                subName = top.subjectName
+
+                dangerColor = middle.filedColor
+                dangerName = middle.typeName
+                dangerLevel = middle.levelName
+                address = middle.subjectName
+                deptName = middle.deptName
+                qrCodeUrl = middle.qrCodeUrl
+                adminName = middle.adminName
+                adminPhone = middle.adminPhone
+                adminUserDesc = "${middle.adminName ?: ""}  |  ${middle.adminPhone ?: ""}"
+                safeUserDesc = middle.safeUserVoList?.fold("") { acc, safeUserVo ->
+                    "$acc  ${safeUserVo.safeUserName ?: ""}  |  ${safeUserVo.safeUserPhone ?: ""}"
+                }
+                buildName = middle.buildName
+                room = middle.room
+                // 责任单位、实验室负责人
+                val safePersons = mutableListOf(
+                    LaboratoryVo.SafePersonInfo("责任单位:", middle.deptName),
+                    LaboratoryVo.SafePersonInfo(
+                        "实验室负责人:",
+                        middle.adminName,
+                        middle.adminPhone
+                    ),
+                )
+                // 安全责任人
+                safePersonList = middle.safeUserVoList?.mapTo(safePersons) { item ->
+                    LaboratoryVo.SafePersonInfo(
+                        "安全责任人:",
+                        item.safeUserName,
+                        item.safeUserPhone
+                    )
+                } ?: safePersons
+                val specialList = middle.brandInfoSubjectVos?.filter { it.isSpecial == "1" }
+                specialClassify = specialList?.flatMap { it.classifyList }?.let { list ->
+                    mutableListOf<String>().apply {
+                        for (i in list.indices step 3) {
+                            val builder = StringBuilder()
+                            builder.append(list[i])
+                            if (i + 1 < list.size) builder.append("    ").append(list[i + 1])
+                            if (i + 2 < list.size) builder.append("    ").append(list[i + 2])
+                            add(builder.toString())
+                        }
+                    }
+                }
+                specialClassify2 = specialList?.map { it.classifyList }
+                safeClassifyList = middle.brandInfoSubjectVos?.filter { it.isSpecial != "1" }
+                inCheck = middle.inCheck
+                inspectInCheck = middle.inspectInCheck
+                outCheck = middle.outCheck
+            }
+        }).schedulers()
+    }
+
+    /**
+     * 查询首页右侧人员信息
+     */
+    fun homeRightInfo(labId: String): Observable<HomeRightResp> {
+        return mLibClient.homeRightInfo(labId).schedulers()
+    }
+
+    /**
+     * 实验室介绍
+     */
+    fun labIntro(labId: String): Observable<String> {
+        return mLibClient.labIntro(labId).schedulers()
+    }
+
+    /**
+     * 查询值班人员列表
+     */
+    fun dutyUserList(labId: String, startTime: String): Observable<DutyPersonVo> {
+        return mLibClient.dutyUserList(labId, startTime).schedulers()
+    }
+
+    /**
+     * 查询实验人员列表
+     */
+    fun signUserList(labId: String): Observable<List<LabPersonVo>> {
+        return mLibClient.signUserList(labId).schedulers()
+    }
+
+    /**
+     * 查询准入人员列表
+     */
+    fun securityUserList(
+        labId: String,
+        pageNumber: Int,
+        pageSize: Int
+    ): Observable<List<LabPersonVo>> {
+        return mLibClient.securityUserList(labId, pageNumber, pageSize).schedulers()
+    }
+
+    /**
+     * 查询巡查人员列表
+     */
+    fun inspectUserList(labId: String, startTime: String): Observable<DutyPersonVo> {
+        return mLibClient.inspectUserList(labId, startTime).schedulers()
+    }
+
+    /**
+     * 查询值班人员/巡查人员
+     * @param type 1-值班人员 2-巡查人员
+     */
+    fun queryDutyPerson(type: Int, labId: String, startTime: String): Observable<DutyPersonVo> {
+        return if (1 == type) dutyUserList(labId, startTime) else inspectUserList(labId, startTime)
+    }
+
+    /**
+     * 查询实验人员/准入人员
+     * @param type 1-实验人员 2-准入人员
+     */
+    fun queryAccessPerson(
+        type: Int,
+        labId: String,
+        pageNumber: Int,
+        pageSize: Int
+    ): Observable<List<LabPersonVo>> {
+        return if (1 == type) signUserList(labId) else securityUserList(labId, pageNumber, pageSize)
+    }
+
+    /**
+     * 视频监控
+     */
+    fun cameraBySubjectId(
+        labId: String,
+        userId: String,
+        username: String,
+        source: Int
+    ): Observable<List<MonitorVo>> {
+        return mLibClient.cameraBySubjectId(labId, userId, username, source).schedulers()
+    }
+
+    /**
+     * 获取准入人员的指纹信息列表
+     */
+    fun getFingerList(labId: String): Observable<List<UserFingerVo>> {
+        return mLibClient.getFingerList(labId).schedulers()
+    }
+
+    /**
+     * 根据用户查询指纹列表
+     */
+    fun getFingerByUserId(labId: String, userId: String): Observable<List<UserFingerVo>> {
+        return mLibClient.getFingerByUserId(labId, userId).schedulers()
+    }
+
+    /**
+     * 指纹录入
+     */
+    fun addUserFinger(param: UserFingerVo): Observable<Boolean> {
+        return mLibClient.addUserFinger(param).schedulers()
+    }
+
+    /**
+     * 删除指纹
+     */
+    fun deleteFingerById(id: String): Observable<Boolean> {
+        return mLibClient.deleteFingerById(id).schedulers()
+    }
+
+    /**
+     * 获取人像特征值
+     */
+    fun faceFeature(file: File): Observable<String> {
+        return mLibClient.faceFeature(file).schedulers()
+    }
+
+    /**
+     * 人脸识别
+     */
+    fun authFace(file: File, labId: String): Observable<UserVo> {
+        return mLibClient.faceFeature(file)
+            .flatMap {
+                val faceAuthReq =
+                    AuthFaceReq(it, labId)
+                mLibClient.multiFaceDetection(faceAuthReq)
+            }.schedulers()
+    }
+
+    /**
+     * 刷卡验证
+     */
+    fun authCard(labId: String, cardNum: String): Observable<UserVo> {
+        return mLibClient.cardValidate(labId, cardNum).schedulers()
+    }
+
+    /**
+     * 密码验证
+     */
+    fun authPassword(param: AuthPwdReq): Observable<UserVo> {
+        return mLibClient.pwdValidate(param).schedulers()
+    }
+
+    /**
+     * 签到人像比对
+     */
+    fun signFaceMatching(file: File, code: String): Observable<String> {
+        return mLibClient.faceFeature(file)
+            .flatMap { feature ->
+                val param = SignInReq()
+                param.data = feature.toByteArray()
+                mLibClient.signInFace(code, param)
+            }.schedulers()
+    }
+
+    /**
+     * 离开人像比对
+     */
+    fun leaveFaceMatching(file: File, userId: String): Observable<Boolean> {
+        return mLibClient.faceFeature(file)
+            .flatMap { feature ->
+                val param = FaceCompareReq()
+                param.data = feature.toByteArray()
+                param.userId = userId
+                mLibClient.faceCompare(param)
+            }.schedulers()
+    }
+
+    /**
+     * 校园卡是否能开启门禁
+     */
+    fun getCardIsOpen(labId: String, cardNum: String): Observable<UserVo> {
+        return mLibClient.getCardIsOpen(labId, cardNum).schedulers()
+    }
+
+    /**
+     * 巡查签到
+     */
+    fun signInWithPatrol(param: PatrolSignInReq): Observable<Boolean> {
+        return mLibClient.signInWithPatrol(param).schedulers()
+    }
+
+    /**
+     * 巡查签退
+     */
+    fun signOutWithPatrol(param: PatrolSignOutReq): Observable<Boolean> {
+        return mLibClient.signOutWithPatrol(param.labId, param.userId).schedulers()
+    }
+
+    /**
+     * 巡查签退/准入签退判断
+     *  true-巡查 false-准入
+     */
+    fun isSignInType(labId: String, userId: String): Observable<Boolean> {
+        return mLibClient.isSignInType(labId, userId).schedulers()
+    }
+
+    /**
+     * 准入签到:跳过人脸,传空数据即可
+     *
+     * @param code 查询人员状态返回的code
+     *
+     */
+    fun jumpFaceSignIn(code: String): Observable<Boolean> {
+        return mLibClient.signInFace(code, SignInReq())
+            .flatMap { id ->
+                mLibClient.signInJump(id, code)
+            }.schedulers()
+    }
+
+    /**
+     * 信息牌准入签到,前置检测
+     */
+    fun accessSignCheck(param: CommonSignInReq): Observable<SignFaceVo> {
+        return mLibClient.signInCheck(true, param.labId, param.userId)
+            .flatMap { response ->
+                if (!response.state) {
+                    val message =
+                        if (response.message.isNullOrEmpty()) "核验失败,请联系管理员!" else response.message[0]
+                    throw NetException("500", message)
+                }
+                mLibClient.signInFace(response.code, SignInReq())
+                    .map { SignFaceVo(it, response.code) }
+            }.schedulers()
+    }
+
+    /**
+     * 巡查签到、准入签到(无AI检查) 通用接口
+     *
+     * 一、巡查签到
+     *   #signInCheckWithPatrol labId userId
+     *   #signInWithPatrol num userId
+     *
+     * 二、准入签到(无AI检查)
+     *   #signInCheck(true, labId, userId)
+     *   #jumpFaceSignIn(code)
+     *
+     * @param signType 1-准入签到 2-巡查签到
+     */
+    fun commonSignIn(signType: Int, param: CommonSignInReq): Observable<Boolean> {
+        return if (2 == signType) {
+            mLibClient.signInCheckWithPatrol(param.labId, param.userId)
+                .flatMap {
+                    val reqParam = PatrolSignInReq().apply {
+                        num = param.num
+                        userId = param.userId
+                        subjectId = param.labId
+                    }
+                    mLibClient.signInWithPatrol(reqParam)
+                }.schedulers()
+        } else {
+            mLibClient.signInCheck(true, param.labId, param.userId)
+                .flatMap { response ->
+                    if (!response.state) {
+                        val message =
+                            if (response.message.isNullOrEmpty()) "核验失败,请联系管理员!" else response.message[0]
+                        throw NetException("500", message)
+                    }
+                    mLibClient.signInFace(response.code, SignInReq())
+                        .map { SignFaceVo(it, response.code) }
+                }.flatMap { face ->
+                    mLibClient.signInJump(face.id, face.code)
+                }.schedulers()
+        }
+    }
+
+    /**
+     * 巡查签退、准入签退(无离开检查项) 通用接口
+     *
+     * 一、巡查签退
+     *   #signOutWithPatrol labId userId
+     *
+     * 二、准入签退(无离开检查项)
+     *   #signOutCheck(true, labId, userId)
+     *   #signOut(code)
+     *
+     * @param signType 1-准入签到 2-巡查签到
+     */
+    fun commonLeave(signType: Int, labId: String, userId: String): Observable<Boolean> {
+        return if (2 == signType) {
+            mLibClient.signOutWithPatrol(labId, userId).schedulers()
+        } else {
+            mLibClient.signOutCheck(true, labId, userId)
+                .flatMap { response ->
+                    if (!response.state) {
+                        val message =
+                            if (response.message.isNullOrEmpty()) "核验失败,请联系管理员!" else response.message[0]
+                        throw NetException("500", message)
+                    }
+                    mLibClient.signOut(response.code)
+                }.schedulers()
+        }
+    }
+
+    /**
+     * 巡查签到,前置检测
+     */
+    fun patrolSignCheck(param: CommonSignInReq): Observable<Boolean> {
+        return mLibClient.signInCheckWithPatrol(param.labId, param.userId).schedulers()
+    }
+
+    /**
+     * 滚动消息列表
+     */
+    fun newMsgGroup(param: NoticeReq): Observable<List<NoticeSummary>> {
+        return mLibClient.newMsgGroup(param).schedulers()
+    }
+
+
+    fun terminalAuth(param: TerminalAuthReq): Observable<String> {
+        return mLibClient.terminalAuth(param).schedulers()
+    }
+
+}
+
+fun <T : Any> Observable<T>.schedulers(): Observable<T> {
+    return this.subscribeOn(Schedulers.io())
+        .observeOn(AndroidSchedulers.mainThread())
+}

+ 128 - 0
app/src/main/java/http/client/HttpTool.java

@@ -0,0 +1,128 @@
+package http.client;
+
+import android.net.Uri;
+import android.util.Log;
+
+import com.blankj.utilcode.util.GsonUtils;
+import com.blankj.utilcode.util.LogUtils;
+
+import http.OkHttpUtils;
+import http.vo.request.TerminalAuthReq;
+
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import okhttp3.Response;
+import xn.xxp.room.RoomTool;
+import xn.xxp.room.bean.DeviceConfig;
+import xn.xxp.room.bean.LabConfig;
+
+public final class HttpTool {
+
+    public static Response terminalAuth(String devId) throws IOException {
+        TerminalAuthReq terminalAuthReq = new TerminalAuthReq();
+        terminalAuthReq.setDeviceCode(devId);
+        terminalAuthReq.setCode("aio_infobord");
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        return OkHttpUtils.postSync(deviceConfig.getBaseUrl() + "terminal/authorize", GsonUtils.toJson(terminalAuthReq));
+    }
+
+    public static Response queryLabConfig(String devId) throws IOException {
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        return OkHttpUtils.getSync(deviceConfig.getBaseUrl() + "terminal/lab/getSubjectIdByNum/" + devId);
+    }
+
+    public static String checkUrl(String url) {
+        try {
+            String uriStr = url;
+            if (!url.matches("https?://.*") && !url.matches("http?://.*")) {
+                DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+                uriStr = Uri.parse(deviceConfig.getBaseUrl() + url).toString();
+            }
+            return uriStr;
+        } catch (Exception e) {
+            LogUtils.e(Log.getStackTraceString(e));
+            return url;
+        }
+    }
+
+    public static Response getLabFaceList() {
+        try {
+            DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+            LabConfig labConfig = RoomTool.getInstance().labConfigDao().getLabConfig();
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("subId", labConfig.getLabId());
+            return OkHttpUtils.postSync(deviceConfig.getBaseUrl() + "terminal/sys/face/data/sub", jsonObject.toString());
+        } catch (Exception e) {
+            LogUtils.e(Log.getStackTraceString(e));
+        }
+        return null;
+    }
+
+    public static Response getRectifyAdviceNoteView(String noticeId) throws IOException {
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        return OkHttpUtils.getSync(deviceConfig.getBaseUrl() + "terminal/sec/getRectifyAdviceNoteView?noticeId=" + noticeId);
+    }
+
+    public static Response getLabStatusById(String labId, String userId) throws IOException {
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        return OkHttpUtils.getSync(deviceConfig.getBaseUrl() + "terminal/lab/getLabStatusById?labId=" + labId + "&userId=" + userId);
+    }
+
+    public static Response heartbeat(String devId) throws IOException {
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        return OkHttpUtils.getSync(deviceConfig.getBaseUrl() + "terminal/device/aio/heartbeat?deviceNo=" + devId);
+    }
+
+
+    /**
+     * 设备状态
+     *
+     * @param metrics json字符串 记录设备状态
+     */
+    public static Response deviceStatus(String devId, String metrics) {
+        try {
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("deviceNo", devId);
+            jsonObject.put("metrics", metrics);
+            DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+            return OkHttpUtils.postSync(deviceConfig.getBaseUrl() + "terminal/device/aio/upload/status", jsonObject.toString());
+        } catch (Exception e) {
+            LogUtils.e(Log.getStackTraceString(e));
+        }
+        return null;
+    }
+
+    /**
+     * 远程开锁发送状态
+     */
+    public static Response remoteUnlock(long messageId) {
+        try {
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("messageId", messageId);
+            jsonObject.put("status", 1);
+            DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+            return OkHttpUtils.postSync(deviceConfig.getBaseUrl() + "terminal/lock/callback", jsonObject.toString());
+        } catch (Exception e) {
+            LogUtils.e(Log.getStackTraceString(e));
+        }
+        return null;
+    }
+
+    /**
+     * 查询二维码登录状态
+     *
+     * @param code 时间
+     */
+    public static Response infobordLogin(String devId, String code) throws IOException {
+        Map<String, Object> map = new HashMap<>();
+        map.put("core", code);
+        map.put("macId", devId);
+        LogUtils.json(map);
+        DeviceConfig deviceConfig = RoomTool.getInstance().deviceConfigDao().getDeviceConfig();
+        return OkHttpUtils.postSync(deviceConfig.getBaseUrl() + "/app/board/infobordLogin", GsonUtils.toJson(map));
+    }
+}

+ 305 - 0
app/src/main/java/http/client/LabClient.kt

@@ -0,0 +1,305 @@
+package http.client
+
+import http.vo.CommonRowsResponse
+import http.vo.request.*
+import http.vo.response.*
+import io.reactivex.rxjava3.core.Observable
+import xn.xxp.room.bean.LabConfig
+import xn.xxp.room.bean.NoticeSummary
+import java.io.File
+
+interface LabClient {
+
+    /**
+     * 登录获取token
+     */
+//    fun authOneLogin(param: AccessTokenReq): Observable<Boolean>
+
+    /**
+     * 查询APK版本
+     *
+     * @param param 设备版本信息
+     */
+    fun apkVersion(param: ApkInfoReq): Observable<ApkInfoResp>
+
+    /**
+     * 实验室信息
+     *
+     * @param id 设备唯一编码
+     */
+    fun laboratoryInfo(id: String): Observable<LaboratoryVo>
+
+    /**
+     * 危险源信息
+     *
+     * @param param 实验室Id、分页信息
+     */
+    fun hazardlist(param: HazardReq): Observable<List<LabHazardVo>>
+
+    /**
+     * 签到验证(进入)
+     *
+     * @param eBoard 是否为电子信息牌
+     * @param subId 实验室id
+     * @param username 学生卡编号/人员id
+     */
+    fun signInCheck(eBoard: Boolean, subId: String, username: String): Observable<SignInCheckResp>
+
+    /**
+     * 签到提交-人脸验证
+     */
+    fun signInFace(code: String, faceFeature: SignInReq? = null): Observable<String>
+
+    /**
+     * 签到-安全准入检测三合一
+     *
+     * @param patrolSign 是否为巡查签到
+     */
+    fun checkInAll(patrolSign: Boolean, param: CheckInAllReq): Observable<Boolean>
+
+    /**
+     * 签到提交
+     */
+    fun signIn(id: String): Observable<Boolean>
+
+    /**
+     * 签到提交-有跳过安全准入检测时使用
+     */
+    fun signInJump(id: String, code: String): Observable<Boolean>
+
+    /**
+     * 签到验证(离开)
+     *
+     * @param eBoard 是否为电子信息牌
+     * @param subId 实验室id
+     * @param username 学生卡编号/人员id
+     */
+    fun signOutCheck(eBoard: Boolean, subId: String, username: String): Observable<SignInCheckResp>
+
+    /**
+     * 离开提交
+     */
+    fun signOut(code: String): Observable<Boolean>
+
+    /**
+     * 获取实验室一体机可控制设备
+     *
+     * @param subId 实验室id
+     */
+    fun controllerList(subId: String): Observable<List<LotDeviceVo>>
+
+    /**
+     * 控制设备
+     *
+     * @param param 设备编号、命令
+     */
+    fun sendControllerCMD(param: ControllerCMD): Observable<Boolean>
+
+    /**
+     * 实验室测点功能列表(首页-左侧看板)
+     *
+     * @param subId 实验室id
+     */
+    fun functionList(subId: String): Observable<List<LabBulletinBoardVo>>
+
+    /**
+     * 实验室预警测点
+     *
+     * @param subId 实验室id
+     */
+    fun warnList(subId: String): Observable<List<LabWarnVo>>
+
+    /**
+     * 人脸比对
+     */
+    fun faceCompare(param: FaceCompareReq): Observable<Boolean>
+
+    /**
+     * 心跳
+     *
+     * @param num 设备唯一编码
+     */
+    fun heartbeat(num: String): Observable<Boolean>
+
+    /**
+     * 查询实验室安全制度列表
+     *
+     * @param type 1:学校制度 2:学院制度 3: 中心制度
+     */
+    fun safeBookList(safetyListReq: SafetyListReq): Observable<List<SafeBook>>
+
+    /**
+     * 获取实验室安全制度详细信息
+     */
+    fun safeBookDetail(id: String): Observable<SafeBook>
+
+    /**
+     * 一体机查询危化品
+     */
+    fun hazardBookList(): Observable<List<HazardBook>>
+
+    /**
+     * 获取危化品安全技术说明书详细信息
+     */
+    fun hazardBookDetail(id: String): Observable<HazardBook>
+
+    /**
+     * 查询实验室在线人员
+     */
+    fun onlineUser(param: OnLineUserReq): Observable<CommonRowsResponse<LabPersonVo>>
+
+    /**
+     * 实验室安全整改信息
+     */
+    fun checkMachineMsgList(subId: String): Observable<List<CheckMachineVo>>
+
+    /**
+     * 实验室文化图
+     */
+    fun bannerImages(param: BannerImageReq): Observable<List<BannerImageBean>>
+
+    /**
+     * 文字转语音
+     */
+    fun textParseVideo(text: String): Observable<SpeakInfo>
+
+    /**
+     * 物联控制权限验证
+     */
+    fun lotInCheck(subId: String, username: String): Observable<String>
+
+    /**
+     * 查询实验室配置
+     */
+    fun queryLabConfig(deviceNum: String): Observable<LabConfig>
+
+    /**
+     * 查询首页头部信息
+     */
+    fun homeTopInfo(labId: String): Observable<HomeTopResp>
+
+    /**
+     * 查询首页中部信息
+     */
+    fun homeMiddleInfo(labId: String): Observable<HomeMiddleResp>
+
+    /**
+     * 查询首页右侧人员信息
+     */
+    fun homeRightInfo(labId: String): Observable<HomeRightResp>
+
+    /**
+     * 实验室介绍
+     */
+    fun labIntro(labId: String): Observable<String>
+
+    /**
+     * 查询值班人员列表
+     */
+    fun dutyUserList(labId: String, startTime: String): Observable<DutyPersonVo>
+
+    /**
+     * 查询实验人员列表
+     */
+    fun signUserList(labId: String): Observable<List<LabPersonVo>>
+
+    /**
+     * 查询准入人员列表
+     */
+    fun securityUserList(
+        labId: String,
+        pageNumber: Int,
+        pageSize: Int
+    ): Observable<List<LabPersonVo>>
+
+    /**
+     * 查询巡查人员列表
+     */
+    fun inspectUserList(labId: String, startTime: String): Observable<DutyPersonVo>
+
+    /**
+     * 视频监控
+     */
+    fun cameraBySubjectId(
+        labId: String,
+        userId: String,
+        userName: String,
+        source: Int
+    ): Observable<List<MonitorVo>>
+
+    /**
+     * 获取准入人员的指纹信息列表
+     */
+    fun getFingerList(labId: String): Observable<List<UserFingerVo>>
+
+    /**
+     * 根据用户查询指纹列表
+     */
+    fun getFingerByUserId(labId: String, userId: String): Observable<List<UserFingerVo>>
+
+    /**
+     * 指纹录入
+     */
+    fun addUserFinger(param: UserFingerVo): Observable<Boolean>
+
+    /**
+     * 删除指纹
+     */
+    fun deleteFingerById(id: String): Observable<Boolean>
+
+    /**
+     * 获取人像特征值
+     */
+    fun faceFeature(file: File): Observable<String>
+
+    /**
+     * 人脸识别
+     */
+    fun multiFaceDetection(param: AuthFaceReq): Observable<UserVo>
+
+    /**
+     * 刷卡验证
+     */
+    fun cardValidate(labId: String, cardNum: String): Observable<UserVo>
+
+    /**
+     * 密码验证
+     */
+    fun pwdValidate(param: AuthPwdReq): Observable<UserVo>
+
+    /**
+     * 校园卡是否能开启门禁
+     */
+    fun getCardIsOpen(labId: String, cardNum: String): Observable<UserVo>
+
+    /**
+     * 巡查签到
+     */
+    fun signInWithPatrol(param: PatrolSignInReq): Observable<Boolean>
+
+    /**
+     * 巡查签退
+     */
+    fun signOutWithPatrol(labId: String, userId: String): Observable<Boolean>
+
+    /**
+     * 巡查签退/准入签退判断
+     *  true-巡查 false-准入
+     */
+    fun isSignInType(labId: String, userId: String): Observable<Boolean>
+
+    /**
+     * 巡查签到前置校验
+     */
+    fun signInCheckWithPatrol(labId: String, userId: String): Observable<Boolean>
+
+    /**
+     * 滚动消息列表
+     */
+    fun newMsgGroup(param: NoticeReq): Observable<List<NoticeSummary>>
+
+    /**
+     * 鉴权
+     */
+    fun terminalAuth(param: TerminalAuthReq): Observable<String>
+}

+ 9 - 0
app/src/main/java/http/client/factory/ClientFactory.kt

@@ -0,0 +1,9 @@
+package http.client.factory
+
+import http.client.LabClient
+
+interface ClientFactory {
+
+    fun createLabClient(): LabClient
+
+}

+ 9 - 0
app/src/main/java/http/client/factory/RetrofitFactory.kt

@@ -0,0 +1,9 @@
+package http.client.factory
+
+import http.client.retrofit.LabRetrofit
+
+class RetrofitFactory : ClientFactory {
+
+    override fun createLabClient() = LabRetrofit()
+
+}

+ 400 - 0
app/src/main/java/http/client/retrofit/ApiService.java

@@ -0,0 +1,400 @@
+package http.client.retrofit;
+
+import http.vo.CommonDataResponse;
+import http.vo.CommonResponse;
+import http.vo.CommonRowsResponse;
+import http.vo.request.AuthFaceReq;
+import http.vo.request.BannerImageReq;
+import http.vo.request.FaceCompareReq;
+import http.vo.request.LotDeviceReq;
+import http.vo.request.MonitorReq;
+import http.vo.request.NoticeReq;
+import http.vo.request.OnLineUserReq;
+import http.vo.request.PatrolSignInReq;
+import http.vo.request.SafetyListReq;
+import http.vo.request.SignInReq;
+import http.vo.request.SubIdReq;
+import http.vo.request.TerminalAuthReq;
+import http.vo.response.ApkInfoResp;
+import http.vo.response.BannerImageBean;
+import http.vo.response.CheckMachineVo;
+import http.vo.response.DutyPersonVo;
+import http.vo.response.HazardBook;
+import http.vo.response.HomeMiddleResp;
+import http.vo.response.HomeRightResp;
+import http.vo.response.HomeTopResp;
+import http.vo.response.LabBulletinBoardVo;
+import xn.xxp.room.bean.LabConfig;
+import http.vo.response.LabHazardVo;
+import http.vo.response.LabPersonVo;
+import http.vo.response.LabWarnVo;
+import http.vo.response.LaboratoryVo;
+import http.vo.response.LotDeviceVo;
+import http.vo.response.MonitorVo;
+import xn.xxp.room.bean.NoticeSummary;
+import http.vo.response.RecordsResponse;
+import http.vo.response.SafeBook;
+import http.vo.response.SignInCheckResp;
+import http.vo.response.SpeakInfo;
+import http.vo.response.UserFingerVo;
+import http.vo.response.UserVo;
+
+import java.util.List;
+import java.util.Map;
+
+import io.reactivex.rxjava3.core.Observable;
+import okhttp3.MultipartBody;
+import okhttp3.RequestBody;
+import retrofit2.http.Body;
+import retrofit2.http.Field;
+import retrofit2.http.FormUrlEncoded;
+import retrofit2.http.GET;
+import retrofit2.http.Multipart;
+import retrofit2.http.POST;
+import retrofit2.http.Part;
+import retrofit2.http.PartMap;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+import retrofit2.http.QueryMap;
+
+public interface ApiService {
+
+    /**
+     * ZD-A002: 查询APK版本
+     * <p>
+     * code: aio_infobord/aio_manager
+     * deviceNo:
+     * version:
+     */
+    @GET("iot/aio/report")
+    Observable<CommonDataResponse<ApkInfoResp>> apkVersion(@QueryMap Map<String, String> filters);
+
+    /**
+     * ZD-A004: 实验室信息
+     */
+    @GET("terminal/lab/onemachine/{id}")
+    Observable<CommonDataResponse<LaboratoryVo>> laboratoryInfo(@Path("id") String id);
+
+    /**
+     * ZD-A005: 危险源信息
+     */
+    @GET("terminal/lab/onemachine/{subId}/Hazard/list")
+    Observable<CommonDataResponse<List<LabHazardVo>>> hazardlist(@Path("subId") String subId, @Query("pageNum") int pageNum, @Query("pageSize") int pageSize);
+
+    /**
+     * ZD-A006: 签到验证(进入)
+     */
+    @GET("terminal/lab/onemachine/{subId}/SignInCheck/{username}")
+    Observable<CommonDataResponse<SignInCheckResp>> signInCheck(@Path("subId") String subId, @Path("username") String username);
+
+    /**
+     * ZD-A007: 签到验证(进入)
+     */
+    @GET("terminal/lab/onemachine/{subId}/SignInXxpCheck/{userId}")
+    Observable<CommonDataResponse<SignInCheckResp>> signInXxpCheck(@Path("subId") String subId, @Path("userId") String userId);
+
+    /**
+     * ZD-A008: 签到提交-人脸验证
+     */
+    @POST("terminal/lab/onemachine/{code}/SignIn2")
+    Observable<CommonDataResponse<String>> signInFace(@Path("core") String code, @Body SignInReq data);
+
+    /**
+     * ZD-A009: 签到-安全准入检测三合一
+     */
+    @Multipart
+    @POST("terminal/lab/signIn/check/checkInAll")
+    Observable<CommonDataResponse<String>> checkInAll(@PartMap Map<String, RequestBody> params, @Part MultipartBody.Part file);
+
+    /**
+     * ZD-A010: 签到-安全准入检测三合一
+     */
+    @Multipart
+    @POST("terminal/lab/signIn/check/checkInXxpAll")
+    Observable<CommonDataResponse<String>> checkInXxpAll(@PartMap Map<String, RequestBody> params, @Part MultipartBody.Part file);
+
+    /**
+     * ZD-A011: 签到提交
+     */
+    @POST("terminal/lab/onemachine/{id}/SignIn/commit")
+    Observable<CommonResponse> signIn(@Path("id") String id);
+
+    /**
+     * ZD-A012: 签到提交-有跳过安全准入检测时使用
+     */
+    @POST("terminal/lab/checklog/{id}/jump/{code}")
+    Observable<CommonResponse> signInJump(@Path("id") String id, @Path("core") String code);
+
+    /**
+     * ZD-A013: 签到验证(离开)
+     */
+    @GET("terminal/lab/onemachine/{subId}/SignOutCheck/{username}")
+    Observable<CommonDataResponse<SignInCheckResp>> signOutCheck(@Path("subId") String subId, @Path("username") String username);
+
+    /**
+     * ZD-A014: 签到验证(离开)
+     */
+    @GET("terminal/lab/onemachine/{subId}/SignOutXXpCheck/{userId}")
+    Observable<CommonDataResponse<SignInCheckResp>> signOutXXpCheck(@Path("subId") String subId, @Path("userId") String userId);
+
+    /**
+     * ZD-A015: 离开提交
+     */
+    @POST("terminal/lab/onemachine/{code}/SignOut")
+    Observable<CommonResponse> signOut(@Path("core") String code);
+
+    /**
+     * ZD-A016: 获取实验室一体机可控制设备
+     */
+    @POST("terminal/iot/onemachine/controller/list")
+    Observable<CommonDataResponse<List<LotDeviceVo>>> controllerList(@Body LotDeviceReq param);
+
+    /**
+     * ZD-A017: 控制设备
+     */
+    @POST("terminal/iot/controlDevice/{id}/{command}")
+    Observable<CommonResponse> sendControllerCMD(@Path("id") String id, @Path("command") String command);
+
+    /**
+     * ZD-A018: 实验室测点功能列表(首页-左侧看板)
+     */
+    @GET("terminal/iot/onemachine/{subId}/function/list")
+    Observable<CommonDataResponse<List<LabBulletinBoardVo>>> functionList(@Path("subId") String subId);
+
+    /**
+     * ZD-A019: 实验室预警测点
+     */
+    @POST("terminal/lab/onemachine/{subId}/noAuthWarn")
+    Observable<CommonDataResponse<List<LabWarnVo>>> warnList(@Path("subId") String subId);
+
+    /**
+     * ZD-A020: 人脸比对
+     */
+    @POST("terminal/sys/user/face/compare")
+    Observable<CommonResponse> faceCompare(@Body FaceCompareReq param);
+
+    /**
+     * ZD-A021: 心跳
+     */
+    @GET("terminal/iot/monitor")
+    Observable<CommonResponse> heartbeat(@Query("deviceNo") String deviceNo);
+
+    /**
+     * ZD-A022: 查询实验室安全制度列表
+     */
+    @POST("terminal/lab/safeBook/queryOptionList")
+    Observable<CommonDataResponse<List<SafeBook>>> safeBookList(@Body SafetyListReq safetyListReq);
+
+    /**
+     * ZD-A023: 获取实验室安全制度详细信息
+     */
+    @GET("terminal/lab/safeBook/info/{id}")
+    Observable<CommonDataResponse<SafeBook>> safeBookDetail(@Path("id") String id);
+
+    /**
+     * ZD-A024: 一体机查询危化品
+     */
+    @GET("chemical/aio/msds")
+    Observable<CommonDataResponse<RecordsResponse<HazardBook>>> hazardBookList();
+
+    /**
+     * ZD-A025: 获取危化品安全技术说明书详细信息
+     */
+    @GET("chemical/aio/msdsDetails")
+    Observable<CommonDataResponse<HazardBook>> hazardBookDetail(@Query("id") String id);
+
+    /**
+     * ZD-A026: 查询实验室在线人员
+     */
+    @POST("terminal/lab/sub/online/user")
+    Observable<CommonRowsResponse<LabPersonVo>> onlineUser(@Body OnLineUserReq param);
+
+    /**
+     * ZD-A027: 实验室安全整改信息
+     */
+    @POST("terminal/lab/aioUnify/warningNoticeList")
+    Observable<CommonDataResponse<RecordsResponse<CheckMachineVo>>> checkMachineMsgList(@Body SubIdReq param);
+
+    /**
+     * ZD-A029: 实验室文化图
+     */
+    @POST("terminal/lab/aioUnify/rotationList")
+    Observable<CommonDataResponse<List<BannerImageBean>>> bannerImages(@Body BannerImageReq param);
+
+    /**
+     * ZD-A030: 文字转语音
+     */
+    @GET("terminal/iot/textParseVoiceUrl")
+    Observable<CommonDataResponse<SpeakInfo>> textParseVideo(@Query("speed") String speed, @Query("volume") String volume, @Query("text") String text);
+
+    /**
+     * ZD-A031: 物联控制权限验证
+     */
+    @GET("terminal/lab/{subId}/LotInCheck/{username}")
+    Observable<CommonDataResponse<String>> lotInCheck(@Path("subId") String subId, @Path("username") String username);
+
+    /**
+     * ZD-A032: 查询实验室配置
+     */
+    @GET("terminal/lab/getSubjectIdByNum/{deviceNum}")
+    Observable<CommonDataResponse<LabConfig>> queryLabConfig(@Path("deviceNum") String deviceNum);
+
+    /**
+     * ZD-A033: 查询首页头部信息
+     */
+    @GET("terminal/lab/topInfo")
+    Observable<CommonDataResponse<HomeTopResp>> homeTopInfo(@Query("labId") String labId);
+
+    /**
+     * ZD-A034: 查询首页中部信息
+     */
+    @GET("terminal/lab/middleInfo")
+    Observable<CommonDataResponse<HomeMiddleResp>> homeMiddleInfo(@Query("labId") String labId);
+
+    /**
+     * ZD-A035: 查询首页右侧人员信息
+     */
+    @GET("terminal/lab/rightInfo")
+    Observable<CommonDataResponse<HomeRightResp>> homeRightInfo(@Query("labId") String labId);
+
+    /**
+     * ZD-A036: 实验室介绍
+     */
+    @GET("terminal/lab/labIntro")
+    Observable<CommonDataResponse<String>> labIntro(@Query("labId") String labId);
+
+    /**
+     * ZD-A037: 查询值班人员列表
+     */
+    @GET("terminal/lab/dutyUserList")
+    Observable<CommonDataResponse<DutyPersonVo>> dutyUserList(@Query("labId") String labId, @Query("startTime") String startTime);
+
+    /**
+     * ZD-A038: 查询实验人员列表
+     */
+    @GET("terminal/lab/signUserList")
+    Observable<CommonDataResponse<List<LabPersonVo>>> signUserList(@Query("labId") String labId);
+
+    /**
+     * ZD-A039: 查询准入人员列表
+     */
+    @GET("terminal/lab/securityUserList")
+    Observable<CommonDataResponse<List<LabPersonVo>>> securityUserList(@Query("labId") String labId, @Query("pageNumber") int pageNumber, @Query("pageSize") int pageSize);
+
+    /**
+     * ZD-A040: 查询巡查人员列表
+     */
+    @GET("terminal/lab/inspectUserList")
+    Observable<CommonDataResponse<DutyPersonVo>> inspectUserList(@Query("labId") String labId, @Query("startTime") String startTime);
+
+    /**
+     * ZD-A041: 视频监控
+     */
+    @POST("terminal/iot/findByCondition")
+    Observable<CommonDataResponse<RecordsResponse<MonitorVo>>> cameraBySubjectId(@Body MonitorReq param);
+
+    /**
+     * ZD-A042: 获取准入人员的指纹信息列表
+     */
+    @GET("terminal/lab/getFingerList")
+    Observable<CommonDataResponse<List<UserFingerVo>>> getFingerList(@Query("labId") String labId);
+
+    /**
+     * ZD-A043: 根据用户查询指纹列表
+     */
+    @GET("terminal/lab/getFingerByUserId")
+    Observable<CommonDataResponse<List<UserFingerVo>>> getFingerByUserId(@Query("labId") String labId,
+                                                                         @Query("userId") String userId);
+
+    /**
+     * ZD-A044: 指纹录入
+     */
+    @POST("terminal/lab/addUserFinger")
+    Observable<CommonResponse> addUserFinger(@Body UserFingerVo param);
+
+    /**
+     * ZD-A045: 删除指纹
+     */
+    @GET("terminal/lab/deleteFingerById")
+    Observable<CommonResponse> deleteFingerById(@Query("id") String id);
+
+    /**
+     * ZD-A046: 获取人像特征值
+     */
+    @Multipart
+    @POST("terminal/sys/faceFeature")
+    Observable<CommonDataResponse<String>> faceFeature(@Part MultipartBody.Part filePart);
+
+    /**
+     * ZD-A047: 人脸识别
+     */
+    @POST("terminal/lab/multiFaceDetection")
+    Observable<CommonDataResponse<UserVo>> multiFaceDetection(@Body AuthFaceReq param);
+
+    /**
+     * ZD-A048: 刷卡验证
+     */
+    @GET("terminal/lab/cardValidate")
+    Observable<CommonDataResponse<UserVo>> cardValidate(@Query("labId") String labId,
+                                                        @Query("cardNum") String cardNum);
+
+    /**
+     * ZD-A049: 密码验证
+     */
+    @GET("terminal/lab/pwdValidate")
+    Observable<CommonDataResponse<UserVo>> pwdValidate(@Query("num") String num,
+                                                       @Query("labId") String labId,
+                                                       @Query("userId") String userId,
+                                                       @Query("pwd") String pwd);
+
+    /**
+     * ZD-A050: 校园卡是否能开启门禁
+     */
+    @GET("terminal/lab/getCardIsOpen")
+    Observable<CommonDataResponse<UserVo>> getCardIsOpen(@Query("labId") String labId,
+                                                         @Query("cardNum") String cardNum);
+
+    /**
+     * ZD-A051: 巡查签到
+     */
+    @POST("terminal/lab/XxpInspection/addInspection/signIn")
+    Observable<CommonResponse> signInWithPatrol(@Body PatrolSignInReq param);
+
+    /**
+     * ZD-A052: 巡查签退
+     */
+    @FormUrlEncoded
+    @POST("terminal/lab/XxpInspection/addInspection/signOut")
+    Observable<CommonResponse> signOutWithPatrol(@Field("labId") String labId,
+                                                 @Field("userId") String userId);
+
+    /**
+     * ZD-A053: 巡查签退/准入签退判断
+     * 1-巡查 2-准入
+     */
+    @FormUrlEncoded
+    @POST("terminal/lab/XxpInspection/isSignInType")
+    Observable<CommonDataResponse<String>> isSignInType(@Field("labId") String labId,
+                                                        @Field("userId") String userId);
+
+    /**
+     * ZD-A054: 巡查签到前置校验
+     */
+    @FormUrlEncoded
+    @POST("terminal/lab/XxpInspection/isSignInspection")
+    Observable<CommonResponse> signInCheckWithPatrol(@Field("labId") String labId,
+                                                     @Field("userId") String userId);
+
+    /**
+     * ZD-A055: 滚动消息列表
+     */
+    @POST("terminal/sys/notice/list")
+    Observable<CommonDataResponse<RecordsResponse<NoticeSummary>>> newMsgGroup(@Body NoticeReq param);
+
+    /**
+     * 鉴权
+     */
+    @POST("terminal/authorize")
+    Observable<CommonDataResponse<String>> terminalAuth(@Body TerminalAuthReq param);
+
+}

+ 596 - 0
app/src/main/java/http/client/retrofit/LabRetrofit.kt

@@ -0,0 +1,596 @@
+package http.client.retrofit
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import http.HttpClient
+import http.client.LabClient
+import http.exception.AICheckException
+import http.exception.NetException
+import http.vo.CommonDataResponse
+import http.vo.CommonResponse
+import http.vo.CommonRowsResponse
+import http.vo.request.*
+import http.vo.response.*
+import io.reactivex.rxjava3.core.Observable
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import xn.xxp.room.bean.LabConfig
+import xn.xxp.room.bean.NoticeSummary
+import java.io.File
+
+open class LabRetrofit : LabClient {
+
+    private val apiService by lazy {
+        HttpClient.createRetrofitApi(ApiService::class.java)
+    }
+
+    /**
+     * 登录获取token
+     */
+//    override fun authOneLogin(param: AccessTokenReq): Observable<Boolean> {
+//        return apiService.authOneLogin(param)
+//            .map { response ->
+//                requireSuccess(response)
+//
+//                HttpClient.token = response.data?.access_token
+//                return@map !HttpClient.token.isNullOrEmpty()
+//            }
+//    }
+
+    /**
+     * 查询APK版本
+     *
+     * @param param 设备版本信息
+     */
+    override fun apkVersion(param: ApkInfoReq): Observable<ApkInfoResp> {
+        val filters = mutableMapOf<String, String>().apply {
+            put("core", param.code)
+            put("deviceNo", param.deviceNo)
+            put("version", param.version)
+        }
+        return apiService.apkVersion(filters)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 实验室信息
+     *
+     * @param id 设备唯一编码
+     */
+    override fun laboratoryInfo(id: String): Observable<LaboratoryVo> {
+        return apiService.laboratoryInfo(id)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 危险源信息
+     *
+     * @param param 实验室Id、分页信息
+     */
+    override fun hazardlist(param: HazardReq): Observable<List<LabHazardVo>> {
+        return apiService.hazardlist(param.subId, param.pageNum, param.pageSize)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 签到验证(进入)
+     *
+     * @param eBoard 是否为电子信息牌
+     * @param subId 实验室id
+     * @param username 学生卡编号/人员id
+     */
+    override fun signInCheck(
+        eBoard: Boolean,
+        subId: String,
+        username: String
+    ): Observable<SignInCheckResp> {
+        val observable = if (eBoard) {
+            apiService.signInXxpCheck(subId, username)
+        } else {
+            apiService.signInCheck(subId, username)
+        }
+        return observable.map(this::dataConvert)
+    }
+
+    /**
+     * 签到提交-人脸验证
+     */
+    override fun signInFace(code: String, faceFeature: SignInReq?): Observable<String> {
+        return apiService.signInFace(code, faceFeature)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 签到-安全准入检测三合一
+     *
+     * @param patrolSign 是否为巡查签到
+     */
+    override fun checkInAll(patrolSign: Boolean, param: CheckInAllReq): Observable<Boolean> {
+        val fileBody = RequestBody.create(
+            "application/octet-stream".toMediaTypeOrNull(),
+            param.file
+        )
+        val filePart = MultipartBody.Part.createFormData("file", param.file.name, fileBody)
+
+        val formMediaType = "multipart/form-data".toMediaTypeOrNull()
+        val bodyPartMap = mutableMapOf<String, RequestBody>()
+        if (!param.id.isNullOrEmpty()) {
+            bodyPartMap["id"] = RequestBody.create(formMediaType, param.id)
+        }
+        if (!param.subId.isNullOrEmpty()) {
+            bodyPartMap["subId"] = RequestBody.create(formMediaType, param.subId)
+        }
+        if (!param.subName.isNullOrEmpty()) {
+            bodyPartMap["subName"] = RequestBody.create(formMediaType, param.subName)
+        }
+
+        val observable = if (patrolSign) {
+            apiService.checkInXxpAll(bodyPartMap, filePart)
+        } else {
+            apiService.checkInAll(bodyPartMap, filePart)
+        }
+        return observable.map { response ->
+            if (!response.isSuccess()) {
+                throw AICheckException(response.code, response.message, response.data)
+            }
+
+            return@map true
+        }
+    }
+
+    /**
+     * 签到提交
+     */
+    override fun signIn(id: String): Observable<Boolean> {
+        return apiService.signIn(id)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 签到提交-有跳过安全准入检测时使用
+     */
+    override fun signInJump(id: String, code: String): Observable<Boolean> {
+        return apiService.signInJump(id, code)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 签到验证(离开)
+     *
+     * @param eBoard 是否为电子信息牌
+     * @param subId 实验室id
+     * @param username 学生卡编号/人员id
+     */
+    override fun signOutCheck(
+        eBoard: Boolean,
+        subId: String,
+        username: String
+    ): Observable<SignInCheckResp> {
+        val observable = if (eBoard) {
+            apiService.signOutXXpCheck(subId, username)
+        } else {
+            apiService.signOutCheck(subId, username)
+        }
+        return observable.map(this::dataConvert)
+    }
+
+    /**
+     * 离开提交
+     */
+    override fun signOut(code: String): Observable<Boolean> {
+        return apiService.signOut(code)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 获取实验室一体机可控制设备
+     *
+     * @param subId 实验室id
+     */
+    override fun controllerList(subId: String): Observable<List<LotDeviceVo>> {
+        val param = LotDeviceReq().apply { subjectId = subId }
+        return apiService.controllerList(param)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 控制设备
+     *
+     * @param param 设备编号、命令
+     */
+    override fun sendControllerCMD(param: ControllerCMD): Observable<Boolean> {
+        return apiService.sendControllerCMD(param.id, param.command)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 实验室测点功能列表(首页-左侧看板)
+     *
+     * @param subId 实验室id
+     */
+    override fun functionList(subId: String): Observable<List<LabBulletinBoardVo>> {
+        return apiService.functionList(subId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 实验室预警测点
+     *
+     * @param subId 实验室id
+     */
+    override fun warnList(subId: String): Observable<List<LabWarnVo>> {
+        return apiService.warnList(subId)
+            .map { response ->
+                requireSuccess(response)
+
+                return@map response.data!!.map { item ->
+                    item.triggerList = Gson().fromJson<List<LabWarnVo.TriggerVo>>(
+                        item.triggerUploadData,
+                        object : TypeToken<List<LabWarnVo.TriggerVo>>() {}.type
+                    )
+                    item
+                }
+            }
+    }
+
+    /**
+     * 人脸比对
+     */
+    override fun faceCompare(param: FaceCompareReq): Observable<Boolean> {
+        return apiService.faceCompare(param)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 心跳
+     *
+     * @param num 设备唯一编码
+     */
+    override fun heartbeat(num: String): Observable<Boolean> {
+        return apiService.heartbeat(num)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 查询实验室安全制度列表
+     *
+     * @param type 1:学校制度 2:学院制度 3: 中心制度
+     */
+    override fun safeBookList(safetyListReq: SafetyListReq): Observable<List<SafeBook>> {
+        return apiService.safeBookList(safetyListReq)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 获取实验室安全制度详细信息
+     */
+    override fun safeBookDetail(id: String): Observable<SafeBook> {
+        return apiService.safeBookDetail(id)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 一体机查询危化品
+     */
+    override fun hazardBookList(): Observable<List<HazardBook>> {
+        return apiService.hazardBookList()
+            .map(this::recordsConvert)
+    }
+
+    /**
+     * 获取危化品安全技术说明书详细信息
+     */
+    override fun hazardBookDetail(id: String): Observable<HazardBook> {
+        return apiService.hazardBookDetail(id)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询实验室在线人员
+     */
+    override fun onlineUser(param: OnLineUserReq): Observable<CommonRowsResponse<LabPersonVo>> {
+        return apiService.onlineUser(param)
+            .map(this::responseConvert)
+    }
+
+    /**
+     * 实验室安全整改信息
+     */
+    override fun checkMachineMsgList(subId: String): Observable<List<CheckMachineVo>> {
+        val param = SubIdReq().apply { this.subId = subId }
+        return apiService.checkMachineMsgList(param)
+            .map(this::recordsConvert)
+    }
+
+    /**
+     * 实验室文化图
+     */
+    override fun bannerImages(param: BannerImageReq): Observable<List<BannerImageBean>> {
+        return apiService.bannerImages(param)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 文字转语音
+     */
+    override fun textParseVideo(text: String): Observable<SpeakInfo> {
+        return apiService.textParseVideo("50", "20", text)
+            .map { response ->
+                requireSuccess(response)
+
+                val data = response.data
+                if ("0" != data?.result) {
+                    throw NetException("1000", "转换异常")
+                }
+                return@map data
+            }
+    }
+
+    /**
+     * 物联控制权限验证
+     */
+    override fun lotInCheck(subId: String, username: String): Observable<String> {
+        return apiService.lotInCheck(subId, username)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询实验室配置
+     */
+    override fun queryLabConfig(deviceNum: String): Observable<LabConfig> {
+        return apiService.queryLabConfig(deviceNum)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询首页头部信息
+     */
+    override fun homeTopInfo(labId: String): Observable<HomeTopResp> {
+        return apiService.homeTopInfo(labId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询首页中部信息
+     */
+    override fun homeMiddleInfo(labId: String): Observable<HomeMiddleResp> {
+        return apiService.homeMiddleInfo(labId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询首页右侧人员信息
+     */
+    override fun homeRightInfo(labId: String): Observable<HomeRightResp> {
+        return apiService.homeRightInfo(labId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 实验室介绍
+     */
+    override fun labIntro(labId: String): Observable<String> {
+        return apiService.labIntro(labId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询值班人员列表
+     */
+    override fun dutyUserList(labId: String, startTime: String): Observable<DutyPersonVo> {
+        return apiService.dutyUserList(labId, startTime)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询实验人员列表
+     */
+    override fun signUserList(labId: String): Observable<List<LabPersonVo>> {
+        return apiService.signUserList(labId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询准入人员列表
+     */
+    override fun securityUserList(
+        labId: String,
+        pageNumber: Int,
+        pageSize: Int
+    ): Observable<List<LabPersonVo>> {
+        return apiService.securityUserList(labId, pageNumber, pageSize)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 查询巡查人员列表
+     */
+    override fun inspectUserList(labId: String, startTime: String): Observable<DutyPersonVo> {
+        return apiService.inspectUserList(labId, startTime)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 视频监控
+     */
+    override fun cameraBySubjectId(
+        labId: String,
+        userId: String,
+        userName: String,
+        source: Int
+    ): Observable<List<MonitorVo>> {
+        val param = MonitorReq().apply {
+            subIds = listOf(labId)
+            this.userId = userId
+            this.userName = userName
+            this.source = source
+        }
+        return apiService.cameraBySubjectId(param)
+            .map(this::recordsConvert)
+    }
+
+    /**
+     * 获取准入人员的指纹信息列表
+     */
+    override fun getFingerList(labId: String): Observable<List<UserFingerVo>> {
+        return apiService.getFingerList(labId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 根据用户查询指纹列表
+     */
+    override fun getFingerByUserId(labId: String, userId: String): Observable<List<UserFingerVo>> {
+        return apiService.getFingerByUserId(labId, userId)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 指纹录入
+     */
+    override fun addUserFinger(param: UserFingerVo): Observable<Boolean> {
+        return apiService.addUserFinger(param)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 删除指纹
+     */
+    override fun deleteFingerById(id: String): Observable<Boolean> {
+        return apiService.deleteFingerById(id)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 获取人像特征值
+     */
+    override fun faceFeature(file: File): Observable<String> {
+        val fileBody = RequestBody.create("application/octet-stream".toMediaTypeOrNull(), file)
+        val filePart = MultipartBody.Part.createFormData("file", file.name, fileBody)
+        return apiService.faceFeature(filePart)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 人脸识别
+     */
+    override fun multiFaceDetection(param: AuthFaceReq): Observable<UserVo> {
+        return apiService.multiFaceDetection(param)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 刷卡验证
+     */
+    override fun cardValidate(labId: String, cardNum: String): Observable<UserVo> {
+        return apiService.cardValidate(labId, cardNum)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 密码验证
+     */
+    override fun pwdValidate(param: AuthPwdReq): Observable<UserVo> {
+        return apiService.pwdValidate(param.num, param.labId, param.userId, param.pwd)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 校园卡是否能开启门禁
+     */
+    override fun getCardIsOpen(labId: String, cardNum: String): Observable<UserVo> {
+        return apiService.getCardIsOpen(labId, cardNum)
+            .map(this::dataConvert)
+    }
+
+    /**
+     * 巡查签到
+     */
+    override fun signInWithPatrol(param: PatrolSignInReq): Observable<Boolean> {
+        return apiService.signInWithPatrol(param)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 巡查签退
+     */
+    override fun signOutWithPatrol(labId: String, userId: String): Observable<Boolean> {
+        return apiService.signOutWithPatrol(labId, userId)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 巡查签退/准入签退判断
+     *  true-巡查 false-准入
+     */
+    override fun isSignInType(labId: String, userId: String): Observable<Boolean> {
+        return apiService.isSignInType(labId, userId)
+            .map { response ->
+                requireSuccess(response)
+                return@map "1" == response.data
+            }
+    }
+
+    /**
+     * 巡查签到前置校验
+     */
+    override fun signInCheckWithPatrol(labId: String, userId: String): Observable<Boolean> {
+        return apiService.signInCheckWithPatrol(labId, userId)
+            .map(this::simpleConvert)
+    }
+
+    /**
+     * 滚动消息列表
+     */
+    override fun newMsgGroup(param: NoticeReq): Observable<List<NoticeSummary>> {
+        return apiService.newMsgGroup(param)
+            .map(this::recordsConvert)
+    }
+
+    /**
+     * 鉴权
+     */
+    override fun terminalAuth(param: TerminalAuthReq): Observable<String> {
+        return apiService.terminalAuth(param)
+            .map(this::dataConvert)
+    }
+
+    @Throws(NetException::class)
+    private fun <T> recordsConvert(response: CommonDataResponse<RecordsResponse<T>>): List<T> {
+        requireSuccess(response)
+        return response.data!!.records
+    }
+
+    @Throws(NetException::class)
+    private fun <T : CommonResponse> responseConvert(response: T): T {
+        requireSuccess(response)
+        return response
+    }
+
+    @Throws(NetException::class)
+    private fun simpleConvert(response: CommonResponse): Boolean {
+        requireSuccess(response)
+        return true
+    }
+
+    @Throws(NetException::class)
+    private fun <T> dataConvert(response: CommonDataResponse<T>): T {
+        requireSuccess(response)
+        return response.data!!
+    }
+
+    @Throws(NetException::class)
+    private fun <T> listConvert(response: CommonRowsResponse<T>): List<T> {
+        requireSuccess(response)
+        return response.rows!!
+    }
+
+    @Throws(NetException::class)
+    private fun requireSuccess(response: CommonResponse) {
+        if (!response.isSuccess()) {
+            throw NetException(response.code, response.message)
+        }
+    }
+
+}

+ 27 - 0
app/src/main/java/http/config/ConfigCore.kt

@@ -0,0 +1,27 @@
+//package http.config
+//
+//import http.HttpConfig
+//
+//object ConfigCore {
+//
+//    fun initConfig(config: ConfigParam?, baseUrlSp: String?, signInCheckBaseUrlSp: String?, apiVersion: Int) {
+//        if (null != config) {
+//            HttpConfig.API_BASE_URL = if (baseUrlSp.isNullOrEmpty()) config.baseUrl else baseUrlSp
+//            HttpConfig.SIGN_IN_CHECK_BASE_URL =
+//                if (signInCheckBaseUrlSp.isNullOrEmpty()) config.signInCheckBaseUrl else signInCheckBaseUrlSp
+//            HttpConfig.HTTP_STRATEGY = config.httpStrategy
+//
+//            when (apiVersion) {
+//                HttpConfig.ApiVersion.V1.code -> {
+//                    HttpConfig.BASE_PATH_FACE = HttpConfig.BasePathV1.FACE
+//                    HttpConfig.BASE_PATH_SPEAK = HttpConfig.BasePathV1.SPEAK
+//                }
+//                HttpConfig.ApiVersion.V2.code -> {
+//                    HttpConfig.BASE_PATH_FACE = HttpConfig.BasePathV2.ALGORITHM
+//                    HttpConfig.BASE_PATH_SPEAK = HttpConfig.BasePathV2.ALGORITHM
+//                }
+//            }
+//        }
+//    }
+//
+//}

+ 27 - 0
app/src/main/java/http/config/ConfigFactory.kt

@@ -0,0 +1,27 @@
+//package http.config
+//
+//import http.HttpConfig
+//
+//class ConfigFactory {
+//
+//    companion object {
+//        val buildDebugConfig = ConfigParam.Builder()
+//            .baseUrl("http://192.168.1.8/api/")
+//            .signInCheckBaseUrl("http://10.20.10.7/labSystem/algorithm/")
+//            .httpStrategy(HttpConfig.HTTP_STRATEGY_Retrofit)
+//            .mqttServerUri("tcp://192.168.1.8:1883")
+//            .mqttUName("mqtt")
+//            .mqttUPwd("mqtt@zd1883")
+//            .build()
+//
+//        val buildReleaseConfig = ConfigParam.Builder()
+//            .baseUrl("http://172.16.0.65/api/")
+//            .signInCheckBaseUrl("http://192.168.251.2:9015/")
+//            .httpStrategy(HttpConfig.HTTP_STRATEGY_Retrofit)
+//            .mqttServerUri("tcp://172.16.0.65:31072")
+//            .mqttUName("mqtt")
+//            .mqttUPwd("mqtt@zd1883")
+//            .build()
+//    }
+//
+//}

+ 22 - 0
app/src/main/java/http/converter/NullOnEmptyConverterFactory.kt

@@ -0,0 +1,22 @@
+package http.converter
+
+import okhttp3.ResponseBody
+import retrofit2.Converter
+import retrofit2.Retrofit
+import java.lang.reflect.Type
+
+class NullOnEmptyConverterFactory : Converter.Factory() {
+
+    override fun responseBodyConverter(
+        type: Type?,
+        annotations: Array<out Annotation>?,
+        retrofit: Retrofit?
+    ): Converter<ResponseBody, *> {
+        val delegate: Converter<ResponseBody, Any>? =
+            retrofit?.nextResponseBodyConverter(this, type, annotations)
+
+        return Converter<ResponseBody, Any>() {
+            if (it.contentLength() == 0L) null else delegate?.convert(it)
+        }
+    }
+}

+ 16 - 0
app/src/main/java/http/exception/AICheckException.kt

@@ -0,0 +1,16 @@
+package http.exception
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class AICheckException(code: String, message: String?, val data: String?) :
+    NetException(code, message) {
+
+    override fun toString(): String {
+        val message = super.toString()
+        return "$message ${data ?: ""}"
+    }
+
+}

+ 9 - 0
app/src/main/java/http/exception/NetException.kt

@@ -0,0 +1,9 @@
+package http.exception
+
+open class NetException(val code: String, message: String?): Exception(message) {
+
+    override fun toString(): String {
+        val message = super.getLocalizedMessage() ?: ""
+        return "$message (${code})"
+    }
+}

+ 92 - 0
app/src/main/java/http/interceptor/TokenHeaderInterceptor.kt

@@ -0,0 +1,92 @@
+package http.interceptor
+
+import android.text.TextUtils
+import android.util.Log
+import android.widget.Toast
+import com.blankj.utilcode.util.ActivityUtils
+import com.blankj.utilcode.util.LogUtils
+import com.blankj.utilcode.util.ThreadUtils
+import com.google.gson.Gson
+import http.vo.CommonResponse
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.ResponseBody
+import xn.xxp.room.RoomTool
+import xn.xxp.utils.Tool
+import java.lang.Exception
+import java.nio.charset.StandardCharsets
+
+class TokenHeaderInterceptor : Interceptor {
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val originalRequest: Request = chain.request()
+        val requestBuilder: Request.Builder = originalRequest.newBuilder()
+        val config = RoomTool.getInstance().deviceConfigDao().deviceConfig
+        val terminalAuth = config.terminalAuth
+        if (!TextUtils.isEmpty(terminalAuth)) {
+            requestBuilder.addHeader("TerminalAuth", terminalAuth)
+        }
+        requestBuilder
+            .header("vName", "1.0.0")
+            .header("Accept", "application/json, text/plain, */*")
+            .build()
+        val newlyRequest = requestBuilder.build()
+        val url = newlyRequest.url.toString()
+        val isPrint = url.contains("terminal/iot/monitor")
+        if (!isPrint) {
+            LogUtils.json(newlyRequest)
+        }
+        val response = chain.proceed(newlyRequest)
+        val responseBody = response.body
+        if (401 == response.code) {
+            ThreadUtils.runOnUiThread {
+                Toast.makeText(
+                    ActivityUtils.getTopActivity(),
+                    "鉴权失败,请重试!",
+                    Toast.LENGTH_LONG
+                )
+                    .show()
+                Tool.INSTANCE.reStartApp("鉴权失败")
+            }
+        }
+        try {
+            responseBody?.let {
+                val source = it.source()
+                source.request(Long.MAX_VALUE)
+                val data = source.buffer.clone().readString(StandardCharsets.UTF_8)
+
+                val commonResponse =
+                    Gson().fromJson<CommonResponse>(data, CommonResponse::class.java)
+                if (commonResponse.isTokenExpired()) {
+                    ThreadUtils.runOnUiThread {
+                        Toast.makeText(
+                            ActivityUtils.getTopActivity(),
+                            "鉴权失败,请重试!",
+                            Toast.LENGTH_LONG
+                        ).show()
+                        Tool.INSTANCE.reStartApp("鉴权失败")
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            LogUtils.d(Log.getStackTraceString(e))
+        }
+
+        val responseBodyString = responseBody?.string() ?: ""
+//        if (!AppUtils.isAppDebug()) {
+
+        if (!isPrint) {
+            if (response.code == 200) {
+                LogUtils.d(response, responseBodyString)
+            } else {
+                LogUtils.e(response, responseBodyString)
+            }
+        }
+//        }
+
+        return response.newBuilder()
+            .body(ResponseBody.create(responseBody?.contentType(), responseBodyString))
+            .build()
+    }
+}

+ 14 - 0
app/src/main/java/http/net/DownloadListener.kt

@@ -0,0 +1,14 @@
+package http.net
+
+interface DownloadListener {
+
+    fun onProgress(progress: Int)
+
+    fun onSuccess()
+
+    fun onFailed(errMsg: String?)
+
+    fun onPaused()
+
+    fun onCanceled()
+}

+ 156 - 0
app/src/main/java/http/net/DownloadTask.kt

@@ -0,0 +1,156 @@
+package http.net
+
+import android.os.AsyncTask
+import android.os.Environment
+import android.text.TextUtils
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.File
+import java.io.InputStream
+import java.io.RandomAccessFile
+
+/**
+ * eg: http://www.download.com/projectname/dl/test.apk
+ * 将文件下载到sd卡/Download目录。
+ * 下载到本地的文件件名,如果没指定则为test.apk。
+ *
+ */
+class DownloadTask(
+    private val listener: DownloadListener,
+    private val dlPath: String = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path,
+    private var fileName: String?,
+    private val headers: Map<String, String>? = null
+) :
+    AsyncTask<String, Int, Int>() {
+
+    companion object {
+        const val TYPE_SUCCESS: Int = 1
+        const val TYPE_FAILED: Int = 2
+        const val TYPE_PAUSED: Int = 3
+        const val TYPE_CANCELED: Int = 4
+    }
+
+    private var isCanceled: Boolean = false
+    private var isPaused: Boolean = false
+
+    private var lastProgress: Int = 0
+
+    override fun doInBackground(vararg params: String?): Int {
+        var file: File? = null
+        var inputStream: InputStream? = null
+        var saveFile: RandomAccessFile? = null
+        try {
+            var downloadedLength = 0L
+            val downloadUrl = params[0]
+            downloadUrl ?: return TYPE_FAILED
+
+            if (TextUtils.isEmpty(fileName)) {
+                fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"))
+            }
+
+            file = File(dlPath, fileName!!)
+            if (file.exists()) {
+                downloadedLength = file.length()
+            }
+            val contentLength: Long = getContentLength(downloadUrl)
+            if (contentLength == 0L) {
+                return TYPE_FAILED
+            } else if (contentLength == downloadedLength) {
+                // 已经下载的字节数和文件的总字节数相等,说明已经下载完成。
+                return TYPE_SUCCESS
+            }
+            val builder = Request.Builder()
+            headers?.let {
+                for ((key, value) in it)
+                    builder.addHeader(key, value)
+            }
+            val request = builder
+                // 断点下载,指定从那个字节开始下
+                .addHeader("RANGE", "bytes=" + downloadedLength + "-")
+                .url(downloadUrl)
+                .build()
+            val response = OkHttpClient()
+                .newCall(request)
+                .execute()
+
+            inputStream = response.body!!.byteStream()
+            saveFile = RandomAccessFile(file, "rw")
+            saveFile.seek(downloadedLength) // 跳过已经下载的字节
+            val buff = ByteArray(1024)
+            var total = 0
+            var len = 0;
+            while ({ len = inputStream.read(buff);len }() != -1) {
+                if (isCanceled) {
+                    return TYPE_CANCELED
+                } else if (isPaused) {
+                    return TYPE_PAUSED
+                } else {
+                    total += len
+                    saveFile.write(buff, 0, len)
+                    // 计算已经下载的百分比
+                    val progress: Int = ((total + downloadedLength) * 100 / contentLength).toInt()
+                    publishProgress(progress)
+                }
+            }
+            response.body!!.close()
+            return TYPE_SUCCESS
+        } catch (e: Exception) {
+            e.printStackTrace()
+        } finally {
+            try {
+                inputStream?.close()
+                saveFile?.close()
+                if (isCanceled) file?.delete()
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }
+        return TYPE_FAILED
+    }
+
+    override fun onProgressUpdate(vararg values: Int?) {
+        val progress = values[0]
+        progress?.let {
+            if (it > lastProgress) {
+                listener.onProgress(it)
+                lastProgress = it
+            }
+        }
+    }
+
+    override fun onPostExecute(status: Int?) {
+        when (status) {
+            TYPE_SUCCESS -> listener.onSuccess()
+            TYPE_FAILED -> listener.onFailed(null)
+            TYPE_PAUSED -> listener.onPaused()
+            TYPE_CANCELED -> listener.onCanceled()
+            else -> Unit
+        }
+    }
+
+    fun pauseDownload() {
+        this.isPaused = true
+    }
+
+    fun cancelDownload() {
+        this.isCanceled = true
+    }
+
+    private fun getContentLength(downloadUrl: String): Long {
+        val builder = Request.Builder()
+        headers?.let {
+            for ((key, value) in it)
+                builder.addHeader(key, value)
+        }
+        val request = builder.url(downloadUrl).build()
+        val response = OkHttpClient()
+            .newCall(request)
+            .execute()
+        if (response.isSuccessful) {
+            val contentLength = response.body!!.contentLength()
+            response.close()
+            return contentLength
+        }
+        return 0
+    }
+}

+ 5 - 0
app/src/main/java/http/vo/CommonDataResponse.kt

@@ -0,0 +1,5 @@
+package http.vo
+
+class CommonDataResponse<T> : CommonResponse() {
+    var data: T? = null
+}

+ 17 - 0
app/src/main/java/http/vo/CommonResponse.kt

@@ -0,0 +1,17 @@
+package http.vo
+
+open class CommonResponse(/*open var code: String, open var msg: String*/) {
+
+    var code: String = ""
+    var message: String = ""
+
+    companion object {
+        const val CODE_SUCCESS = "200"
+        const val CODE_TOKEN_EXPIRED = "5002"
+    }
+
+    fun isSuccess() = CODE_SUCCESS == code
+
+    fun isTokenExpired() = CODE_TOKEN_EXPIRED == code
+
+}

+ 11 - 0
app/src/main/java/http/vo/CommonRowsResponse.kt

@@ -0,0 +1,11 @@
+package http.vo
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+class CommonRowsResponse<T> : CommonResponse() {
+    var rows: List<T>? = null
+    var total: Int = 0
+}

+ 28 - 0
app/src/main/java/http/vo/request/AccessTokenReq.java

@@ -0,0 +1,28 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AccessTokenReq {
+
+    public String username;
+    public String password;
+    public String subjectId;
+    public String deviceNO;
+
+//    Pair("username", "onecUser"),
+//    Pair("password", "admin123"))
+
+
+    @Override
+    public String toString() {
+        return "AccessTokenReq{" +
+                "username='" + username + '\'' +
+                ", password='" + password + '\'' +
+                ", subjectId='" + subjectId + '\'' +
+                ", deviceNO='" + deviceNO + '\'' +
+                '}';
+    }
+}

+ 22 - 0
app/src/main/java/http/vo/request/ApkInfoReq.java

@@ -0,0 +1,22 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ApkInfoReq {
+
+    public String code;
+    public String deviceNo;
+    public String version;
+
+    @Override
+    public String toString() {
+        return "ApkInfoReq{" +
+                "code='" + code + '\'' +
+                ", deviceNo='" + deviceNo + '\'' +
+                ", version='" + version + '\'' +
+                '}';
+    }
+}

+ 28 - 0
app/src/main/java/http/vo/request/AuthFaceReq.java

@@ -0,0 +1,28 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AuthFaceReq {
+
+    public String data; // 人像特征值
+    public String labId; // 实验室ID
+
+    public AuthFaceReq() {
+    }
+
+    public AuthFaceReq(String data, String labId) {
+        this.data = data;
+        this.labId = labId;
+    }
+
+    @Override
+    public String toString() {
+        return "AuthFaceReq{" +
+                "data='" + data + '\'' +
+                ", labId='" + labId + '\'' +
+                '}';
+    }
+}

+ 24 - 0
app/src/main/java/http/vo/request/AuthPwdReq.java

@@ -0,0 +1,24 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AuthPwdReq {
+
+    public String num; // 设备号
+    public String labId; // 实验室ID
+    public String userId; // 用户Id
+    public String pwd; // 密码
+
+    @Override
+    public String toString() {
+        return "AuthPwdReq{" +
+                "num='" + num + '\'' +
+                ", labId='" + labId + '\'' +
+                ", userId='" + userId + '\'' +
+                ", pwd='" + pwd + '\'' +
+                '}';
+    }
+}

+ 20 - 0
app/src/main/java/http/vo/request/BannerImageReq.java

@@ -0,0 +1,20 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class BannerImageReq {
+
+    public String imgCategory; // 1是轮播图,2是文化图
+    public String pcType; // 管控一体机0 学习考试一体机1
+
+    @Override
+    public String toString() {
+        return "BannerImageReq{" +
+                "imgCategory='" + imgCategory + '\'' +
+                ", pcType='" + pcType + '\'' +
+                '}';
+    }
+}

+ 26 - 0
app/src/main/java/http/vo/request/CheckInAllReq.java

@@ -0,0 +1,26 @@
+package http.vo.request;
+
+import java.io.File;
+
+/**
+ * 签到-安全准入检测三合一 request
+ *
+ * @author ReiChin_
+ */
+public class CheckInAllReq {
+
+    public String id; // 进出记录id
+    public String subId; // 实验室id
+    public String subName; // 实验室名称
+    public File file; // 图片文件
+
+    @Override
+    public String toString() {
+        return "CheckInAllReq{" +
+                "id='" + id + '\'' +
+                ", subId='" + subId + '\'' +
+                ", subName='" + subName + '\'' +
+                ", file=" + file +
+                '}';
+    }
+}

+ 22 - 0
app/src/main/java/http/vo/request/CommonSignInReq.java

@@ -0,0 +1,22 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class CommonSignInReq {
+
+    public String num; // 设备号
+    public String labId; // 实验室ID
+    public String userId; // 人员ID
+
+    @Override
+    public String toString() {
+        return "CommonSignInReq{" +
+                "num='" + num + '\'' +
+                ", labId='" + labId + '\'' +
+                ", userId='" + userId + '\'' +
+                '}';
+    }
+}

+ 24 - 0
app/src/main/java/http/vo/request/ControllerCMD.java

@@ -0,0 +1,24 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ControllerCMD {
+
+    public String id; // 设备ID
+    public String command; // 命令 1:开 0:关
+    public String num; // 设备编号[hardwareNUM]
+    public Object param; // 扩展参数
+
+    @Override
+    public String toString() {
+        return "ControllerCMD{" +
+                "id='" + id + '\'' +
+                ", command='" + command + '\'' +
+                ", num='" + num + '\'' +
+                ", param=" + param +
+                '}';
+    }
+}

+ 22 - 0
app/src/main/java/http/vo/request/FaceCompareReq.java

@@ -0,0 +1,22 @@
+package http.vo.request;
+
+import java.util.Arrays;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class FaceCompareReq {
+
+    public byte[] data; // 特征码
+    public String userId;
+
+    @Override
+    public String toString() {
+        return "FaceCompareReq{" +
+                "data=" + Arrays.toString(data) +
+                ", userId='" + userId + '\'' +
+                '}';
+    }
+}

+ 24 - 0
app/src/main/java/http/vo/request/HazardReq.java

@@ -0,0 +1,24 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HazardReq {
+
+    public int pageNum;
+    public int pageSize;
+
+    public String subId; // 实验室ID
+
+
+    @Override
+    public String toString() {
+        return "HazardReq{" +
+                "pageNum=" + pageNum +
+                ", pageSize=" + pageSize +
+                ", subId='" + subId + '\'' +
+                '}';
+    }
+}

+ 18 - 0
app/src/main/java/http/vo/request/LotDeviceReq.java

@@ -0,0 +1,18 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LotDeviceReq {
+
+    public String subjectId; // 实验室ID
+
+    @Override
+    public String toString() {
+        return "LotDeviceReq{" +
+                "subjectId='" + subjectId + '\'' +
+                '}';
+    }
+}

+ 28 - 0
app/src/main/java/http/vo/request/MonitorReq.java

@@ -0,0 +1,28 @@
+package http.vo.request;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class MonitorReq {
+
+    public List<String> subIds;
+    public String protocol = "rtsp";
+    public String userId;
+    public String userName;
+    public int source;
+
+    @Override
+    public String toString() {
+        return "MonitorReq{" +
+                "subIds=" + subIds +
+                ", protocol='" + protocol + '\'' +
+                ", userId='" + userId + '\'' +
+                ", userName='" + userName + '\'' +
+                ", source=" + source +
+                '}';
+    }
+}

+ 62 - 0
app/src/main/java/http/vo/request/NoticeReq.java

@@ -0,0 +1,62 @@
+package http.vo.request;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class NoticeReq {
+    /**
+     * 1.预警消息
+     * 13.整改通知
+     */
+    public List<Long> businessTypeList = new ArrayList<>();
+    public String subIds;
+    public int page;
+    public int pageSize;
+
+    public List<Long> getBusinessTypeList() {
+        return businessTypeList;
+    }
+
+    public void setBusinessTypeList(List<Long> businessTypeList) {
+        this.businessTypeList = businessTypeList;
+    }
+
+    public String getSubIds() {
+        return subIds;
+    }
+
+    public void setSubIds(String subIds) {
+        this.subIds = subIds;
+    }
+
+    public int getPage() {
+        return page;
+    }
+
+    public void setPage(int page) {
+        this.page = page;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    @Override
+    public String toString() {
+        return "NoticeReq{" +
+                "businessTypeList=" + businessTypeList +
+                ", subIds='" + subIds + '\'' +
+                ", page=" + page +
+                ", pageSize=" + pageSize +
+                '}';
+    }
+}

+ 22 - 0
app/src/main/java/http/vo/request/OnLineUserReq.java

@@ -0,0 +1,22 @@
+package http.vo.request;
+
+/**
+ * 在线人员
+ *
+ * @author ReiChin_
+ */
+public class OnLineUserReq {
+
+    public String pageNum;
+    public String pageSize;
+    public String subId;
+
+    @Override
+    public String toString() {
+        return "OnLineUserReq{" +
+                "pageNum='" + pageNum + '\'' +
+                ", pageSize='" + pageSize + '\'' +
+                ", subId='" + subId + '\'' +
+                '}';
+    }
+}

+ 22 - 0
app/src/main/java/http/vo/request/PatrolSignInReq.java

@@ -0,0 +1,22 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class PatrolSignInReq {
+
+    public String num;
+    public String userId;
+    public String subjectId; // 实验室ID
+
+    @Override
+    public String toString() {
+        return "PatrolSignInReq{" +
+                "num='" + num + '\'' +
+                ", userId='" + userId + '\'' +
+                ", subjectId='" + subjectId + '\'' +
+                '}';
+    }
+}

+ 20 - 0
app/src/main/java/http/vo/request/PatrolSignOutReq.java

@@ -0,0 +1,20 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class PatrolSignOutReq {
+
+    public String labId;
+    public String userId;
+
+    @Override
+    public String toString() {
+        return "PatrolSignOutReq{" +
+                "labId='" + labId + '\'' +
+                ", userId='" + userId + '\'' +
+                '}';
+    }
+}

+ 16 - 0
app/src/main/java/http/vo/request/SafetyListReq.java

@@ -0,0 +1,16 @@
+package http.vo.request;
+
+public class SafetyListReq {
+
+    /**
+     * 1:学校制度 2:学院制度 3: 中心制度
+     */
+    public String type;
+
+    @Override
+    public String toString() {
+        return "SafetyListReq{" +
+                "type='" + type + '\'' +
+                '}';
+    }
+}

+ 20 - 0
app/src/main/java/http/vo/request/SignInReq.java

@@ -0,0 +1,20 @@
+package http.vo.request;
+
+import java.util.Arrays;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class SignInReq {
+
+    public byte[] data;
+
+    @Override
+    public String toString() {
+        return "SignInReq{" +
+                "data=" + Arrays.toString(data) +
+                '}';
+    }
+}

+ 18 - 0
app/src/main/java/http/vo/request/SubIdReq.java

@@ -0,0 +1,18 @@
+package http.vo.request;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class SubIdReq {
+
+    public String subId; // 实验室ID
+
+    @Override
+    public String toString() {
+        return "SubIdReq{" +
+                "subId='" + subId + '\'' +
+                '}';
+    }
+}

+ 34 - 0
app/src/main/java/http/vo/request/TerminalAuthReq.java

@@ -0,0 +1,34 @@
+package http.vo.request;
+
+public class TerminalAuthReq {
+    private String deviceCode;
+    // AIO_MANAGER("aio_manager","管控一体机"),
+    //AIO_CHEMICAL("aio_chemical","化学品一体机"),
+    //AIO_INFOBORD("aio_infobord","电子信息牌"),
+    //AIO_EXAM("aio_exam","学习考试一体机");
+    private String code;
+
+    public String getDeviceCode() {
+        return deviceCode;
+    }
+
+    public void setDeviceCode(String deviceCode) {
+        this.deviceCode = deviceCode;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    @Override
+    public String toString() {
+        return "TerminalAuthReq{" +
+                "deviceCode='" + deviceCode + '\'' +
+                ", code='" + code + '\'' +
+                '}';
+    }
+}

+ 14 - 0
app/src/main/java/http/vo/response/AccessTokenResp.java

@@ -0,0 +1,14 @@
+package http.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class AccessTokenResp {
+
+    public String access_token;
+    public String user_id;
+    public String expires_in;
+    public String username;
+}

+ 70 - 0
app/src/main/java/http/vo/response/ApkInfoResp.java

@@ -0,0 +1,70 @@
+package http.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ApkInfoResp {
+
+    public boolean needUpgrade;
+    public LabInfo labInfo;
+    public AppInfo appInfo;
+
+    public static class LabInfo {
+        public String floorId;
+        public String floorName;
+        public String subjectId;
+        public String room;
+        public String subjectName;
+    }
+
+    public static class AppInfo {
+        public String id;
+        public String name;
+        public String code;
+        public String size;
+        public String url;
+        public String version;
+        public String info;
+        public boolean state;
+        public String createBy;
+        public String createTime;
+        public String updateBy;
+        public String updateTime;
+        public String remark;
+    }
+
+    /*
+    {
+       "code": 200,
+       "message": "操作成功",
+       "data": {
+           "needUpgrade": true,
+           "labInfo": {
+               "floorId": "1701070978336448514",
+               "floorName": "楼层1-1",
+               "subjectId": "1769924713371934721",
+               "room": "22",
+               "subjectName": "贺洋测试2"
+           },
+           "appInfo": {
+               "id": "1810133309475274754",
+               "name": "电子信息牌0708955",
+               "code": "aio_infobord",
+               "size": 17.71,
+               "url": "http://192.168.1.43/api/statics/bigFile/20240708/bb66366b-4c30-44bd-b1aa-00bf22eb224a.apk",
+               "version": 11.0,
+               "info": "电子信息牌0708955",
+               "state": true,
+               "createBy": 1664091126523312160,
+               "createTime": "2024-07-08T10:06:26",
+               "updateBy": null,
+               "updateTime": null,
+               "remark": null
+           }
+       }
+    }
+    */
+
+}

+ 12 - 0
app/src/main/java/http/vo/response/BannerImageBean.java

@@ -0,0 +1,12 @@
+package http.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class BannerImageBean {
+
+    public String imgUrl;
+
+}

+ 31 - 0
app/src/main/java/http/vo/response/CheckMachineVo.java

@@ -0,0 +1,31 @@
+package http.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class CheckMachineVo {
+
+    public List<Detail> details;
+    public List<Detail> detailList;
+    public CheckRecord checkRecord;
+
+    public String jcRyxm; // 检查人
+    public String createTime; // 检查时间
+
+    public static class CheckRecord {
+        public String laboratoryName; // 实验室名称
+        public String jcDwName; // 检查单位
+    }
+
+    public static class Detail {
+        public String jcxName; // 检查项名称
+        public String yhMs; // 隐患描述
+        public String optionCode; // 条款号- 对应条款
+        public String zgjzTime; // 整改完成期限
+    }
+
+}

+ 14 - 0
app/src/main/java/http/vo/response/ContentMachineVo.java

@@ -0,0 +1,14 @@
+package http.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class ContentMachineVo {
+
+    public String createBy; // 发送人
+    public String createTime; // 发送时间
+    public String contentMsg; // 消息内容
+
+}

+ 20 - 0
app/src/main/java/http/vo/response/DutyPersonVo.java

@@ -0,0 +1,20 @@
+package http.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class DutyPersonVo {
+
+    public List<LabPersonVo> weekday1;
+    public List<LabPersonVo> weekday2;
+    public List<LabPersonVo> weekday3;
+    public List<LabPersonVo> weekday4;
+    public List<LabPersonVo> weekday5;
+    public List<LabPersonVo> weekday6;
+    public List<LabPersonVo> weekday7;
+
+}

+ 36 - 0
app/src/main/java/http/vo/response/HazardBook.java

@@ -0,0 +1,36 @@
+package http.vo.response;
+
+import java.util.List;
+
+/**
+ * 危化品安全技术说明书
+ *
+ * @author ReiChin_
+ */
+public class HazardBook {
+
+    public String code; // 编号
+    public String content; // 内容
+    public String createBy;
+    public String createTime; // 创建时间
+    public String deptId; // 题目ID
+    public String deptName; // 部门名称
+    public String id;
+    public List<String> ids; //批量操作的id集合
+    public String name; // 名称
+    public Object params;
+    public String pinYin; // 名称全拼
+    public String pinYinChar; // 首字母
+    public String qrCodeUrl; // 二维码地址
+    public String remark;
+    public String scanCount; // 查看次数
+    public String searchValue;
+    public String updateBy;
+    public String updateTime; // 更新时间
+    public String userId; // 用户ID(数据权限)
+
+    @Override
+    public String toString() {
+        return this.name;
+    }
+}

+ 35 - 0
app/src/main/java/http/vo/response/HomeMiddleResp.java

@@ -0,0 +1,35 @@
+package http.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HomeMiddleResp {
+
+    public String adminName; // 负责人姓名
+    public String adminPhone; // 负责人电话
+    public String levelName; // 危险级别名称 1高危险级,2较高危险级,3中危险级,4一般危险级
+    public String filedColor; // 分级颜色 #13E63A
+    public String typeName; // 实验室类别 化学类
+    public String qrCodeUrl; // 二维码
+    public String deptName; // 责任单位
+    public String subjectName; // 实验室名称
+    public String buildName; // 楼栋名称
+    public String room; // 房间号
+
+    public List<SafeUserVo> safeUserVoList; // 安全责任人
+    public List<SafeClassify> brandInfoSubjectVos; // 安全分类信息
+    public List<LaboratoryVo.InCheckItem> inCheck; // AI检测项(准入)
+    public List<LaboratoryVo.InCheckItem> inspectInCheck; // AI检测项(巡查)
+    public List<String> outCheck; // 离开检查项
+
+    public static class SafeUserVo {
+        public String id;
+        public String safeUserName;
+        public String safeUserPhone;
+    }
+
+}

+ 16 - 0
app/src/main/java/http/vo/response/HomeRightResp.java

@@ -0,0 +1,16 @@
+package http.vo.response;
+
+import java.util.List;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HomeRightResp {
+
+    public List<LabPersonVo> tentativeUser; // 实验人员
+    public List<LabPersonVo> dutyUser; // 值班人员
+    public List<LabPersonVo> securityUser; // 准入人员
+
+}

+ 15 - 0
app/src/main/java/http/vo/response/HomeTopResp.java

@@ -0,0 +1,15 @@
+package http.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class HomeTopResp {
+
+    public String currentTime; // 当前时间
+    public String schoolName; // 学校名称
+    public String circularLogo; // LOGO
+    public String subjectName; // 实验室名称
+
+}

+ 47 - 0
app/src/main/java/http/vo/response/LabBulletinBoardVo.java

@@ -0,0 +1,47 @@
+package http.vo.response;
+
+/**
+ * info
+ *
+ * @author ReiChin_
+ */
+public class LabBulletinBoardVo {
+    // ("传感器设备编号")
+    public String deviceNo;
+    // ("传感器设备名")
+    public String deviceName;
+    // ("传感器值")
+    public String deviceValue;
+    // ("最后在线时间")
+    public String lastOnlineTime;
+    // ("实验室id")
+    public long subjectId;
+    // ("实验室名称")
+    public String subjectName;
+    // ("属性id")
+    public long attributeId;
+    // ("运行状态:0-未运行,1-运行")
+    public boolean operatingState;
+    // ("在线:0-离线,1-在线")
+    public boolean online;
+    // ("状态")
+    public boolean state;
+    // ("单位")
+    public String unit;
+    // ("图标")
+    public String icon;
+    // ("异常图标")
+    public String exceptionIcon;
+    public String funNum; // 功能编码
+    public String code; // 设备Code
+
+    public String formatWaring() {
+        String describe_ = null == deviceName ? "" : deviceName;
+        String val_ = null == deviceValue ? "" : deviceValue;
+        String unit_ = null == unit ? "" : unit;
+        return String.format("%s:%s%s", describe_, val_, unit_);
+    }
+    
+    
+
+}

+ 18 - 0
app/src/main/java/http/vo/response/LabHazardVo.java

@@ -0,0 +1,18 @@
+package http.vo.response;
+
+/**
+ * 危险源信息
+ *
+ * @author ReiChin_
+ */
+public class LabHazardVo {
+
+    public String hazardId;
+    public String chName; // 中文名称
+    public String hazardCode; // 编号
+    public String content; // 内容
+    public String qrCodeUrl; // 二维码地址
+    public Integer scanCount; // 查看次数
+    public String subName;
+
+}

+ 0 - 0
app/src/main/java/http/vo/response/LabPersonVo.java


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.