From 1157a0c4ed328c7f8d235f36ac417436e215d29f Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Fri, 17 Apr 2026 14:57:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=AD=97=E6=AE=B5=E8=AF=AD=E4=B9=89=E9=A2=A0?= =?UTF-8?q?=E5=80=92=E5=8F=8A=E5=8A=A0=E8=BD=BD=E7=BC=BA=E5=A4=B1=E9=89=B4?= =?UTF-8?q?=E6=9D=83=E5=A4=B4=E5=AF=BC=E8=87=B4=E7=9A=84=20403?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正 UploadUtil 返回字段到 FileBean 的映射: newName 是原图(较大)、zipFileName 是缩略图(较小) - 保证 bean.pic 存缩略图、bean.originalPic 存原图 - 全局 loadImage BindingAdapter 对 http(s) URL 自动包装 GlideUrl + Authorization,避免 /file/getImg/ 接口 403 - ImageSelectViewHolder 缩略图带鉴权加载,点击预览传原图 - 覆盖国内/国际事故签证、国内进港移库/移交编辑页面 - CLAUDE.md 同步修正 UploadBean 字段语义文档 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 23 ++-- .../details/AccidentVisaDetailsViewModel.kt | 37 +++--- .../module_base/adapter/BindingAdapter.kt | 18 ++- .../module_base/impl/ImageSelectViewHolder.kt | 120 ++++++++++-------- .../src/main/res/layout/item_image_select.xml | 1 - .../IntImpAccidentVisaEditViewModel.kt | 6 +- .../page/yiku/edit/GnjYiKuEditViewModel.kt | 8 +- .../yiku/handover/GnjYiKuHandoverViewModel.kt | 6 +- 8 files changed, 128 insertions(+), 91 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 24d0b81..7ea0df8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1013,21 +1013,21 @@ companion object { 上传图片后提交表单时,**必须同时传 `pic`、`originalPic`、`picNumber` 三个字段**,缺一不可。 -**`UploadUtil.upload()` 返回值**: -- `data?.newName` — 缩略图/压缩图文件名 -- `data?.zipFileName` — 原图文件名 +**`UploadUtil.upload()` 返回值**(注意:**与字面意思相反**): +- `data?.newName` — **原图**文件名(较大) +- `data?.zipFileName` — **缩略图/压缩图**文件名(较小) -**提交时字段映射**(参考事故签证 `AccidentVisaDetailsViewModel`): +**提交时字段映射**(参考事故签证 `AccidentVisaDetailsViewModel`、`IntImpAccidentVisaEditViewModel`): ```kotlin -// FileBean 字段含义: -// - FileBean.url = newName(缩略图文件名) -// - FileBean.originalPic = zipFileName(原图文件名) +// FileBean 字段含义(约定用途,与 UploadBean 字段名不一致): +// - FileBean.url 作缩略图标识(提交到 bean.pic) +// - FileBean.originalPic 作原图标识(提交到 bean.originalPic) -// 上传新图片 +// 上传新图片(注意 UploadBean 字段名的误导性,按实际含义赋值) val data = UploadUtil.upload(fileBean.path).data -fileBean.url = data?.newName ?: "" -fileBean.originalPic = data?.zipFileName ?: "" +fileBean.url = data?.zipFileName ?: "" // 缩略图 +fileBean.originalPic = data?.newName ?: "" // 原图 // 提交时设置三个字段 bean.picNumber = list.size.toString() @@ -1037,7 +1037,8 @@ bean.originalPic = list.joinToString(",") { MediaUtil.removeUrl(it.originalPic) **常见错误**: - ❌ 只传 `images` 或 `originalPic` 单个字段 — 接口不认或数据不完整 -- ❌ 只取 `newName` 不取 `zipFileName` — 丢失原图路径 +- ❌ 只取 `newName` 不取 `zipFileName` — 丢失缩略图/原图之一 +- ❌ 按 `UploadBean` 字段字面含义赋值(`url = newName`)— 会导致 pic/originalPic 内容和字段语义颠倒(缩略图字段装原图、原图字段装缩略图) - ❌ 用 `fileBean.path.startsWith("http")` 判断已上传 — 应该用 `fileBean.url.isNotEmpty()` ### 编辑页加载已有图片 diff --git a/app/src/main/java/com/lukouguoji/aerologic/page/accident/visa/details/AccidentVisaDetailsViewModel.kt b/app/src/main/java/com/lukouguoji/aerologic/page/accident/visa/details/AccidentVisaDetailsViewModel.kt index 813000f..490e257 100644 --- a/app/src/main/java/com/lukouguoji/aerologic/page/accident/visa/details/AccidentVisaDetailsViewModel.kt +++ b/app/src/main/java/com/lukouguoji/aerologic/page/accident/visa/details/AccidentVisaDetailsViewModel.kt @@ -75,23 +75,18 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener { onSuccess = { dataBean.value = it.data ?: AccidentVisaBean() - // 渲染图片 - val list = dataBean.value!!.pic.split(",") - .filter { url -> url.isNotEmpty() } - .map { url -> - FileBean(MediaUtil.fillUrl(url), url) - } - val zipList = dataBean.value!!.originalPic.split(",") - .filter { url -> url.isNotEmpty() } - .map { url -> - FileBean(MediaUtil.fillUrl(url)) - } - for ((index, fileBean) in list.withIndex()) { - val originalPic = zipList.get(index).path - list.get(index).originalPic = originalPic + // 渲染图片:pic 存缩略图文件名,originalPic 存原图文件名 + val picList = dataBean.value!!.pic.split(",").filter { it.isNotEmpty() } + val originalPicList = dataBean.value!!.originalPic.split(",").filter { it.isNotEmpty() } + val list = picList.mapIndexed { index, picFilename -> + val originalFilename = originalPicList.getOrElse(index) { picFilename } + FileBean( + path = MediaUtil.fillUrl(picFilename), + url = picFilename, + originalPic = MediaUtil.fillUrl(originalFilename) + ) } - rv?.commonAdapter() - ?.loadMore(list) + rv?.commonAdapter()?.loadMore(list) } } } @@ -110,8 +105,10 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener { .filter { it.path.isNotEmpty() && it.url.isEmpty() } .onEach { val data = UploadUtil.upload(it.path).data - it.url = data?.newName ?: "" - it.originalPic = data?.zipFileName ?: "" + // UploadUtil 返回:newName=原图(较大),zipFileName=缩略图(较小) + // FileBean.url 用作缩略图标识,FileBean.originalPic 用作原图标识 + it.url = data?.zipFileName ?: "" + it.originalPic = data?.newName ?: "" } .flowOn(Dispatchers.IO) .onStart { showLoading() } @@ -124,8 +121,8 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener { val list = (rv?.commonAdapter()?.items as List).filter { it.path.isNotEmpty() } bean.picnumber = list.size.toString() - bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) } - bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) } + bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) } + bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) } NetApply.api.anyPost( url = if (pageType.value == DetailsPageType.Add) "GnAccidentVisa/saveVisa" else "GnAccidentVisa/updateVisa", diff --git a/module_base/src/main/java/com/lukouguoji/module_base/adapter/BindingAdapter.kt b/module_base/src/main/java/com/lukouguoji/module_base/adapter/BindingAdapter.kt index e82981f..f5d932d 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/adapter/BindingAdapter.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/adapter/BindingAdapter.kt @@ -12,12 +12,16 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.base.CommonAdapter +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil import com.lukouguoji.module_base.ktx.loge import com.lukouguoji.module_base.util.SizeUtils @@ -111,10 +115,22 @@ fun loadImage( com.bumptech.glide.request.target.Target.SIZE_ORIGINAL ) + // 对 http(s) 字符串 URL,自动包装为带 Authorization header 的 GlideUrl,避免 /file/getImg/ 接口 403 + val actualSource: Any? = if (source is String && (source.startsWith("http://") || source.startsWith("https://"))) { + GlideUrl( + source, + LazyHeaders.Builder() + .addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token)) + .build() + ) + } else { + source + } + // 设置图片加载 val load = Glide.with(imageView) .setDefaultRequestOptions(requestOptions) - .load(source) + .load(actualSource) .diskCacheStrategy(diskCacheStrategy) .encodeFormat(encodeFormat) diff --git a/module_base/src/main/java/com/lukouguoji/module_base/impl/ImageSelectViewHolder.kt b/module_base/src/main/java/com/lukouguoji/module_base/impl/ImageSelectViewHolder.kt index 7b849ac..59e41fc 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/impl/ImageSelectViewHolder.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/impl/ImageSelectViewHolder.kt @@ -1,51 +1,69 @@ -package com.lukouguoji.module_base.impl - -import android.view.View -import com.luck.picture.lib.adapter.holder.PreviewImageHolder -import com.luck.picture.lib.basic.PictureSelector -import com.lukouguoji.module_base.adapter.loadImage -import com.lukouguoji.module_base.base.BaseViewHolder -import com.lukouguoji.module_base.bean.FileBean -import com.lukouguoji.module_base.databinding.ItemImageSelectBinding -import com.lukouguoji.module_base.ktx.commonAdapter -import com.lukouguoji.module_base.ktx.logd -import com.lukouguoji.module_base.ktx.loge -import com.lukouguoji.module_base.ui.page.preview.PreviewActivity -import com.lukouguoji.module_base.util.MediaUtil - -class ImageSelectViewHolder(view: View) : BaseViewHolder(view) { - - override fun onBind(item: Any?, position: Int) { - val bean = getItemBean(item)!! - binding.bean = bean - - binding.rl.setOnClickListener { - if (bean.path.isEmpty()) { - MediaUtil.pickImage(itemView.context, maxNum = 10) { - it.forEach { - logd("添加了图片 : ${it.realPath}") - getRecyclerView()?.commonAdapter()?.addItem(FileBean(path = it.realPath)) - } - } - } else { - PreviewActivity.start(itemView.context, listOf(bean)) - } - } - - // 长按事件 - binding.rl.setOnLongClickListener { - clickListener?.onItemClick(bindingAdapterPosition, binding.rl.id) - true - } - - notifyItemClick(position, binding.ivDelete) - - if (bean.isOnlineResource()) { - loge("开始下载 : ${bean.path}") - bean.download { - loadImage(binding.iv, it) - } - } - } - -} \ No newline at end of file +package com.lukouguoji.module_base.impl + +import android.view.View +import com.bumptech.glide.Glide +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders +import com.lukouguoji.module_base.base.BaseViewHolder +import com.lukouguoji.module_base.bean.FileBean +import com.lukouguoji.module_base.common.Constant +import com.lukouguoji.module_base.databinding.ItemImageSelectBinding +import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil +import com.lukouguoji.module_base.ktx.commonAdapter +import com.lukouguoji.module_base.ktx.logd +import com.lukouguoji.module_base.ui.page.preview.PreviewActivity +import com.lukouguoji.module_base.util.MediaUtil +import java.io.File + +class ImageSelectViewHolder(view: View) : BaseViewHolder(view) { + + override fun onBind(item: Any?, position: Int) { + val bean = getItemBean(item)!! + binding.bean = bean + + // 加载缩略图 + if (bean.path.isNotEmpty()) { + if (bean.isOnlineResource()) { + val glideUrl = GlideUrl( + bean.path, + LazyHeaders.Builder() + .addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token)) + .build() + ) + Glide.with(itemView.context).load(glideUrl).into(binding.iv) + } else { + Glide.with(itemView.context).load(File(bean.path)).into(binding.iv) + } + } + + binding.rl.setOnClickListener { + if (bean.path.isEmpty()) { + MediaUtil.pickImage(itemView.context, maxNum = 10) { + it.forEach { + logd("添加了图片 : ${it.realPath}") + getRecyclerView()?.commonAdapter()?.addItem(FileBean(path = it.realPath)) + } + } + } else { + val items = getRecyclerView()?.commonAdapter()?.items + ?.filterIsInstance() + ?.filter { it.path.isNotEmpty() } + ?: listOf(bean) + val previewList = items.map { fb -> + FileBean(path = if (fb.originalPic.isNotEmpty()) fb.originalPic else fb.path) + } + val previewPosition = items.indexOfFirst { it === bean }.coerceAtLeast(0) + PreviewActivity.start(itemView.context, previewList, previewPosition) + } + } + + // 长按事件 + binding.rl.setOnLongClickListener { + clickListener?.onItemClick(bindingAdapterPosition, binding.rl.id) + true + } + + notifyItemClick(position, binding.ivDelete) + } + +} diff --git a/module_base/src/main/res/layout/item_image_select.xml b/module_base/src/main/res/layout/item_image_select.xml index 796c1f2..c09ce27 100644 --- a/module_base/src/main/res/layout/item_image_select.xml +++ b/module_base/src/main/res/layout/item_image_select.xml @@ -24,7 +24,6 @@ @@ -123,9 +123,11 @@ class GnjYiKuEditViewModel : BaseViewModel(), IOnItemClickListener { // 已上传的图片,保持原有的 url 和 originalPic } else { // 本地新图片需要上传 + // UploadUtil 返回:newName=原图(较大),zipFileName=缩略图(较小) + // FileBean.url 用作缩略图标识,FileBean.originalPic 用作原图标识 val data = UploadUtil.upload(fileBean.path).data - fileBean.url = data?.newName ?: "" - fileBean.originalPic = data?.zipFileName ?: "" + fileBean.url = data?.zipFileName ?: "" + fileBean.originalPic = data?.newName ?: "" } } diff --git a/module_gnj/src/main/java/com/lukouguoji/gnj/page/yiku/handover/GnjYiKuHandoverViewModel.kt b/module_gnj/src/main/java/com/lukouguoji/gnj/page/yiku/handover/GnjYiKuHandoverViewModel.kt index 28fc2e6..086c3cd 100644 --- a/module_gnj/src/main/java/com/lukouguoji/gnj/page/yiku/handover/GnjYiKuHandoverViewModel.kt +++ b/module_gnj/src/main/java/com/lukouguoji/gnj/page/yiku/handover/GnjYiKuHandoverViewModel.kt @@ -118,9 +118,11 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener { // 已上传的图片,保持原有的 url 和 originalPic } else { // 本地新图片需要上传 + // UploadUtil 返回:newName=原图(较大),zipFileName=缩略图(较小) + // FileBean.url 用作缩略图标识,FileBean.originalPic 用作原图标识 val data = UploadUtil.upload(fileBean.path).data - fileBean.url = data?.newName ?: "" - fileBean.originalPic = data?.zipFileName ?: "" + fileBean.url = data?.zipFileName ?: "" + fileBean.originalPic = data?.newName ?: "" } }