Compare commits

..

2 Commits

Author SHA1 Message Date
623ebc22f7 feat: opt pic 2026-04-17 14:59:16 +08:00
1157a0c4ed fix: 修复图片上传字段语义颠倒及加载缺失鉴权头导致的 403
- 修正 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 <noreply@anthropic.com>
2026-04-17 14:57:26 +08:00
10 changed files with 131 additions and 104 deletions

View File

@@ -1013,21 +1013,21 @@ companion object {
上传图片后提交表单时,**必须同时传 `pic``originalPic``picNumber` 三个字段**,缺一不可。 上传图片后提交表单时,**必须同时传 `pic``originalPic``picNumber` 三个字段**,缺一不可。
**`UploadUtil.upload()` 返回值**: **`UploadUtil.upload()` 返回值**(注意:**与字面意思相反**:
- `data?.newName`缩略图/压缩图文件名 - `data?.newName`**原图**文件名(较大)
- `data?.zipFileName`原图文件名 - `data?.zipFileName`**缩略图/压缩图**文件名(较小)
**提交时字段映射**(参考事故签证 `AccidentVisaDetailsViewModel`: **提交时字段映射**(参考事故签证 `AccidentVisaDetailsViewModel``IntImpAccidentVisaEditViewModel`:
```kotlin ```kotlin
// FileBean 字段含义: // FileBean 字段含义(约定用途,与 UploadBean 字段名不一致)
// - FileBean.url = newName缩略图文件名 // - FileBean.url 作缩略图标识(提交到 bean.pic
// - FileBean.originalPic = zipFileName原图文件名 // - FileBean.originalPic 作原图标识(提交到 bean.originalPic
// 上传新图片 // 上传新图片(注意 UploadBean 字段名的误导性,按实际含义赋值)
val data = UploadUtil.upload(fileBean.path).data val data = UploadUtil.upload(fileBean.path).data
fileBean.url = data?.newName ?: "" fileBean.url = data?.zipFileName ?: "" // 缩略图
fileBean.originalPic = data?.zipFileName ?: "" fileBean.originalPic = data?.newName ?: "" // 原图
// 提交时设置三个字段 // 提交时设置三个字段
bean.picNumber = list.size.toString() bean.picNumber = list.size.toString()
@@ -1037,7 +1037,8 @@ bean.originalPic = list.joinToString(",") { MediaUtil.removeUrl(it.originalPic)
**常见错误**: **常见错误**:
- ❌ 只传 `images``originalPic` 单个字段 — 接口不认或数据不完整 - ❌ 只传 `images``originalPic` 单个字段 — 接口不认或数据不完整
- ❌ 只取 `newName` 不取 `zipFileName` — 丢失原图路径 - ❌ 只取 `newName` 不取 `zipFileName` — 丢失缩略图/原图之一
- ❌ 按 `UploadBean` 字段字面含义赋值(`url = newName`)— 会导致 pic/originalPic 内容和字段语义颠倒(缩略图字段装原图、原图字段装缩略图)
- ❌ 用 `fileBean.path.startsWith("http")` 判断已上传 — 应该用 `fileBean.url.isNotEmpty()` - ❌ 用 `fileBean.path.startsWith("http")` 判断已上传 — 应该用 `fileBean.url.isNotEmpty()`
### 编辑页加载已有图片 ### 编辑页加载已有图片

View File

@@ -75,23 +75,18 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener {
onSuccess = { onSuccess = {
dataBean.value = it.data ?: AccidentVisaBean() dataBean.value = it.data ?: AccidentVisaBean()
// 渲染图片 // 渲染图片pic 存缩略图文件名originalPic 存原图文件名
val list = dataBean.value!!.pic.split(",") val picList = dataBean.value!!.pic.split(",").filter { it.isNotEmpty() }
.filter { url -> url.isNotEmpty() } val originalPicList = dataBean.value!!.originalPic.split(",").filter { it.isNotEmpty() }
.map { url -> val list = picList.mapIndexed { index, picFilename ->
FileBean(MediaUtil.fillUrl(url), url) val originalFilename = originalPicList.getOrElse(index) { picFilename }
} FileBean(
val zipList = dataBean.value!!.originalPic.split(",") path = MediaUtil.fillUrl(picFilename),
.filter { url -> url.isNotEmpty() } url = picFilename,
.map { url -> originalPic = MediaUtil.fillUrl(originalFilename)
FileBean(MediaUtil.fillUrl(url)) )
}
for ((index, fileBean) in list.withIndex()) {
val originalPic = zipList.get(index).path
list.get(index).originalPic = originalPic
} }
rv?.commonAdapter() rv?.commonAdapter()?.loadMore(list)
?.loadMore(list)
} }
} }
} }
@@ -110,8 +105,10 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener {
.filter { it.path.isNotEmpty() && it.url.isEmpty() } .filter { it.path.isNotEmpty() && it.url.isEmpty() }
.onEach { .onEach {
val data = UploadUtil.upload(it.path).data val data = UploadUtil.upload(it.path).data
it.url = data?.newName ?: "" // UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
it.originalPic = data?.zipFileName ?: "" // FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
it.url = data?.zipFileName ?: ""
it.originalPic = data?.newName ?: ""
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.onStart { showLoading() } .onStart { showLoading() }
@@ -124,8 +121,8 @@ class AccidentVisaDetailsViewModel : BaseViewModel(), IOnItemClickListener {
val list = val list =
(rv?.commonAdapter()?.items as List<FileBean>).filter { it.path.isNotEmpty() } (rv?.commonAdapter()?.items as List<FileBean>).filter { it.path.isNotEmpty() }
bean.picnumber = list.size.toString() bean.picnumber = list.size.toString()
bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) } bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.url) }
bean.pic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) } bean.originalPic = list.joinToString(separator = ",") { MediaUtil.removeUrl(it.originalPic) }
NetApply.api.anyPost( NetApply.api.anyPost(
url = if (pageType.value == DetailsPageType.Add) "GnAccidentVisa/saveVisa" else "GnAccidentVisa/updateVisa", url = if (pageType.value == DetailsPageType.Add) "GnAccidentVisa/saveVisa" else "GnAccidentVisa/updateVisa",

View File

@@ -41,7 +41,7 @@ import me.jessyan.autosize.internal.CustomAdapt
* ========== 开发调试开关 ========== * ========== 开发调试开关 ==========
* TODO: 正式发布前务必设置为 false * TODO: 正式发布前务必设置为 false
*/ */
private const val DEV_AUTO_LOGIN = false // 自动登录开关 private const val DEV_AUTO_LOGIN = true // 自动登录开关
@Route(path = ARouterConstants.ACTIVITY_URL_LOGIN) @Route(path = ARouterConstants.ACTIVITY_URL_LOGIN)
class LoginActivity : BaseActivity(), class LoginActivity : BaseActivity(),

View File

@@ -12,12 +12,16 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.CenterCrop
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.base.CommonAdapter 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.ktx.loge
import com.lukouguoji.module_base.util.SizeUtils import com.lukouguoji.module_base.util.SizeUtils
@@ -111,10 +115,22 @@ fun loadImage(
com.bumptech.glide.request.target.Target.SIZE_ORIGINAL 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) val load = Glide.with(imageView)
.setDefaultRequestOptions(requestOptions) .setDefaultRequestOptions(requestOptions)
.load(source) .load(actualSource)
.diskCacheStrategy(diskCacheStrategy) .diskCacheStrategy(diskCacheStrategy)
.encodeFormat(encodeFormat) .encodeFormat(encodeFormat)

View File

@@ -1,17 +1,19 @@
package com.lukouguoji.module_base.impl package com.lukouguoji.module_base.impl
import android.view.View import android.view.View
import com.luck.picture.lib.adapter.holder.PreviewImageHolder import com.bumptech.glide.Glide
import com.luck.picture.lib.basic.PictureSelector import com.bumptech.glide.load.model.GlideUrl
import com.lukouguoji.module_base.adapter.loadImage import com.bumptech.glide.load.model.LazyHeaders
import com.lukouguoji.module_base.base.BaseViewHolder import com.lukouguoji.module_base.base.BaseViewHolder
import com.lukouguoji.module_base.bean.FileBean 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.databinding.ItemImageSelectBinding
import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil
import com.lukouguoji.module_base.ktx.commonAdapter import com.lukouguoji.module_base.ktx.commonAdapter
import com.lukouguoji.module_base.ktx.logd 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.ui.page.preview.PreviewActivity
import com.lukouguoji.module_base.util.MediaUtil import com.lukouguoji.module_base.util.MediaUtil
import java.io.File
class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSelectBinding>(view) { class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSelectBinding>(view) {
@@ -19,6 +21,21 @@ class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSele
val bean = getItemBean(item)!! val bean = getItemBean(item)!!
binding.bean = bean 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 { binding.rl.setOnClickListener {
if (bean.path.isEmpty()) { if (bean.path.isEmpty()) {
MediaUtil.pickImage(itemView.context, maxNum = 10) { MediaUtil.pickImage(itemView.context, maxNum = 10) {
@@ -28,7 +45,15 @@ class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSele
} }
} }
} else { } else {
PreviewActivity.start(itemView.context, listOf(bean)) val items = getRecyclerView()?.commonAdapter()?.items
?.filterIsInstance<FileBean>()
?.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)
} }
} }
@@ -39,13 +64,6 @@ class ImageSelectViewHolder(view: View) : BaseViewHolder<FileBean, ItemImageSele
} }
notifyItemClick(position, binding.ivDelete) notifyItemClick(position, binding.ivDelete)
if (bean.isOnlineResource()) {
loge("开始下载 : ${bean.path}")
bean.download {
loadImage(binding.iv, it)
}
}
} }
} }

