From c073c88e101a5fad2830d704febc26f31a0f54eb Mon Sep 17 00:00:00 2001 From: maxinyu Date: Tue, 26 May 2026 11:50:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=A4=B4=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../defenseapplication/bean/UploadResponse.kt | 13 ++ .../personal/PersonalProfileActivity.kt | 52 +++++-- .../defenseapplication/utils/ApiService.kt | 9 ++ .../defenseapplication/utils/OssUtil.kt | 129 ------------------ .../defenseapplication/view/LoadingDialog.kt | 52 +++++++ app/src/main/res/layout/dialog_loading.xml | 26 ++++ app/src/main/res/values-zh/strings.xml | 8 ++ 7 files changed, 145 insertions(+), 144 deletions(-) create mode 100644 app/src/main/java/com/example/defenseapplication/bean/UploadResponse.kt delete mode 100644 app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt create mode 100644 app/src/main/java/com/example/defenseapplication/view/LoadingDialog.kt create mode 100644 app/src/main/res/layout/dialog_loading.xml diff --git a/app/src/main/java/com/example/defenseapplication/bean/UploadResponse.kt b/app/src/main/java/com/example/defenseapplication/bean/UploadResponse.kt new file mode 100644 index 0000000..8939fca --- /dev/null +++ b/app/src/main/java/com/example/defenseapplication/bean/UploadResponse.kt @@ -0,0 +1,13 @@ +package com.example.defenseapplication.bean + +data class UploadResponse( + val code: Int, + val msg: String, + val data: UploadData? +) + +data class UploadData( + val url: String, + val fileName: String?, + val ossId: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt b/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt index 35d0e06..c8cc00d 100644 --- a/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt +++ b/app/src/main/java/com/example/defenseapplication/personal/PersonalProfileActivity.kt @@ -4,8 +4,6 @@ import android.Manifest import android.content.ContentResolver import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri import android.os.Build import android.os.Bundle @@ -26,16 +24,19 @@ import com.example.defenseapplication.R import com.example.defenseapplication.bean.UpdateUserInfo import com.example.defenseapplication.bean.UserInfoBean import com.example.defenseapplication.databinding.PersonalProfileActivityBinding -import com.example.defenseapplication.utils.OssUtil import com.example.defenseapplication.utils.RetrofitNetwork import com.example.defenseapplication.view.AvatarSelectDialog import com.example.defenseapplication.view.CustomDialogFragmentText +import com.example.defenseapplication.view.LoadingDialog import com.example.defenseapplication.view.PhoneModifyDialogFragment import com.gyf.immersionbar.ImmersionBar import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -51,6 +52,9 @@ class PersonalProfileActivity : AppCompatActivity() { // 拍照返回的文件 private var photoFile: File? = null + // 加载弹窗 + private lateinit var loadingDialog: LoadingDialog + // 请求权限回调 private val requestPermissionsLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() @@ -116,6 +120,8 @@ class PersonalProfileActivity : AppCompatActivity() { .statusBarDarkFont(true) .titleBar(binding.topBar) .init() + // 初始化加载弹窗 + loadingDialog = LoadingDialog(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { userInfo = intent.getParcelableExtra("userInfo", UserInfoBean::class.java) } else { @@ -327,7 +333,6 @@ class PersonalProfileActivity : AppCompatActivity() { ".jpg", storageDir ).apply { - // 保存文件路径以便后续使用 Log.d("PersonalProfile", "创建图片文件: $absolutePath") } } @@ -352,7 +357,7 @@ class PersonalProfileActivity : AppCompatActivity() { val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) val file = File(storageDir, "JPEG_${timeStamp}_temp.jpg") - + FileOutputStream(file).use { outputStream -> val buffer = ByteArray(1024) var bytesRead: Int @@ -370,18 +375,34 @@ class PersonalProfileActivity : AppCompatActivity() { } /** - * 上传头像到 OSS 并更新个人信息 + * 上传头像到服务器并更新个人信息 */ private fun uploadAvatar(filePath: String) { - Toast.makeText(this, getString(R.string.uploading), Toast.LENGTH_SHORT).show() + // 显示加载弹窗 + loadingDialog.show() - OssUtil.uploadFileAsync(this, filePath, "avatar") { ossUrl -> - runOnUiThread { - if (ossUrl != null) { - Log.d("PersonalProfile", "OSS 上传成功,URL: $ossUrl") - updateAvatar(ossUrl) - } else { - Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show() + CoroutineScope(Dispatchers.IO).launch { + try { + val file = File(filePath) + val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) + val part = MultipartBody.Part.createFormData("file", file.name, requestBody) + + val uploadResponse = RetrofitNetwork.getNetworkService().uploadFile(part) + + withContext(Dispatchers.Main) { + if (uploadResponse.code == 200 && uploadResponse.data?.url != null) { + Log.d("PersonalProfile", "文件上传成功,URL: ${uploadResponse.data?.url}") + updateAvatar(uploadResponse.data!!.url) + } else { + loadingDialog.dismiss() + Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show() + } + } + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Main) { + loadingDialog.dismiss() + Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show() } } } @@ -399,8 +420,8 @@ class PersonalProfileActivity : AppCompatActivity() { ) val response = RetrofitNetwork.getNetworkService().updateUserInfo(request) withContext(Dispatchers.Main) { + loadingDialog.dismiss() if (response.code == 200) { - // 更新本地头像显示 Glide.with(this@PersonalProfileActivity) .load(avatarUrl) .apply(RequestOptions() @@ -417,6 +438,7 @@ class PersonalProfileActivity : AppCompatActivity() { } catch (e: Exception) { e.printStackTrace() withContext(Dispatchers.Main) { + loadingDialog.dismiss() Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/com/example/defenseapplication/utils/ApiService.kt b/app/src/main/java/com/example/defenseapplication/utils/ApiService.kt index 5e9801f..3d8d795 100644 --- a/app/src/main/java/com/example/defenseapplication/utils/ApiService.kt +++ b/app/src/main/java/com/example/defenseapplication/utils/ApiService.kt @@ -19,10 +19,13 @@ import com.example.defenseapplication.bean.ObtainInviterInformationRequest import com.example.defenseapplication.bean.SwitchFamilyRequest import com.example.defenseapplication.bean.UpdatePassword import com.example.defenseapplication.bean.UpdateUserInfo +import com.example.defenseapplication.bean.UploadResponse import com.example.defenseapplication.bean.UserInfoBean import com.example.defenseapplication.bean.VerifyOptionPasswordRequest +import okhttp3.MultipartBody import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Query @@ -206,6 +209,12 @@ interface ApiService { @POST("/app/revokeInvitation") suspend fun revokeInvitation(@Body request: VerifyOptionPasswordRequest): GetCodeBean + /** + * 上传文件接口 + */ + @Multipart + @POST("/app/upload") + suspend fun uploadFile(@retrofit2.http.Part file: MultipartBody.Part): UploadResponse /** * 修改家庭用户状态 diff --git a/app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt b/app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt deleted file mode 100644 index e22e8b2..0000000 --- a/app/src/main/java/com/example/defenseapplication/utils/OssUtil.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.example.defenseapplication.utils - -import android.content.Context -import android.util.Log -import com.alibaba.sdk.android.oss.ClientConfiguration -import com.alibaba.sdk.android.oss.OSS -import com.alibaba.sdk.android.oss.OSSClient -import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider -import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider -import com.alibaba.sdk.android.oss.model.PutObjectRequest -import com.alibaba.sdk.android.oss.model.PutObjectResult -import java.io.File -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -/** - * OSS 工具类 - * 用于上传文件到阿里云 OSS - */ -object OssUtil { - - private const val TAG = "OssUtil" - - // OSS 配置信息 - 需要根据实际情况配置 - private const val ENDPOINT = "your-oss-endpoint" // 例如: oss-cn-hangzhou.aliyuncs.com - private const val ACCESS_KEY_ID = "your-access-key-id" - private const val ACCESS_KEY_SECRET = "your-access-key-secret" - private const val BUCKET_NAME = "your-bucket-name" - - private var ossClient: OSS? = null - - /** - * 初始化 OSS 客户端 - */ - private fun initOssClient(context: Context): OSS { - if (ossClient == null) { - // 创建凭证提供者 - val credentialProvider: OSSCredentialProvider = - OSSPlainTextAKSKCredentialProvider(ACCESS_KEY_ID, ACCESS_KEY_SECRET) - - // 创建配置 - val conf = ClientConfiguration().apply { - connectionTimeout = 15 * 1000 // 连接超时,默认15秒 - socketTimeout = 15 * 1000 // socket超时,默认15秒 - maxConcurrentRequest = 5 // 最大并发请求数,默认5 - maxErrorRetry = 2 // 失败后最大重试次数,默认2 - } - - // 创建 OSS 客户端 - ossClient = OSSClient(context.applicationContext, ENDPOINT, credentialProvider, conf) - } - return ossClient!! - } - - /** - * 上传文件到 OSS - * @param context 上下文 - * @param filePath 文件路径 - * @param folderName 文件夹名称(可选,用于分类存储) - * @return OSS 文件 URL - */ - fun uploadFile(context: Context, filePath: String, folderName: String = "avatar"): String? { - return try { - val file = File(filePath) - if (!file.exists()) { - Log.e(TAG, "File not found: $filePath") - return null - } - - // 生成文件名:时间戳 + 原文件名 - val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date()) - val originalFileName = file.name - val fileName = "${timestamp}_${originalFileName}" - val objectKey = "$folderName/$fileName" - - Log.d(TAG, "Uploading file: $filePath to OSS key: $objectKey") - - // 创建上传请求 - val putObjectRequest = PutObjectRequest(BUCKET_NAME, objectKey, filePath) - - // 执行上传 - val oss = initOssClient(context) - val result: PutObjectResult = oss.putObject(putObjectRequest) - - Log.d(TAG, "Upload success, ETAG: ${result.eTag}") - - // 返回文件 URL - "https://$BUCKET_NAME.$ENDPOINT/$objectKey" - - } catch (e: Exception) { - Log.e(TAG, "Upload failed: ${e.message}", e) - null - } - } - - /** - * 异步上传文件到 OSS - * @param context 上下文 - * @param filePath 文件路径 - * @param folderName 文件夹名称 - * @param callback 回调函数 - */ - fun uploadFileAsync( - context: Context, - filePath: String, - folderName: String = "avatar", - callback: (String?) -> Unit - ) { - Thread { - val result = uploadFile(context, filePath, folderName) - callback(result) - }.start() - } - - /** - * 配置 OSS 参数(动态配置) - * @param endpoint OSS 端点 - * @param accessKeyId Access Key ID - * @param accessKeySecret Access Key Secret - * @param bucketName Bucket 名称 - */ - fun setOssConfig(endpoint: String, accessKeyId: String, accessKeySecret: String, bucketName: String) { - // 由于常量无法修改,这里需要重新创建客户端 - ossClient = null - // 注意:实际项目中应该使用动态配置,这里简化处理 - Log.d(TAG, "OSS config updated") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/defenseapplication/view/LoadingDialog.kt b/app/src/main/java/com/example/defenseapplication/view/LoadingDialog.kt new file mode 100644 index 0000000..785c9cc --- /dev/null +++ b/app/src/main/java/com/example/defenseapplication/view/LoadingDialog.kt @@ -0,0 +1,52 @@ +package com.example.defenseapplication.view + +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.WindowManager +import com.example.defenseapplication.databinding.DialogLoadingBinding + +class LoadingDialog(context: Context) { + + private val dialog: Dialog = Dialog(context) + private val binding: DialogLoadingBinding + + init { + binding = DialogLoadingBinding.inflate(LayoutInflater.from(context)) + dialog.setContentView(binding.root) + dialog.window?.apply { + setGravity(Gravity.CENTER) + setLayout( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setDimAmount(0.5f) + } + dialog.setCancelable(false) + } + + fun setMessage(message: String): LoadingDialog { + binding.tvLoading.text = message + return this + } + + fun show() { + if (!dialog.isShowing) { + dialog.show() + } + } + + fun dismiss() { + if (dialog.isShowing) { + dialog.dismiss() + } + } + + fun isShowing(): Boolean { + return dialog.isShowing + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml new file mode 100644 index 0000000..cd687d7 --- /dev/null +++ b/app/src/main/res/layout/dialog_loading.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 04ac085..b22929d 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -461,4 +461,12 @@ 从相册选择 选择日期 选择时间 + 需要相机权限 + 需要存储权限 + 需要写入存储权限 + 拍照失败 + 获取文件失败 + 创建文件失败 + 上传中... + 上传失败 \ No newline at end of file