From cf8a7f38fb51bb2464ee16156a78f0f00a7f87c0 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Thu, 2 Apr 2026 16:25:24 +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=E3=80=81=E8=88=AA=E7=8F=AD?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=E5=8F=8A=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E5=8A=A0=E8=BD=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 国际进港舱单列表页航班查询接口改为 /flt/searchFlightList,支持多航班校验 - 修复国内进港移库编辑/交接页图片上传缺少 pic、picNumber 字段 - 国际进港舱单详情页对接交接图片展示 - 图片缩略图和大图预览加载带 Authorization header 解决 403 - CLAUDE.md 新增图片上传与展示规范 Co-Authored-By: Claude Opus 4.6 --- .claude/settings.local.json | 12 ++- .mcp.json | 7 ++ CLAUDE.md | 87 +++++++++++++++++++ .../module_base/bean/GjjManifest.kt | 5 +- .../lukouguoji/module_base/http/net/Api.kt | 6 ++ .../ui/page/preview/PreviewImageViewHolder.kt | 25 +++++- .../main/res/layout/item_preview_image.xml | 2 +- .../gjj/activity/GjjManifestListActivity.kt | 1 + .../activity/IntImpManifestDetailsActivity.kt | 29 +++++++ .../gjj/holder/GjjManifestPicViewHolder.kt | 16 ++++ .../gjj/viewModel/IntImpManifestViewModel.kt | 40 +++++---- .../activity_int_imp_manifest_details.xml | 17 +--- .../main/res/layout/item_gjj_manifest_pic.xml | 1 - .../page/yiku/edit/GnjYiKuEditViewModel.kt | 54 ++++++------ .../yiku/handover/GnjYiKuHandoverViewModel.kt | 16 ++-- 15 files changed, 246 insertions(+), 72 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 09b069f..3a4df93 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -119,13 +119,21 @@ "mcp__apifox__read_project_oas_ruugy8", "mcp__apifox__read_project_oas_ref_resources_ldmedm", "mcp__apifox__read_project_oas_ldmedm", - "mcp__apifox__refresh_project_oas_ldmedm" + "mcp__apifox__refresh_project_oas_ldmedm", + "Skill(update-config)", + "mcp__apidoc__get_project_overview", + "mcp__apidoc__search_endpoints", + "mcp__apidoc__list_endpoints", + "mcp__apidoc__get_endpoint_detail" ], "deny": [], "ask": [] }, + "enableAllProjectMcpServers": true, "enabledMcpjsonServers": [ "空港集团 - API 文档", - "apifox" + "apifox", + "aerologic-app", + "apidoc" ] } diff --git a/.mcp.json b/.mcp.json index 9df3a46..de11a57 100644 --- a/.mcp.json +++ b/.mcp.json @@ -10,6 +10,13 @@ "env": { "APIFOX_ACCESS_TOKEN": "APS-S2aVVwqasbdByzPLgSqryRC8BB0ZFqhQ" } + }, + "apidoc": { + "type": "http", + "url": "http://localhost:3001/mcp/c6a17835-6389-446c-8334-004b998835e5", + "headers": { + "Authorization": "Bearer afk_Snhv1JVACdbd91_NS699bb-2MN237Jww" + } } } } diff --git a/CLAUDE.md b/CLAUDE.md index 8106b8a..6bb9ef7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -958,6 +958,93 @@ companion object { --- +## 图片上传与展示规范 + +### 图片上传三字段规范 + +上传图片后提交表单时,**必须同时传 `pic`、`originalPic`、`picNumber` 三个字段**,缺一不可。 + +**`UploadUtil.upload()` 返回值**: +- `data?.newName` — 缩略图/压缩图文件名 +- `data?.zipFileName` — 原图文件名 + +**提交时字段映射**(参考事故签证 `AccidentVisaDetailsViewModel`): + +```kotlin +// FileBean 字段含义: +// - FileBean.url = newName(缩略图文件名) +// - FileBean.originalPic = zipFileName(原图文件名) + +// 上传新图片 +val data = UploadUtil.upload(fileBean.path).data +fileBean.url = data?.newName ?: "" +fileBean.originalPic = data?.zipFileName ?: "" + +// 提交时设置三个字段 +bean.picNumber = list.size.toString() +bean.pic = list.joinToString(",") { MediaUtil.removeUrl(it.url) } // 缩略图 +bean.originalPic = list.joinToString(",") { MediaUtil.removeUrl(it.originalPic) } // 原图 +``` + +**常见错误**: +- ❌ 只传 `images` 或 `originalPic` 单个字段 — 接口不认或数据不完整 +- ❌ 只取 `newName` 不取 `zipFileName` — 丢失原图路径 +- ❌ 用 `fileBean.path.startsWith("http")` 判断已上传 — 应该用 `fileBean.url.isNotEmpty()` + +### 编辑页加载已有图片 + +从详情接口获取图片后,需要同时解析 `pic`(缩略图)和 `originalPic`(原图),构建完整的 `FileBean`: + +```kotlin +val picList = bean.pic.split(",").filter { it.isNotEmpty() } +val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() } +val images = picList.mapIndexed { index, picUrl -> + val originalFile = originalPicList.getOrElse(index) { picUrl } + FileBean( + path = MediaUtil.fillUrl(picUrl), // 完整URL,用于显示 + url = picUrl, // 相对路径,提交时用 + originalPic = MediaUtil.fillUrl(originalFile) // 原图完整URL + ) +}.toMutableList() +``` + +### 图片加载必须带 Authorization Header + +`/file/getImg/` 接口需要鉴权,Glide 默认不带 token,直接用 `loadImage` BindingAdapter 会 **403 Forbidden**。 + +**正确做法** — 在 ViewHolder 中使用 `GlideUrl` + `LazyHeaders`: + +```kotlin +// 缩略图加载(ViewHolder 中) +val glideUrl = GlideUrl( + bean.path, + LazyHeaders.Builder() + .addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token)) + .build() +) +Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail) +``` + +**同时必须去掉 XML 布局中的 `loadImage` 属性**,否则 BindingAdapter 会触发不带 token 的请求覆盖手动加载: + +```xml + + + + + +``` + +**大图预览同理** — `PreviewImageViewHolder` 也需要用 `GlideUrl` 带 token 加载网络图片。 + +**参考文件**: +- 缩略图加载: `module_gjj/.../GjjManifestPicViewHolder.kt` +- 大图预览: `module_base/.../PreviewImageViewHolder.kt` +- 图片上传提交: `app/.../AccidentVisaDetailsViewModel.kt` +- 带 token 的 Glide 加载: `module_mit/.../PictureAdapter.kt` + +--- + ## 开发原则 - 资源引用必须存在 — 创建布局前确认 drawable/color/string 资源真实存在或主动创建 diff --git a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjjManifest.kt b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjjManifest.kt index 2121731..6ea086f 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/bean/GjjManifest.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/bean/GjjManifest.kt @@ -56,7 +56,10 @@ data class GjjManifest( var subCode: String = "", // 子代码 var unNumber: String = "", // 危险品编号 var activeId: Long = 0, // 活动ID - var locationTally: String = "" // 理货库位号 + var locationTally: String = "", // 理货库位号 + var pic: String = "", // 交接图片缩略图路径 + var originalPic: String = "", // 交接图片原图地址 + var picNumber: String = "" // 交接图片数量 ) : Serializable { // 分单列表 var haWbList: List? = null diff --git a/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt b/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt index 775ddf4..d95df45 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/http/net/Api.kt @@ -1367,6 +1367,12 @@ interface Api { @POST("flt/queryFlight") suspend fun queryFlightByDateAndNo(@Body data: RequestBody): BaseResultBean + /** + * 根据航班日期、航班号、地区类型、进出港查询航班(返回列表) + */ + @POST("flt/searchFlightList") + suspend fun searchFlightList(@Body data: RequestBody): BaseResultBean> + /** * 获取航班目的站、经停站 */ diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/page/preview/PreviewImageViewHolder.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/page/preview/PreviewImageViewHolder.kt index 1456c1d..c039697 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/ui/page/preview/PreviewImageViewHolder.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/page/preview/PreviewImageViewHolder.kt @@ -1,9 +1,14 @@ package com.lukouguoji.module_base.ui.page.preview 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.ItemPreviewImageBinding +import com.lukouguoji.module_base.db.perference.SharedPreferenceUtil /** * @author:孟凡华 @@ -14,8 +19,26 @@ class PreviewImageViewHolder(view: View) : BaseViewHolder(view) { override fun onBind(item: Any?, position: Int) { - binding.bean = getItemBean(item) + val bean = getItemBean(item) ?: return + binding.bean = bean + // 加载图片 + val path = bean.path + if (path.isNotEmpty()) { + if (path.startsWith("http")) { + // 网络图片带 Authorization header + val glideUrl = GlideUrl( + path, + LazyHeaders.Builder() + .addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token)) + .build() + ) + Glide.with(itemView.context).load(glideUrl).into(binding.photoView) + } else { + // 本地图片直接加载 + Glide.with(itemView.context).load(path).into(binding.photoView) + } + } } } \ No newline at end of file diff --git a/module_base/src/main/res/layout/item_preview_image.xml b/module_base/src/main/res/layout/item_preview_image.xml index 136d256..d53482e 100644 --- a/module_base/src/main/res/layout/item_preview_image.xml +++ b/module_base/src/main/res/layout/item_preview_image.xml @@ -13,7 +13,7 @@ android:layout_height="match_parent"> diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/GjjManifestListActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/GjjManifestListActivity.kt index 7b5c313..4087a17 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/GjjManifestListActivity.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/GjjManifestListActivity.kt @@ -13,6 +13,7 @@ import com.lukouguoji.module_base.base.CommonAdapter import com.lukouguoji.module_base.ktx.addOnItemClickListener import com.lukouguoji.module_base.router.ARouterConstants +@Deprecated("旧的实现") @Route(path = ARouterConstants.ACTIVITY_URL_GJJ_MANIFEST) class GjjManifestListActivity : BaseBindingActivity() { diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpManifestDetailsActivity.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpManifestDetailsActivity.kt index 14f3fd7..bb4e7e8 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpManifestDetailsActivity.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/activity/IntImpManifestDetailsActivity.kt @@ -3,14 +3,20 @@ package com.lukouguoji.gjj.activity import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.recyclerview.widget.LinearLayoutManager import com.alibaba.android.arouter.facade.annotation.Route import com.lukouguoji.gjj.R import com.lukouguoji.gjj.databinding.ActivityIntImpManifestDetailsBinding +import com.lukouguoji.gjj.holder.GjjManifestPicViewHolder import com.lukouguoji.gjj.viewModel.IntImpManifestDetailsViewModel import com.lukouguoji.module_base.base.BaseBindingActivity +import com.lukouguoji.module_base.base.CommonAdapter +import com.lukouguoji.module_base.bean.FileBean import com.lukouguoji.module_base.bean.GjjManifest import com.lukouguoji.module_base.common.Constant import com.lukouguoji.module_base.router.ARouterConstants +import com.lukouguoji.module_base.util.MediaUtil +import com.lukouguoji.module_base.ktx.noNull /** * 国际进港舱单详情 @@ -26,6 +32,29 @@ class IntImpManifestDetailsActivity : setBackArrow("进港舱单详情") binding.viewModel = viewModel viewModel.initOnCreated(intent) + + // 交接图片 + val picAdapter = CommonAdapter( + this, + R.layout.item_gjj_manifest_pic, + GjjManifestPicViewHolder::class.java + ) + binding.rvPic.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + binding.rvPic.adapter = picAdapter + + viewModel.dataBean.observe(this) { bean -> + val picList = bean.pic.noNull().split(",").filter { it.isNotEmpty() } + val originalPicList = bean.originalPic.noNull().split(",").filter { it.isNotEmpty() } + val list = picList.mapIndexed { index, picUrl -> + val originalFile = originalPicList.getOrElse(index) { picUrl } + FileBean( + path = MediaUtil.fillUrl(picUrl), + url = picUrl, + originalPic = MediaUtil.fillUrl(originalFile) + ) + } + picAdapter.refresh(list) + } } companion object { diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/GjjManifestPicViewHolder.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/GjjManifestPicViewHolder.kt index 81c505d..d17c659 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/holder/GjjManifestPicViewHolder.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/holder/GjjManifestPicViewHolder.kt @@ -1,9 +1,14 @@ package com.lukouguoji.gjj.holder 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.gjj.databinding.ItemGjjManifestPicBinding 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.db.perference.SharedPreferenceUtil import com.lukouguoji.module_base.ktx.commonAdapter import com.lukouguoji.module_base.ui.page.preview.PreviewActivity @@ -14,6 +19,17 @@ class GjjManifestPicViewHolder(view: View) : val bean = getItemBean(item)!! binding.bean = bean + // 带 Authorization header 加载图片 + if (bean.path.isNotEmpty()) { + val glideUrl = GlideUrl( + bean.path, + LazyHeaders.Builder() + .addHeader("Authorization", SharedPreferenceUtil.getString(Constant.Share.token)) + .build() + ) + Glide.with(itemView.context).load(glideUrl).into(binding.ivThumbnail) + } + binding.ivThumbnail.setOnClickListener { val items = getRecyclerView()?.commonAdapter()?.items ?.filterIsInstance() ?: listOf(bean) diff --git a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpManifestViewModel.kt b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpManifestViewModel.kt index a5f4ae0..c4060cb 100644 --- a/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpManifestViewModel.kt +++ b/module_gjj/src/main/java/com/lukouguoji/gjj/viewModel/IntImpManifestViewModel.kt @@ -64,30 +64,40 @@ class IntImpManifestViewModel : BasePageViewModel() { lastQueriedFlight = key launchCollect({ - NetApply.api.getGjFlightBean( + NetApply.api.searchFlightList( mapOf( "fdate" to fdate, "fno" to fno, - "ieFlag" to "I", + "status" to "1", ).toRequestBody() ) }) { onSuccess = { - if (it.verifySuccess() && it.data != null) { - val flight = it.data!! - fid = flight.fid.noNull() - fdep = flight.fdep.noNull() - fdest.value = flight.fdest.noNull() + if (it.verifySuccess() && !it.data.isNullOrEmpty()) { + val dataList = it.data!! + if (dataList.size > 1) { + showToast("存在多个航班记录,请核实") + fid = "" + fdep = "" + fdest.value = "" + sendAddressList.value = emptyList() + sendAddress.value = "" + } else { + val flight = dataList[0] + fid = flight.fid.noNull() + fdep = flight.fdep.noNull() + fdest.value = flight.fdest.noNull() - // 构建始发站下拉列表:fdep + jtz(经停港) - val list = mutableListOf( - KeyValue(flight.fdep.noNull(), flight.fdep.noNull()), - ) - if (!flight.jtz.isNullOrEmpty()) { - list.add(KeyValue(flight.jtz.noNull(), flight.jtz.noNull())) + // 构建始发站下拉列表:fdep + jtz(经停港) + val list = mutableListOf( + KeyValue(flight.fdep.noNull(), flight.fdep.noNull()), + ) + if (!flight.jtz.isNullOrEmpty()) { + list.add(KeyValue(flight.jtz.noNull(), flight.jtz.noNull())) + } + sendAddressList.value = list + sendAddress.value = flight.fdep.noNull() } - sendAddressList.value = list - sendAddress.value = flight.fdep.noNull() } else { fid = "" fdep = "" diff --git a/module_gjj/src/main/res/layout/activity_int_imp_manifest_details.xml b/module_gjj/src/main/res/layout/activity_int_imp_manifest_details.xml index 0a28dfd..73fb613 100644 --- a/module_gjj/src/main/res/layout/activity_int_imp_manifest_details.xml +++ b/module_gjj/src/main/res/layout/activity_int_imp_manifest_details.xml @@ -263,25 +263,14 @@ android:textColor="@color/text_gray" android:textSize="14sp" /> - - - - - - + android:orientation="horizontal" /> diff --git a/module_gjj/src/main/res/layout/item_gjj_manifest_pic.xml b/module_gjj/src/main/res/layout/item_gjj_manifest_pic.xml index 0f2ee44..cf9f354 100644 --- a/module_gjj/src/main/res/layout/item_gjj_manifest_pic.xml +++ b/module_gjj/src/main/res/layout/item_gjj_manifest_pic.xml @@ -11,7 +11,6 @@ - FileBean(path = url) + // 处理图片列表:pic=缩略图(newName),originalPic=原图(zipFileName) + val picList = bean.pic.split(",").filter { it.isNotEmpty() } + val originalPicList = bean.originalPic.split(",").filter { it.isNotEmpty() } + val images = picList.mapIndexed { index, picUrl -> + val fb = FileBean( + path = MediaUtil.fillUrl(picUrl), + url = picUrl + ) + if (index < originalPicList.size) { + fb.originalPic = MediaUtil.fillUrl(originalPicList[index]) + } + fb }.toMutableList() // 如果是编辑模式,添加一个空的FileBean用于添加新图片 @@ -108,38 +118,24 @@ class GnjYiKuEditViewModel : BaseViewModel(), IOnItemClickListener { launchLoadingCollect({ // 1. 上传图片 - val uploadedUrls = mutableListOf() images.forEach { fileBean -> - // 判断是否为已上传的图片(在线URL) - if (fileBean.path.startsWith("http")) { - uploadedUrls.add(fileBean.path) + if (fileBean.url.isNotEmpty()) { + // 已上传的图片,保持原有的 url 和 originalPic } else { - // 本地图片需要上传 - val result = UploadUtil.upload(fileBean.path) - if (result.verifySuccess()) { - uploadedUrls.add(result.data?.newName ?: "") - } + // 本地新图片需要上传 + val data = UploadUtil.upload(fileBean.path).data + fileBean.url = data?.newName ?: "" + fileBean.originalPic = data?.zipFileName ?: "" } } - // 2. 提交表单数据 - val params = mapOf( - "id" to id, - "wbNo" to bean.wbNo, - "pc" to bean.pc, - "weight" to bean.weight, - "spCode" to bean.spCode, - "agentCode" to bean.agentCode, - "goods" to bean.goods, - "flight" to bean.flight, - "route" to bean.route, - "awbType" to bean.awbType, - "telegramNo" to bean.telegramNo, - "remark" to bean.remark, - "images" to uploadedUrls.joinToString(","), - ).toRequestBody(removeEmptyOrNull = true) + // 2. 设置图片字段 + bean.picNumber = images.size.toString() + bean.pic = images.joinToString(",") { MediaUtil.removeUrl(it.url) } + bean.originalPic = images.joinToString(",") { MediaUtil.removeUrl(it.originalPic) } - NetApply.api.saveGnjYiKu(params) + // 3. 提交表单数据 + NetApply.api.saveGnjYiKu(bean.toRequestBody()) }) { onSuccess = { showToast(if (pageType.value == DetailsPageType.Add) "新增成功" else "保存成功") 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 b6f675e..fde767c 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 @@ -100,16 +100,14 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener { launchLoadingCollect({ // 上传图片 - val uploadedUrls = mutableListOf() images.forEach { fileBean -> if (fileBean.url.isNotEmpty()) { - // 已上传的图片,直接用文件名 - uploadedUrls.add(fileBean.url) + // 已上传的图片,保持原有的 url 和 originalPic } else { - val result = UploadUtil.upload(fileBean.path) - if (result.verifySuccess()) { - uploadedUrls.add(result.data?.newName ?: "") - } + // 本地新图片需要上传 + val data = UploadUtil.upload(fileBean.path).data + fileBean.url = data?.newName ?: "" + fileBean.originalPic = data?.zipFileName ?: "" } } @@ -117,7 +115,9 @@ class GnjYiKuHandoverViewModel : BaseViewModel(), IOnItemClickListener { val params = mapOf( "mawbId" to mawbId, "remark" to bean.remark, - "originalPic" to uploadedUrls.joinToString(","), + "picNumber" to images.size.toString(), + "pic" to images.joinToString(",") { MediaUtil.removeUrl(it.url) }, + "originalPic" to images.joinToString(",") { MediaUtil.removeUrl(it.originalPic) }, ).toRequestBody(removeEmptyOrNull = true) NetApply.api.modifyGnjMoveStash(params)