View File

@@ -24,7 +24,6 @@
<ImageView <ImageView
android:id="@+id/iv" android:id="@+id/iv"
loadImage="@{bean.path}"
visible="@{bean.path}" visible="@{bean.path}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -277,8 +277,10 @@ class IntImpAccidentVisaEditViewModel : BaseViewModel(), IOnItemClickListener {
.filter { it.path.isNotEmpty() && it.url.isEmpty() } .filter { it.path.isNotEmpty() && it.url.isEmpty() }
.onEach { .onEach {
val data = UploadUtil.upload(it.path).data val data = UploadUtil.upload(it.path).data
it.url = data?.newName ?: "" // UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
it.originalPic = data?.zipFileName ?: "" // FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
it.url = data?.zipFileName ?: ""
it.originalPic = data?.newName ?: ""
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.onStart { showLoading() } .onStart { showLoading() }

View File

@@ -148,20 +148,10 @@
title='@{"品名(中)"}' title='@{"品名(中)"}'
titleLength="@{5}" titleLength="@{5}"
type="@{DataLayoutType.INPUT}" type="@{DataLayoutType.INPUT}"
value='@{viewModel.dataBean.goodsCn}' value='@{viewModel.dataBean.goods}'
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" /> android:layout_weight="2" />
<com.lukouguoji.module_base.ui.weight.data.layout.PadDataLayoutNew
enable="@{false}"
title='@{"品名(英)"}'
titleLength="@{5}"
type="@{DataLayoutType.INPUT}"
value='@{viewModel.dataBean.goodsEn}'
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout> </LinearLayout>

View File

@@ -72,7 +72,7 @@ class GnjYiKuEditViewModel : BaseViewModel(), IOnItemClickListener {
val bean = it.data ?: GnjYiKuBean() val bean = it.data ?: GnjYiKuBean()
dataBean.value = bean dataBean.value = bean
// 处理图片列表pic=缩略图(newName)originalPic=原图(zipFileName) // 处理图片列表pic 字段存缩略图文件名originalPic 字段存原图文件名
val picList = bean.pic.split(",").filter { it.isNotEmpty() } val picList = bean.pic.split(",").filter { it.isNotEmpty() }
val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() } val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() }
val images = picList.mapIndexed { index, picUrl -> val images = picList.mapIndexed { index, picUrl ->
@@ -123,9 +123,11 @@ class GnjYiKuEditViewModel : BaseViewModel(), IOnItemClickListener {
// 已上传的图片,保持原有的 url 和 originalPic // 已上传的图片,保持原有的 url 和 originalPic
} else { } else {
// 本地新图片需要上传 // 本地新图片需要上传
// UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
// FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
val data = UploadUtil.upload(fileBean.path).data val data = UploadUtil.upload(fileBean.path).data
fileBean.url = data?.newName ?: "" fileBean.url = data?.zipFileName ?: ""
fileBean.originalPic = data?.zipFileName ?: "" fileBean.originalPic = data?.newName ?: ""
} }
} }

View File

@@ -118,9 +118,11 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener {
// 已上传的图片,保持原有的 url 和 originalPic // 已上传的图片,保持原有的 url 和 originalPic
} else { } else {
// 本地新图片需要上传 // 本地新图片需要上传
// UploadUtil 返回newName=原图(较大)zipFileName=缩略图(较小)
// FileBean.url 用作缩略图标识FileBean.originalPic 用作原图标识
val data = UploadUtil.upload(fileBean.path).data val data = UploadUtil.upload(fileBean.path).data
fileBean.url = data?.newName ?: "" fileBean.url = data?.zipFileName ?: ""
fileBean.originalPic = data?.zipFileName ?: "" fileBean.originalPic = data?.newName ?: ""
} }
} }