Ver código fonte

1. 支持人脸上传

bjb 3 meses atrás
pai
commit
03b2a6334b

+ 12 - 0
app/src/main/java/com/iscs/bozzys/api/ApiRequest.kt

@@ -339,4 +339,16 @@ object ApiRequest {
         return requestApi { api.deleteUserCharacteristic(params) }
     }
 
+    /**
+     * 上传用户人像信息
+     *
+     * @param username  用户名
+     * @param file      上传的文件
+     * @param type      上传的数据类型
+     */
+    suspend fun uploadUserFace(username: String, file: File, type: String = "image"): Result<Response<UserCharacteristic>> {
+        val part = MultipartBody.Part.createFormData("file", file.name, file.asRequestBody("$type/*".toMediaType()))
+        return requestApi { api.uploadUserFace(username, part) }
+    }
+
 }

+ 11 - 0
app/src/main/java/com/iscs/bozzys/api/ApiService.kt

@@ -301,4 +301,15 @@ interface ApiService {
         @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
     ): Response<Boolean>
 
+    /**
+     * 上传用户特征
+     */
+    @Multipart
+    @POST("/admin-api/iscs/user-characteristic/insertUserFace")
+    suspend fun uploadUserFace(
+        @Query("userName") username: String,
+        @Part part: MultipartBody.Part,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<UserCharacteristic>
+
 }

+ 3 - 1
app/src/main/java/com/iscs/bozzys/service/AliPushService.kt

@@ -24,7 +24,9 @@ class AliPushService : AliyunMessageIntentService() {
     }
 
     override fun onNotificationClickedWithNoAction(p0: Context?, p1: String?, p2: String?, p3: String?) {
-
+        LogUtil.i("MPS", "AliPushService -> onNotificationClickedWithNoAction")
+        // 没有配置行为,这里执行跳转到消息详情页面
+        RefreshEventBus.onRefreshData(RefreshEvent.Notification)
     }
 
     override fun onNotificationRemoved(p0: Context?, p1: String?) {

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/common/Empty.kt

@@ -21,7 +21,7 @@ import com.iscs.bozzys.ui.theme.Text
 @Composable
 fun Empty(tips: String = "", modifier: Modifier = Modifier) {
     Column(modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
-        Icon(painter = painterResource(R.drawable.empty), contentDescription = null, modifier = Modifier.size(64.dp), tint = Text.copy(alpha = 0.8f))
+        Icon(painter = painterResource(R.drawable.empty), contentDescription = null, modifier = Modifier.size(128.dp), tint = Text.copy(alpha = 0.8f))
         Text(tips, color = Text.copy(alpha = 0.8f), fontSize = 14.sp)
     }
 }

+ 19 - 19
app/src/main/java/com/iscs/bozzys/ui/pages/home/HomeCompose.kt

@@ -169,25 +169,25 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
                     lineHeight = 10.sp
                 )
             }
-            Icon(
-                painter = painterResource(R.drawable.settings),
-                contentDescription = null,
-                modifier = Modifier
-                    .size(36.dp)
-                    .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = {
-                        if (ctx is PageBase) {
-                            ctx.lifecycleScope.launch(Dispatchers.IO) {
-                                bleKeyTest(ctx.application, "CC:BA:97:21:72:C6")
-                                // bleKeyTest(ctx.application, "CC:BA:97:21:71:E6")
-                                // bleKeyTest(ctx.application, "CC:BA:97:21:72:0A")
-                                // bleKeyTest(ctx.application, "CC:BA:97:21:71:CA")
-                            }
-                        }
-                    })
-                    .padding(10.dp),
-                tint = Color.White
-            )
+//            Icon(
+//                painter = painterResource(R.drawable.settings),
+//                contentDescription = null,
+//                modifier = Modifier
+//                    .size(36.dp)
+//                    .clip(RoundedCornerShape(6.dp))
+//                    .clickable(onClick = {
+//                        if (ctx is PageBase) {
+//                            ctx.lifecycleScope.launch(Dispatchers.IO) {
+//                                bleKeyTest(ctx.application, "CC:BA:97:21:72:C6")
+//                                // bleKeyTest(ctx.application, "CC:BA:97:21:71:E6")
+//                                // bleKeyTest(ctx.application, "CC:BA:97:21:72:0A")
+//                                // bleKeyTest(ctx.application, "CC:BA:97:21:71:CA")
+//                            }
+//                        }
+//                    })
+//                    .padding(10.dp),
+//                tint = Color.White
+//            )
         }
     }
 }

