Selaa lähdekoodia

安科院特殊要求提交

donggaosheng 26 minuuttia sitten
vanhempi
commit
dc0269a79d

+ 16 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/ApiRepository.kt

@@ -95,6 +95,15 @@ object ApiRepository {
             .observeOn(AndroidSchedulers.mainThread())
     }
 
+    // 新增:人脸识别登录
+    fun faceByPic(param: FaceCompareReq): Observable<LearnLoginVo> {
+        return mClientFactory.createLabClient()
+            .faceByPic(param)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
+
+
     /**
      * 学习一体机 用户端登录
      *
@@ -422,4 +431,11 @@ object ApiRepository {
             .observeOn(AndroidSchedulers.mainThread())
     }
 
+    // 获取服务器时间
+    fun getServerTime(): Observable<Long> {
+        return mClientFactory.createLabClient()
+            .getServerTime()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+    }
 }

+ 5 - 1
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/ExamClient.kt

@@ -46,6 +46,9 @@ interface ExamClient {
 
     fun compare(param: FaceCompareReq): Observable<Boolean>
 
+    // 新增:人脸识别登录
+    fun faceByPic(param: FaceCompareReq): Observable<LearnLoginVo>
+
     /**
      * 学习一体机 用户端登录
      *
@@ -105,7 +108,6 @@ interface ExamClient {
 
     /**
      * 开始-查询考试前后否需要人脸验证
-     *
      * @param examId 非模拟考试模式下都传-1
      */
     fun examStartVerify(type: String, examId: String = "-1"): Observable<ExamVerify>
@@ -210,4 +212,6 @@ interface ExamClient {
      */
     fun loginOut(): Observable<Boolean>
 
+    // 获取服务器时间
+    fun getServerTime(): Observable<Long>
 }

+ 11 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/retrofit/ApiService.java

@@ -106,6 +106,9 @@ public interface ApiService {
     @POST("system/user/face/compare")
     Observable<CommonDataResponse<String>> compare(@Body FaceCompareReq param);
 
+    // 新增:人脸识别登录接口
+    @POST("auth/faceByBytes")
+    Observable<CommonDataResponse<LearnLoginVo>> faceByPic(@Body FaceCompareReq param);
 
     /**
      * 学习一体机 用户端登录
@@ -316,4 +319,12 @@ public interface ApiService {
     @GET("auth/logout")
     Observable<CommonResponse> loginOut();
 
+
+    /**
+     * 获取服务器时间
+     * 返回服务器当前时间戳(毫秒)
+     */
+    @GET("auth/time/current")
+    Observable<CommonDataResponse<Long>> getServerTime();
+
 }

+ 29 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/client/retrofit/ExamRetrofit.kt

@@ -4,6 +4,7 @@ import com.rc.httpcore.HttpClient
 import com.rc.httpcore.apk.AppInfo
 import com.rc.httpcore.client.ExamClient
 import com.rc.httpcore.exception.NetException
+import com.rc.httpcore.utils.ServerTimeManager
 import com.rc.httpcore.vo.CommonListResponse
 import com.rc.httpcore.vo.request.*
 import com.rc.httpcore.vo.response.*
@@ -126,6 +127,20 @@ class ExamRetrofit : ExamClient {
             }
     }
 
+
+    // 新增:实现 faceByPic 方法
+    override fun faceByPic(param: FaceCompareReq): Observable<LearnLoginVo> {
+        return apiService.faceByPic(param)
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.message)
+                }
+                // 设置 token(和 cardNum 一样的逻辑)
+                HttpClient.token = response.data?.token
+                return@map response.data
+            }
+    }
+
     /**
      * 学习一体机 用户端登录
      *
@@ -612,4 +627,18 @@ class ExamRetrofit : ExamClient {
             }
     }
 
+
+    /**
+     * 获取服务器时间
+     */
+    override fun getServerTime(): Observable<Long> {
+        return apiService.getServerTime()
+            .map { response ->
+                if (!response.isSuccess()) {
+                    throw NetException(response.code, response.message)
+                }
+                return@map response.data ?: ServerTimeManager.getServerTimeMillis()
+            }
+    }
+
 }

+ 110 - 0
HttpCoreLibrary/src/main/java/com/rc/httpcore/utils/ServerTimeManager.kt