+ 15 - 0
app/src/main/java/com/iscs/bozzys/ui/pages/message/PageMessage.kt

@@ -186,6 +186,21 @@ private fun MessageList(vm: VMMessage) {
             listState.firstVisibleItemIndex
         }
     }
+    // 监听列表是否滑动到底部
+    val shouldLoadMore by remember {
+        derivedStateOf {
+            val layoutInfo = listState.layoutInfo
+            val totalItems = layoutInfo.totalItemsCount
+            val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+            lastVisibleItemIndex >= totalItems - 2
+        }
+    }
+    // 处理加载更多数据
+    LaunchedEffect(shouldLoadMore) {
+        if (shouldLoadMore && state.messages.isNotEmpty() && !state.page.noMore) {
+            vm.getMessage(state.page.copy(page = state.page.page + 1, isRefresh = false))
+        }
+    }
     PullToRefreshBox(state.page.isRefresh, onRefresh = {
         vm.getMessage(StatePageMessage(isRefresh = true, page = 1))
     }, modifier = Modifier.fillMaxSize()) {

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/profile/PageProfile.kt

@@ -212,7 +212,7 @@ class PageProfile : PageBase() {
                     }
                     SpacerLine()
                     // 人脸数据
-                    UserInfoItem("人脸", "") { openPageFace(state.user.id) }
+                    UserInfoItem("人脸", "") { openPageFace(state.user.id, state.user.username) }
                 }
                 ProfileDialog(
                     show = state.showModifyDialog,

+ 61 - 11
app/src/main/java/com/iscs/bozzys/ui/pages/profile/face/PageFace.kt

@@ -1,11 +1,17 @@
 package com.iscs.bozzys.ui.pages.profile.face
 
+import android.Manifest
 import android.content.Context
 import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
@@ -33,19 +39,23 @@ import androidx.lifecycle.viewmodel.compose.viewModel
 import coil.compose.AsyncImage
 import com.iscs.bozzys.R
 import com.iscs.bozzys.api.UserCharacteristic
+import com.iscs.bozzys.ui.common.Empty
 import com.iscs.bozzys.ui.common.PageBase
 import com.iscs.bozzys.ui.common.Title
 import com.iscs.bozzys.ui.dialog.TipsDialog
 import com.iscs.bozzys.ui.pages.compose.CardBox
 import com.iscs.bozzys.ui.pages.vm.VMFace
 import com.iscs.bozzys.utils.DateUtil.getShowDateOrTime
+import com.iscs.bozzys.utils.SystemUtil.uriToFile
+import java.io.File
 
 /**
  * 打开人脸录入界面
  */
-fun Context.openPageFace(userId: Int) {
+fun Context.openPageFace(userId: Int, username: String) {
     startActivity(Intent(this, PageFace::class.java).apply {
         putExtra("userId", userId)
+        putExtra("username", username)
     })
 }
 
@@ -54,10 +64,43 @@ fun Context.openPageFace(userId: Int) {
  */
 class PageFace : PageBase() {
 
+
+    // 跳转到选择照片页面
+    private val imageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+        uriToFile(uri)?.let { onImagePickerFinish(it) }
+    }
+
+    // 照片选择成功回调
+    private var onImagePickerFinish: (file: File) -> Unit = {}
+
+    /**
+     * 执行照片选择
+     */
+    private fun pickerImage(done: (file: File) -> Unit) {
+        this.onImagePickerFinish = done
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+                requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1001)
+                return
+            }
+        }
+        imageLauncher.launch("image/*")
+    }
+
+    /**
+     * 获取用户id
+     */
     private fun getUserId(): Int {
         return intent.getIntExtra("userId", 0)
     }
 
+    /**
+     * 从页面携带数据中获取用户名称
+     */
+    private fun getUsername(): String {
+        return intent.getStringExtra("username") ?: ""
+    }
+
     @Composable
     override fun GetViews(pv: PaddingValues) {
         val vm: VMFace = viewModel()
@@ -65,22 +108,29 @@ class PageFace : PageBase() {
         LaunchedEffect(Unit) {
             vm.loading.initLoading()
             vm.toast.initToast()
-            vm.init(getUserId())
+            vm.init(getUserId(), getUsername())
         }
-        Column() {
-            Title(pv, "人脸")
-            Column(
-                modifier = Modifier
-                    .padding(horizontal = 16.dp, vertical = 8.dp)
-                    .verticalScroll(rememberScrollState())
-            ) {
-                state.faceList.forEach { FaceListItem(vm, it) }
+        Column {
+            Title(pv, "人脸", rightShow = true, rightIcon = R.drawable.add, rightClick = {
+                pickerImage { vm.uploadUserFace(it) }
+            })
+            Box(contentAlignment = Alignment.Center) {
+                Column(
+                    modifier = Modifier
+                        .padding(horizontal = 16.dp, vertical = 8.dp)
+                        .fillMaxSize()
+                        .verticalScroll(rememberScrollState())
+                ) {
+                    state.faceList.forEach { FaceListItem(vm, it) }
+                }
+                if (state.faceList.isEmpty() && !state.isFirstLoad) Empty(tips = "暂无人脸数据")
             }
         }
+
     }
 
     /**
-     * 人脸特诊数据
+     * 人脸特数据
      */
     @Composable
     fun FaceListItem(vm: VMFace, face: UserCharacteristic) {

+ 49 - 6
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMFace.kt

@@ -10,6 +10,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
+import java.io.File
 
 /**
  * 人脸数据模型页面
@@ -23,11 +24,15 @@ class VMFace : VMBase() {
     // 当前用户id
     private var userId = 0
 
+    // 用户名称,上传特征的时候需要调用
+    private var username = ""
+
     /**
      * 做数据初始化操作
      */
-    fun init(userId: Int = 0) {
+    fun init(userId: Int = 0, username: String = "") {
         this.userId = userId
+        this.username = username
         getFaceList(true)
     }
 
@@ -38,10 +43,10 @@ class VMFace : VMBase() {
         viewModelScope.launch(Dispatchers.IO) {
             if (showLoading) loading.emit(StateLoading(show = true))
             ApiRequest.getUserCharacteristicList(mutableMapOf("pageNo" to 1, "pageSize" to 10, "userId" to userId, "type" to 2)).onSuccess {
-                delay(500)
+                delay(200)
                 loading.emit(StateLoading())
                 delay(100)
-                _state.value = _state.value.copy(faceList = (it.data?.list ?: emptyList()).toMutableList())
+                _state.value = _state.value.copy(faceList = (it.data?.list ?: emptyList()).toMutableList(), isFirstLoad = false)
             }.onFailure {
                 delay(500)
                 loading.emit(StateLoading())
@@ -62,8 +67,7 @@ class VMFace : VMBase() {
                 delay(500)
                 loading.emit(StateLoading())
                 delay(100)
-                val list = _state.value.faceList.filter { it.id != face.id }.toMutableList()
-                _state.value = _state.value.copy(faceList = list)
+                getFaceList()
             }.onFailure {
                 delay(500)
                 loading.emit(StateLoading())
@@ -73,10 +77,43 @@ class VMFace : VMBase() {
         }
     }
 
+    /**
+     * 上传人脸照片
+     *
+     * @param file
+     */
+    fun uploadUserFace(file: File) {
+        viewModelScope.launch(Dispatchers.IO) {
+            loading.emit(StateLoading(show = true))
+            ApiRequest.uploadUserFace(username, file).onSuccess {
+                delay(500)
+                loading.emit(StateLoading())
+                delay(100)
+                getFaceList()
+            }.onFailure {
+                delay(500)
+                loading.emit(StateLoading())
+                delay(100)
+                val ersp = it.getResponse<Any>()
+                if (ersp.code == 500) {
+                    toast.emit("未识别到人脸,请重新上传")
+                } else {
+                    toast.emit(ersp.msg)
+                }
+            }
+        }
+    }
+
+    /**
+     * 显示删除弹窗
+     */
     fun showDeleteTips() {
         _state.value = _state.value.copy(showDeleteTips = true)
     }
 
+    /**
+     * 隐藏删除弹窗
+     */
     fun hideDeleteTips() {
         _state.value = _state.value.copy(showDeleteTips = false)
     }
@@ -87,5 +124,11 @@ class VMFace : VMBase() {
  * 人脸页面状态存储
  *
  * @param faceList  人脸列表
+ * @param showDeleteTips    是否显示删除弹框
+ * @param isFirstLoad       是否首次加载,以免
  */
-data class StateFace(val faceList: MutableList<UserCharacteristic> = mutableListOf(), val showDeleteTips: Boolean = false)
+data class StateFace(
+    val faceList: MutableList<UserCharacteristic> = mutableListOf(),
+    val showDeleteTips: Boolean = false,
+    val isFirstLoad: Boolean = true
+)

+ 4 - 2
app/src/main/java/com/iscs/bozzys/utils/Exts.kt

@@ -7,6 +7,8 @@ fun List<String>.getRoleName(): String {
     val roles = Storage.readRoleList()
     return if (this.contains("super_admin")) {
         roles.find { it.code == "super_admin" }?.name ?: ""
+    } else if (this.contains("tenant_admin")) {
+        roles.find { it.code == "tenant_admin" }?.name ?: ""
     } else if (this.contains("jtdrawer")) {
         roles.find { it.code == "jtdrawer" }?.name ?: ""
     } else if (this.contains("jtlocker")) {
@@ -23,7 +25,7 @@ fun List<String>.getRoleName(): String {
  */
 fun List<String>?.isAdmin(): Boolean {
     if (this == null) return false
-    return this.contains("super_admin")
+    return this.contains("super_admin") || this.contains("tenant_admin")
 }
 
 /**
@@ -60,7 +62,7 @@ fun Int.toByteArray(capability: Int = 2): ByteArray {
  * CRC16 校验值
  * @return 两字节的校验值
  */
-fun ByteArray.crc16(from: Int = 0, to: Int = size) : ByteArray {
+fun ByteArray.crc16(from: Int = 0, to: Int = size): ByteArray {
     val value = CRC16.crc16(this, from.coerceAtLeast(0), to.coerceAtMost(size))
     val c1 = (0xff00 and value shr 8).toByte()
     val c2 = (0xff and value).toByte()

+ 18 - 16
app/src/main/java/com/iscs/bozzys/utils/network/Request.kt

@@ -5,6 +5,7 @@ import com.iscs.bozzys.utils.DateUtil.format
 import com.iscs.bozzys.utils.LogUtil
 import okhttp3.OkHttpClient
 import okhttp3.Request
+import okhttp3.logging.HttpLoggingInterceptor
 import okio.Buffer
 import retrofit2.Retrofit
 import retrofit2.converter.gson.GsonConverterFactory
@@ -32,22 +33,23 @@ object Request {
 
     // 构建请求客户端
     private val okClient = OkHttpClient.Builder()
-        .addInterceptor {
-            val start = System.currentTimeMillis()
-            // 请求拦截 处理一些需要处理的数据,或者打印
-            val request = it.request()
-            val response: okhttp3.Response
-            try {
-                response = it.proceed(request)
-                val end = System.currentTimeMillis()
-                printLog(request, response, null, start, end)
-            } catch (e: Exception) {
-                val end = System.currentTimeMillis()
-                printLog(request, null, e, start, end)
-                throw e
-            }
-            return@addInterceptor response
-        }
+//        .addInterceptor {
+//            val start = System.currentTimeMillis()
+//            // 请求拦截 处理一些需要处理的数据,或者打印
+//            val request = it.request()
+//            val response: okhttp3.Response
+//            try {
+//                response = it.proceed(request)
+//                val end = System.currentTimeMillis()
+//                printLog(request, response, null, start, end)
+//            } catch (e: Exception) {
+//                val end = System.currentTimeMillis()
+//                printLog(request, null, e, start, end)
+//                throw e
+//            }
+//            return@addInterceptor response
+//        }
+        .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
         .sslSocketFactory(getSocketFactory(), createTrustManager())
         .hostnameVerifier { _, _ -> true }
         .build()