@@ -0,0 +1,110 @@
+package com.rc.httpcore.utils
+
+import android.os.SystemClock
+import android.util.Log
+import com.rc.httpcore.client.ApiRepository
+import io.reactivex.disposables.Disposable
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * 服务器时间管理器
+ * 用于获取和同步服务器时间,解决本地时间不准确的问题
+ */
+object ServerTimeManager {
+
+    private const val TAG = "ServerTimeManager"
+
+    // 服务器时间与本地时间的偏移量(毫秒)
+    @Volatile
+    private var timeOffset: Long = 0L
+
+    // 是否已经同步过服务器时间
+    @Volatile
+    private var isSynced: Boolean = false
+
+    // 上次同步的时间戳(系统启动时间)
+    @Volatile
+    private var lastSyncElapsedRealtime: Long = 0L
+
+    /**
+     * 获取当前服务器时间(毫秒时间戳)
+     * 如果还未同步,则返回本地时间
+     */
+    fun getServerTimeMillis(): Long {
+        if (!isSynced) {
+            Log.w(TAG, "尚未同步服务器时间,使用本地时间")
+            return System.currentTimeMillis()
+        }
+
+        // 计算从上次同步到现在经过的时间
+        val elapsedSinceSync = SystemClock.elapsedRealtime() - lastSyncElapsedRealtime
+
+        // 服务器时间 = 上次同步的服务器时间 + 经过的时间
+        return System.currentTimeMillis() + timeOffset
+    }
+
+    /**
+     * 获取当前服务器时间戳(秒)
+     */
+    fun getServerTimeSeconds(): Long {
+        return getServerTimeMillis() / 1000
+    }
+
+    /**
+     * 同步服务器时间
+     * 通过调用后端接口获取服务器时间,并计算偏移量
+     */
+    fun syncServerTime(callback: ((success: Boolean) -> Unit)? = null) {
+        Log.i(TAG, "开始同步服务器时间...")
+
+        val localTimeBefore = System.currentTimeMillis()
+
+        // 调用后端接口获取服务器时间
+        val disposable: Disposable = ApiRepository.getServerTime()
+            .subscribe(
+                { serverTime: Long ->
+                    val localTimeAfter = System.currentTimeMillis()
+
+                    // 使用往返时间的中点作为更准确的本地时间
+                    val localTimeMidpoint = (localTimeBefore + localTimeAfter) / 2
+
+                    // 计算时间偏移量
+                    timeOffset = serverTime - localTimeMidpoint
+
+                    // 记录同步状态
+                    isSynced = true
+                    lastSyncElapsedRealtime = SystemClock.elapsedRealtime()
+
+                    val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+                    Log.i(TAG, "服务器时间同步成功")
+                    Log.i(TAG, "服务器时间: ${sdf.format(Date(serverTime))}")
+                    Log.i(TAG, "本地时间: ${sdf.format(Date(localTimeMidpoint))}")
+                    Log.i(TAG, "时间偏移: ${timeOffset}ms (${timeOffset / 1000.0}s)")
+
+                    callback?.invoke(true)
+                },
+                { error: Throwable ->
+                    Log.w(TAG, "服务器时间同步失败: ${error.message}")
+                    isSynced = false
+                    callback?.invoke(false)
+                }
+            )
+    }
+
+    /**
+     * 检查是否已同步服务器时间
+     */
+    fun isTimeSynced(): Boolean {
+        return isSynced
+    }
+
+    /**
+     * 重置同步状态(用于重新同步)
+     */
+    fun resetSync() {
+        isSynced = false
+        timeOffset = 0L
+        lastSyncElapsedRealtime = 0L
+    }
+}

+ 3 - 2
RcCore/src/main/java/com/rc/core/util/MediaUtils.kt

@@ -6,6 +6,7 @@ import android.net.Uri
 import android.os.Build
 import android.os.Environment
 import android.provider.MediaStore
+import com.rc.httpcore.utils.ServerTimeManager
 import java.io.File
 import java.util.*
 
@@ -37,7 +38,7 @@ object MediaUtils {
         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.DATE_TAKEN, "${ServerTimeManager.getServerTimeMillis()}")
         }
         values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
 
@@ -75,7 +76,7 @@ object MediaUtils {
         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.DATE_TAKEN, "${ServerTimeManager.getServerTimeMillis()}")
         }
         values.put(MediaStore.Video.Media.MIME_TYPE, mimeType)
 

+ 21 - 0
app/src/main/java/com/dlc/exam/ExamApp.kt

@@ -26,6 +26,7 @@ import com.rc.core.util.ScreenAdapter
 import com.rc.httpcore.HttpClient
 import com.rc.httpcore.config.ConfigCore
 import com.rc.httpcore.config.ConfigFactory
+import com.rc.httpcore.utils.ServerTimeManager
 import com.rc.httpcore.vo.response.LearnLoginVo
 import com.tencent.smtt.sdk.QbSdk
 import com.tencent.smtt.sdk.QbSdk.PreInitCallback
@@ -79,6 +80,26 @@ class ExamApp : Application() {
         initIflytek()
 
         initX5()
+
+        // 同步服务器时间(异步执行,不阻塞启动)
+        syncServerTimeOnStartup()
+    }
+
+
+    /**
+     * 应用启动时同步服务器时间
+     */
+    private fun syncServerTimeOnStartup() {
+        // 延迟2秒后同步,避免影响启动速度
+        android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
+            ServerTimeManager.syncServerTime { success ->
+                if (success) {
+                    RcLog.info("应用启动时服务器时间同步成功")
+                } else {
+                    RcLog.warn("应用启动时服务器时间同步失败,将使用本地时间")
+                }
+            }
+        }, 2000)
     }
 
     private fun initHttpConfig() {

+ 2 - 1
app/src/main/java/com/dlc/exam/iflytek/VoiceItsManager.kt

@@ -6,6 +6,7 @@ import android.os.Environment
 import com.iflytek.cloud.*
 import com.iflytek.cloud.util.ResourceUtil
 import com.rc.core.log.RcLog
+import com.rc.httpcore.utils.ServerTimeManager
 
 /**
  * info
@@ -162,7 +163,7 @@ object VoiceItsManager {
     private val mTtsListener = object : SynthesizerListener {
 
         override fun onSpeakBegin() {
-            RcLog.info("[Iflytek]开始播放:${System.currentTimeMillis()}")
+            RcLog.info("[Iflytek]开始播放:${ServerTimeManager.getServerTimeMillis()}")
         }
 
         override fun onSpeakPaused() {

+ 2 - 1
app/src/main/java/com/dlc/exam/mqtt/MqttManager.kt

@@ -14,6 +14,7 @@ import com.dlc.exam.sp.EnvConfig
 import com.dlc.exam.sp.EnvSp
 import com.google.gson.Gson
 import com.rc.core.log.RcLog
+import com.rc.httpcore.utils.ServerTimeManager
 import org.eclipse.paho.client.mqttv3.MqttMessage
 import org.greenrobot.eventbus.EventBus
 
@@ -107,7 +108,7 @@ class MqttManager private constructor() {
                 .apply {
                     type = "heart"
                     macno = clientId
-                    time = (System.currentTimeMillis() / 1000).toInt()
+                    time = (ServerTimeManager.getServerTimeMillis() / 1000).toInt()
                 }
 
             val message = mGson.toJson(heartBeatEntry)

+ 183 - 26
app/src/main/java/com/dlc/exam/ui/common/FaceMatchingFragment.kt

@@ -136,50 +136,207 @@ class FaceMatchingFragment : RcBaseFragment<FragmentFaceMatchingBinding>(),
         callback.invoke(true)
 
         // 调用人脸比对方法
-        callFaceMatchingApi(faceFeature.featureData, callback)
+        //callFaceMatchingApi(faceFeature.featureData, callback)
+        callFaceLoginApi(faceFeature.featureData, callback)
     }
 
-    private fun callFaceMatchingApi(
+//    private fun callFaceMatchingApi(
+//        featureData: ByteArray,
+//        callback: (comparing: Boolean) -> Unit
+//    ) {
+//        showLoading("比对中....")
+////        val param = FaceCompareReq().apply {
+////            data = featureData
+//////            userId = ExamApp.sLearnLoginVo!!.user_id
+////            userId = ExamApp.sLearnLoginVo!!.userId
+////        }
+////        RcLog.info("=====登录id ${param.userId}")
+//////        val disposable = ApiRepository.faceCompare(param)
+////        val disposable = ApiRepository.compare(param)
+////            .subscribe({ success ->
+////                dismissLoading()
+////                RcLog.info("======SHIBAI $mType")
+//////                callback.invoke(false)
+////                if (success) {
+////                    dispatchMatchingSuccess()
+////                }
+//////                var userInfo= LearnLoginVo()
+//////                userInfo.user_id=success.userId
+//////                userInfo.type=success.userType
+//////                userInfo.username=success.cardNum
+//////                userInfo.nickName=success.userName
+//////                userInfo.user_id=success.userId
+//////                ExamApp.sLearnLoginVo=userInfo
+//////                ExamApp.sLearnLoginVo!!.nickName=success.userName
+////
+////
+////            }, { throwable ->
+////                RcLog.info("======SHIBATWO")
+////                dismissLoading()
+////                callback.invoke(false)
+////                showNetError(throwable)
+////
+//////                if (!pretreatmentError(throwable)) {
+//////                    faceEngineHelper?.stopPreview(false)
+//////                    throwable.printStackTrace()
+//////                    showNetError(throwable)
+//////                }
+////            })
+////        addDisposable(disposable)
+//
+//        // 如果还没有登录信息,需要先通过人脸识别获取用户信息
+//        var userId = ExamApp.sLearnLoginVo?.userId
+//
+//        if (userId.isNullOrEmpty()) {
+//            // 未登录状态,先调用识别接口获取用户信息
+//            callFaceRecognizeApi(featureData, callback)
+//        } else {
+//            // 已登录状态,直接比对
+//            val param = FaceCompareReq().apply {
+//                data = featureData
+//                userId = ExamApp.sLearnLoginVo!!.userId
+//            }
+//            RcLog.info("=====登录id ${param.userId}")
+//            val disposable = ApiRepository.compare(param)
+//                .subscribe({ success ->
+//                    dismissLoading()
+//                    RcLog.info("======SHIBAI $mType")
+//                    if (success) {
+//                        dispatchMatchingSuccess()
+//                    }
+//                }, { throwable ->
+//                    RcLog.info("======SHIBATWO")
+//                    dismissLoading()
+//                    callback.invoke(false)
+//                    showNetError(throwable)
+//                })
+//            addDisposable(disposable)
+//        }
+//    }
+
+    private fun callFaceLoginApi(
+        featureData: ByteArray,
+        callback: (comparing: Boolean) -> Unit
+    ) {
+        showLoading("识别中...")
+
+        // 构建请求参数(和 compare 接口参数一样)
+        val param = FaceCompareReq().apply {
+            data = featureData
+            userId = "" // 空表示识别模式
+        }
+
+        RcLog.info("=====调用人脸识别登录接口")
+
+        // 调用 faceByPic 接口,直接完成登录
+        val disposable = ApiRepository.faceByPic(param)
+            .subscribe({ data ->
+                dismissLoading()
+                RcLog.info("======人脸识别登录成功")
+
+                // ✅ 保存用户信息(和刷卡登录一样)
+                ExamApp.sLearnLoginVo = data
+
+                // ✅ Token 已经在 ExamRetrofit.faceByPic 中设置了
+                RcLog.info("=====Token: ${HttpClient.token}")
+                RcLog.info("=====用户ID: ${data.userId}")
+                RcLog.info("=====用户名: ${data.userName}")
+
+                // 登录成功,根据类型处理
+                dispatchMatchingSuccess()
+
+            }, { throwable ->
+                dismissLoading()
+                RcLog.info("======人脸识别登录失败")
+                callback.invoke(false)
+
+                // 错误处理
+                if (throwable is NetException) {
+                    when (throwable.code) {
+                        "500" -> {
+                            // 人脸不匹配
+                            AlertDialog.Builder(requireContext())
+                                .setCancelable(false)
+                                .setTitle("提示")
+                                .setMessage(throwable.message ?: "人脸识别失败")
+                                .setPositiveButton("确定") { _, _ ->
+                                    faceEngineHelper?.stopPreview(false)
+                                }.show()
+                        }
+                        else -> {
+                            showNetError(throwable)
+                            faceEngineHelper?.stopPreview(false)
+                        }
+                    }
+                } else {
+                    throwable.printStackTrace()
+                    showNetError(throwable)
+                    faceEngineHelper?.stopPreview(false)
+                }
+            })
+        addDisposable(disposable)
+    }
+
+
+    private fun callFaceRecognizeApi(
         featureData: ByteArray,
         callback: (comparing: Boolean) -> Unit
     ) {
-        showLoading("比对中....")
+        // 调用后端人脸识别接口,返回用户信息
+        // 注意:这里需要根据你后端的实际接口来调整
         val param = FaceCompareReq().apply {
             data = featureData
-//            userId = ExamApp.sLearnLoginVo!!.user_id
-            userId = ExamApp.sLearnLoginVo!!.userId
+            userId = "" // 空userId表示识别模式
         }
-        RcLog.info("=====登录id ${param.userId}")
-//        val disposable = ApiRepository.faceCompare(param)
+
         val disposable = ApiRepository.compare(param)
             .subscribe({ success ->
                 dismissLoading()
-                RcLog.info("======SHIBAI $mType")
-//                callback.invoke(false)
                 if (success) {
-                    dispatchMatchingSuccess()
+                    // 识别成功,获取用户信息后登录
+                    // 这里假设后端会在success中返回用户信息
+                    // 如果后端接口不同,需要相应调整
+                    requireArguments().getString("username")?.let { username ->
+                        callLearnLoginApi(username, true) {
+                            dispatchMatchingSuccess()
+                        }
+                    } ?: run {
+                        // 如果没有username参数,需要从响应中获取
+                        showNetError(Exception("无法获取用户信息"))
+                    }
                 }
-//                var userInfo= LearnLoginVo()
-//                userInfo.user_id=success.userId
-//                userInfo.type=success.userType
-//                userInfo.username=success.cardNum
-//                userInfo.nickName=success.userName
-//                userInfo.user_id=success.userId
-//                ExamApp.sLearnLoginVo=userInfo
-//                ExamApp.sLearnLoginVo!!.nickName=success.userName
-
-
             }, { throwable ->
-                RcLog.info("======SHIBATWO")
                 dismissLoading()
                 callback.invoke(false)
                 showNetError(throwable)
+            })
+        addDisposable(disposable)
+    }
 
-//                if (!pretreatmentError(throwable)) {
-//                    faceEngineHelper?.stopPreview(false)
-//                    throwable.printStackTrace()
-//                    showNetError(throwable)
-//                }
+    private fun callLearnLoginApi(username: String, realLogin: Boolean = false, callback: () -> Unit) {
+        if (username.isNullOrEmpty()) {
+            ExamApp.sLearnLoginVo = null
+            HttpClient.token = null
+
+            CommonUtils.restartApp(requireActivity())
+            return
+        }
+
+        showLoading("查询中...")
+        val disposable = ApiRepository.learnLogin(
+            username,
+            CommonUtils.getAndroidId(requireContext()),
+            realLogin,
+            "1"
+        )
+            .subscribe({ data ->
+                dismissLoading()
+                ExamApp.sLearnLoginVo = data
+                callback.invoke()
+            }, { throwable ->
+                dismissLoading()
+                throwable.printStackTrace()
+                showNetError(throwable)
             })
         addDisposable(disposable)
     }

+ 9 - 2
app/src/main/java/com/dlc/exam/ui/login/LoginActivity.kt

@@ -13,6 +13,7 @@ import com.dlc.exam.R
 import com.dlc.exam.common.Constants
 import com.dlc.exam.databinding.ActivityLoginBinding
 import com.dlc.exam.ui.common.BaseCountDownActivity
+import com.dlc.exam.ui.common.FaceMatchingFragment
 import com.dlc.exam.ui.widget.TitleBar
 import com.rc.core.ui.fragment.RcBaseFragment
 import com.rc.httpcore.HttpClient
@@ -32,10 +33,16 @@ class LoginActivity : BaseCountDownActivity<ActivityLoginBinding>(), LoginFragme
 
     override fun initViews(savedInstanceState: Bundle?) {
         val transaction = supportFragmentManager.beginTransaction()
+//        transaction.add(
+//            R.id.container,
+//            StudentCardFragment.newInstance(),
+//            StudentCardFragment.FRAGMENT_TAG
+//        )
+        val faceFragment = FaceMatchingFragment.newInstance(FaceMatchingFragment.TYPE_LOGIN)
         transaction.add(
             R.id.container,
-            StudentCardFragment.newInstance(),
-            StudentCardFragment.FRAGMENT_TAG
+            faceFragment,
+            FaceMatchingFragment.FRAGMENT_TAG
         )
         transaction.commit()
     }

+ 1 - 1
app/src/main/res/layout/activity_certificate_management.xml

@@ -69,7 +69,7 @@
                 android:layout_height="wrap_content"
                 android:layout_weight="1.14"
                 android:gravity="center"
-                android:text="所属学院"
+                android:text="所属所"
                 android:textColor="@android:color/white"
                 android:textSize="11sp"
                 tools:ignore="SmallSp" />

+ 1 - 1
app/src/main/res/layout/activity_personal_center.xml

@@ -83,7 +83,7 @@
             app:layout_constraintStart_toStartOf="@id/pName"
             app:layout_constraintTop_toBottomOf="@id/pName"
             tools:ignore="SmallSp"
-            tools:text="环境与测绘学院" />
+            tools:text="环境与测绘所" />
 
         <TextView
             android:id="@+id/violationCount"

+ 1 - 1
app/src/main/res/layout/item_certificate_management.xml

@@ -37,7 +37,7 @@
         android:layout_height="wrap_content"
         android:layout_weight="1.14"
         android:gravity="center"
-        android:text="所属学院"
+        android:text="所属所"
         android:textColor="#333333"
         android:textSize="10sp"
         tools:ignore="SmallSp" />