# 安卓端调试成功全过程问题总结与修复指南 > **文档类型**:经验沉淀文档(非简单周报) > **维护人**:黄镇堡 > **最后更新**:2026-04-20 > **项目**:智慧医养 mall(uni-app x) > **分支**:`huangzhenbao-admin` > **AppID**:`__UNI__81482FF` --- ## 目录 1. [文档说明](#1-文档说明) 2. [时间范围与排查范围](#2-时间范围与排查范围) 3. [总体结论](#3-总体结论) 4. [生成本地打包APP资源阶段——UTS 语法问题](#4-生成本地打包app资源阶段uts-语法问题) 5. [Run 到 Android 端时的报错与修复全过程](#5-run-到-android-端时的报错与修复全过程) 6. [JS / UTS / Kotlin 语义差异总结](#6-js--uts--kotlin-语义差异总结) 7. [Android 适配与页面运行问题总结](#7-android-适配与页面运行问题总结) 8. [额外发现的重要隐藏问题](#8-额外发现的重要隐藏问题) 9. [面向后续开发的编码规范清单](#9-面向后续开发的编码规范清单) 10. [面向后续排查的标准流程清单](#10-面向后续排查的标准流程清单) 11. [关键文件与关键改动索引](#11-关键文件与关键改动索引) 12. [结语](#12-结语) --- ## 1. 文档说明 ### 1.1 目的 本文档记录了将 uni-app x 商家端项目从"能在小程序/H5运行"推进到"能在 Android 真机正常调试运行"的完整过程。 内容覆盖三个层次: - **编译层**:HBuilderX 的 UTS → Kotlin 编译阶段报错 - **构建层**:Android Studio `assembleDebug` 阶段 Kotlin 符号缺失报错 - **运行层**:APK 安装后页面布局异常、逻辑错误、跳转失败 ### 1.2 阅读建议 | 角色 | 建议阅读章节 | | ------------------------------ | ------------ | | 新接手本项目的开发者 | 全部 | | 遇到 UTS 编译报错 | §4 + §6 | | 遇到 Android Studio 构建失败 | §5 | | 遇到 Android 页面布局/显示问题 | §7 | | 快速查规范 | §9 + §10 | ### 1.3 项目技术栈 | 层次 | 技术 | | ---------------- | ---------------------------------------- | | 前端框架 | uni-app x(HBuilderX 5.07) | | 前端语言 | UTS(TypeScript 子集,编译为 Kotlin) | | 后端 | Supabase(PostgreSQL + Edge Functions) | | Android 构建 | Android Studio Ladybug + `assembleDebug` | | Android 最低版本 | API 21(Android 5.0) | --- ## 2. 时间范围与排查范围 ### 2.1 时间范围 - **开始**:2026-04-13(开始尝试 Android 本地打包) - **完成**:2026-04-20(merchant 端能够在 Android 上正常运行) ### 2.2 关键提交节点 | 提交 Hash | 提交信息 | 意义 | | ----------------------- | ------------------------------ | ----------------------------------------------- | | `5ab4789d` | 保存能够打包成功的版本 | **基线**:首次编译通过,但 Android 运行仍有问题 | | `8765e948` | 小程序条件编译 | 分离小程序/Android 条件编译 | | `5473f0fc` / `3c256812` | 修复页面展示bug | 布局适配修复 | | `f99bd5a7` | merchant能够在安卓上面进行运行 | **终点**:Android 端完整可用 | ### 2.3 排查范围 - **页面**:所有 `pages/mall/merchant/**`(共约 30 个页面)+ `pages/user/login` - **公共组件**:`components/merchant-tabbar/`、`utils/eventHelpers.uts`(新增) - **配置文件**:`pages.json`、`manifest.json`、`fix_kt_files.ps1` - **Android 项目**:`D:\Android\Project\mall\app\src\main\java\` ### 2.4 主要错误文件索引 | 文件 | 内容 | | -------------------------------------- | --------------------------------------------------- | | `errors_round1_clean.txt` | A阶段第一轮 UTS 编译错误(清洗版) | | `errors_round2.txt` | A阶段第二轮 UTS 编译错误 | | `D:\Android\Project\mall\报错信息.txt` | B阶段 Android Studio Kotlin 符号缺失错误 | | `kotlinc_errors_real.txt` | 独立 kotlinc 编译尝试的过滤结果(已确认无参考价值) | | `build_output_r1_android.txt` | Android Studio assembleDebug 完整输出 | --- ## 3. 总体结论 ### 3.1 根本性结论 **uni-app x 的 UTS 并不是 JavaScript,是一门独立的强类型语言,编译到 Kotlin 后遵循 Kotlin 的严格类型规则。** 大量问题来源于以 JS 习惯写 UTS: - JS 中合法的表达式(`if (str)`、`a === b`、`obj.field`)在 UTS/Kotlin 中要么报编译错误,要么产生意料之外的运行时行为。 - CSS 在 Web 浏览器中自动降级处理,在 Android 原生渲染器中不行,无效属性直接被忽略或崩溃。 ### 3.2 三阶段问题分布 ``` ┌────────────────────────────────────────────────────────────────────────┐ │ 阶段A:HBuilderX "生成本地打包APP资源" │ │ 工具:HBuilderX 5.07 UTS编译器 │ │ 产物:.kt 文件到 unpackage/dist/build/app-android/ │ │ 问题:UTS 语法错误(13类),编译直接失败 │ ├────────────────────────────────────────────────────────────────────────┤ │ 阶段B:Android Studio assembleDebug │ │ 工具:Gradle + Kotlin 编译器 │ │ 产物:app-debug.apk │ │ 问题:Kotlin 符号 unresolved(uni SDK AAR缺失)、=== 运行时语义错误 │ ├────────────────────────────────────────────────────────────────────────┤ │ 阶段C:APK 安装到真机/模拟器运行 │ │ 工具:Android 真机 │ │ 问题:布局截断、顶部遮挡、双层TabBar、跳转白屏、无法滚动 │ └────────────────────────────────────────────────────────────────────────┘ ``` ### 3.3 问题数量统计 | 阶段 | 问题分类数 | 已解决 | 备注 | | ---------------- | ---------- | -------- | -------------------------------------------------------- | | A(UTS编译) | 13类 | 13类 | | | B(Android构建) | 11类 | 9类 | AAR依赖需要补充;getPushClientId待重写 | | C(运行适配) | 14类 | 14类 | 含flex-direction、scroll-into-view ID、safe-area双重兜底 | | 隐藏问题 | 6类 | 6类 | 含AkReq链路模式、sessionUser访问模式 | | **合计** | **44类** | **42类** | | --- ## 4. 生成本地打包APP资源阶段——UTS 语法问题 > **触发场景**:在 HBuilderX 中点击"发行 → 本地打包 → 生成本地打包App资源",UTS 编译器报错,`.kt` 文件无法生成。 > > **错误来源文件**:`errors_round1_clean.txt`、`errors_round2.txt` --- ### 4.1 非严格布尔条件(最常见) **错误信息** ``` condition type mismatch: String but Boolean expected condition type mismatch: Int? but Boolean expected ``` **根因** JavaScript 有 truthy/falsy 机制(空字符串、0、null 均为 falsy),UTS/Kotlin 的 `if` 条件**必须是 Boolean 类型**,不接受其他类型隐式转换。 **错误写法 → 修复写法** ```typescript // ❌ JS习惯写法(UTS报错) if (this.merchantId) { ... } v-if="notice.tag" if (item.url) { ... } // ✅ 修复后 if (this.merchantId !== '') { ... } v-if="notice.tag !== ''" if (item.url.length > 0) { ... } // ❌ 可空类型 if (result.data) { ... } // result.data: string | null // ✅ 修复后 if (result.data != null && result.data !== '') { ... } ``` **影响文件**:`profile.uvue`、`index.uvue`、`messages.uvue`、`finance.uvue` 等大量文件。 --- ### 4.2 onLoad 参数字段访问 **错误信息** ``` unresolved reference 'type' unresolved reference 'orderId' ``` **根因** `onLoad(options: any)` 中的 `options` 在 Android UTS 运行时是 `UTSJSONObject`,不能用点访问(`.field`)读取属性;`as any` 掩盖了类型问题,运行时返回 null。 ```typescript // ❌ 错误写法 onLoad(options: any) { const type = options.type // unresolved reference const id = options?.orderId as string } // ✅ 修复写法 onLoad(options: UTSJSONObject | null) { if (options != null) { const type = options.getString('type') ?? '' const id = options.getString('orderId') ?? '' } } ``` --- ### 4.3 `any[]` 泛型不兼容 **错误信息** ``` type mismatch: inferred type is Array but Array was expected ``` **根因** UTS 不支持 `any[]`,Android 端数组必须有明确的元素类型。 ```typescript // ❌ 错误写法 const rawData = response.data as any[]; const item = rawData[0] as UTSJSONObject; // ✅ 修复写法 const rawData = response.data as Array; const item = rawData[0]; // 已知类型,不需要二次 as ``` --- ### 4.4 UTSJSONObject 点访问 **错误信息** ``` unresolved reference 'name' unresolved reference 'total_sales' ``` **根因** `UTSJSONObject` 不是普通对象,字段不能用点访问,必须用方法访问。 ```typescript // ❌ 错误写法 const name = shopInfo.name; const count = shopInfo.total; // ✅ 修复写法(按值类型选方法) const name = shopInfo.getString("name") ?? ""; const count = shopInfo.getNumber("total") ?? 0; const flag = shopInfo.getBoolean("is_active") ?? false; const sub = shopInfo.getJSON("address"); // 嵌套对象 const list = shopInfo.getArray("items"); // 嵌套数组 ``` --- ### 4.5 request/uploadFile 回调类型不匹配 **错误信息** ``` argument type mismatch: AkRequestSuccessResult is not assignable to RequestSuccess return type mismatch: AkUploadFileSuccessResult vs UniUploadFileSuccess ``` **根因** 自定义请求封装 `AkReq` 的回调类型(`AkRequestSuccessResult`)与 uni-app x 的标准类型(`RequestSuccess`)不兼容,UTS 强类型检查不允许隐式兼容。 **修复**:对所有使用 `AkReq` 回调的地方,显式声明回调参数类型为项目自定义类型,或用中间变量转换。 --- ### 4.6 内联箭头函数事件处理器 **错误信息** ``` only expressions are allowed here ``` **根因** UTS 模板中的 `@click` 不支持内联多行逻辑,只能调用命名方法。 ```html ``` ```typescript // script 中 handleClick() { this.doA() this.doB() } ``` --- ### 4.7 picker 事件类型错误 **错误信息** ``` UniPickerChangeEvent does not exist argument type mismatch: UniInputEvent vs expected ``` **根因** `` 的 `@change` 事件在 uni-app x Android 端使用 `UniInputEvent`(或通用事件接口),`UniPickerChangeEvent` 不存在。 **修复**:提取 `utils/eventHelpers.uts` 工具函数统一处理: ```typescript // utils/eventHelpers.uts export function getPickerIdx(e: UniInputEvent): number { return parseInt(e.detail.value as string) ?? 0; } export function getInputVal(e: UniInputEvent): string { return (e.detail.value as string) ?? ""; } ``` 模板使用: ```html ``` --- ### 4.8 对象字面量未声明命名类型 **错误信息** ``` smart cast to 'String' is impossible unresolved reference 'link' ``` **根因** UTS 的对象字面量需要有已声明的 `type` 类型定义,否则编译器无法推断字段名,smart cast 失败。 ```typescript // ❌ 错误写法 const item = { link: '/page', title: '标题' } if (item.link) { ... } // unresolved reference 'link' // ✅ 修复写法 type NavItem = { link: string; title: string } const item: NavItem = { link: '/page', title: '标题' } if (item.link !== '') { ... } ``` --- ### 4.9 `String?` 赋值给 `String` **错误信息** ``` assignment type mismatch: String? cannot be assigned to String ``` **根因** `UTSJSONObject.getString()` 返回 `String?`(可空),不能直接赋给非空 `String` 变量。 ```typescript // ❌ 错误写法 const name: string = obj.getString("name"); // String? → String 报错 // ✅ 修复写法 const name: string = obj.getString("name") ?? ""; ``` --- ### 4.10 `Record` 类型使用 **错误信息** ``` unresolved reference 'Record' type argument is not within its bounds ``` **根因** UTS Android 端不支持 `Record` 泛型,Kotlin 没有对应的直接映射。 ```typescript // ❌ 错误写法 const map: Record = {}; map["key"] = 1; // ✅ 修复写法(用 if-else 替代或用 UTSJSONObject) const map = {} as UTSJSONObject; // 或者直接用具体 type 替代 Record ``` --- ### 4.11 函数参数变量名不一致 **错误信息** ``` unresolved reference 'link' unresolved reference 'item' ``` **根因** 事件处理函数的形参名与函数体内使用的变量名不一致(如声明时叫 `item`,调用时传的是 `tip.link`)。 **修复**:参见 §7 问题9——将事件处理器改为只接收基础类型参数,在模板层完成字段提取: ```html viewDetail(link: string) { if (link.length > 0) { uni.navigateTo({ url: link }) } } ``` --- ### 4.12 可空接收者运算符调用 **错误信息** ``` operator call is prohibited on nullable receiver: String? ``` **根因** `String?` 类型不能直接进行字符串运算,需要先用 `?? ''` 确保非空。 ```typescript // ❌ 错误写法 const total = price * count; // price: number | null // ✅ 修复写法 const total = (price ?? 0) * count; ``` --- ### 4.13 多级可选链类型推断失败 **错误信息** ``` Cannot infer type for this parameter. Please specify it explicitly. Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver ``` **根因** UTS/Kotlin 不支持像 JS 那样无限级联的可选链(`a?.b?.c?.d`),多级嵌套时编译器无法推断中间类型。 ```typescript // ❌ 错误写法 const val = response?.data?.items?.[0]?.name; // ✅ 修复写法(分步判空) if (response != null && response.data != null) { const items = response.data as Array; if (items.length > 0) { const name = items[0].getString("name") ?? ""; } } ``` --- ### 4.14 UTS 编译阶段问题汇总表 | # | 错误类型 | 典型报错 | 核心修复 | | --- | -------------------- | ------------------------------------------ | ---------------------------- | | 1 | 非布尔条件 | `condition type mismatch: String` | `!== ''` / `!= null` | | 2 | onLoad参数点访问 | `unresolved reference 'type'` | `UTSJSONObject.getString()` | | 3 | `any[]` 泛型 | `Array vs Array` | 明确泛型类型 | | 4 | UTSJSONObject点访问 | `unresolved reference 'name'` | `.getString()` 等方法 | | 5 | 回调类型不匹配 | `AkRequestSuccessResult vs RequestSuccess` | 显式类型声明 | | 6 | 内联箭头函数 | `only expressions are allowed` | 抽取命名方法 | | 7 | picker事件类型 | `UniPickerChangeEvent does not exist` | `eventHelpers.uts` | | 8 | 未声明类型对象 | `smart cast impossible` | 声明 `type` + `as TypeName` | | 9 | `String?` → `String` | `assignment type mismatch` | `?? ''` 兜底 | | 10 | `Record` | `unresolved reference 'Record'` | 改用具体类型或 UTSJSONObject | | 11 | 参数名不一致 | `unresolved reference 'link'` | 模板层提取基础类型参数 | | 12 | 可空运算符 | `prohibited on nullable receiver` | `?? 0` 后运算 | | 13 | 多级可选链 | `Cannot infer type` | 分步判空 | --- ## 5. Run 到 Android 端时的报错与修复全过程 > **触发场景**:UTS 编译通过,生成 `.kt` 文件后,复制到 Android Studio 项目执行 `assembleDebug`,出现 Kotlin 编译错误;或 APK 安装后逻辑行为异常。 > > **错误来源文件**:`D:\Android\Project\mall\报错信息.txt`、`build_output_r1_android.txt` --- ### 5.1 问题链路概览 ``` HBuilderX UTS编译 → .kt文件生成 ↓ 复制到 D:\Android\Project\mall\app\src\main\java\ ↓ 运行 fix_kt_files.ps1(必须!) ↓ Android Studio assembleDebug ↓ adb install / Run ``` --- ### 5.2 问题1:`main.uts` / `App.uvue` 语法错误导致整个编译中止 **现象**:HBuilderX 在处理入口文件时报错,后续所有 `.kt` 文件无法生成。 **根因**:`main.uts` 中有 UTS 不支持的写法,或 `App.uvue` 的 ``,错误表明编译器在读到 `` 时还没有找到对应的 `` 关闭标签。 ### 根因 在添加"删除等级确认弹窗"时,多文件替换操作的 `oldString` 匹配了包含 `\n\t\n` 的区块,但生成的 `newString` 重新排布后,模板的 `` 结构被混入了 ``、`` 这类文档级边界标签的替换操作,**oldString 不应包含这些边界标签**;应该只替换边界内部的内容,让边界标签保持原位不动。批量替换后,必须用 `Select-String -Pattern "template|/template|script|/script"` 验证三段式结构完整性。 --- ## 第二轮通用修复规律总结 ### 规律一:`uni.showModal` / `uni.showActionSheet` 回调中不能用 Promise 链 | ❌ 旧模式 | ✅ 新模式 | | -------------------------------------------- | ----------------------------------------- | | `success: (res) => { asyncFn().then(...) }` | 响应式状态弹窗 + `async method` + `await` | | `void this._doSomething()` 在 void lambda 中 | 在组件的 `async method` 中直接 `await` | **影响页面**:settings.uvue(退出)、members.uvue(删除等级)、任何需要在系统弹窗确认后执行异步操作的页面。 ### 规律二:安卓端三大滚动/布局模式 | 场景 | 正确模式 | | ---------------- | ----------------------------------------------------------------------------------- | | 纯列表页 | 根 `height:100% flex-column overflow:hidden` + scroll-view `flex:1 overflow:hidden` | | 含底部操作栏 | 同上 + 操作栏 `flex-shrink:0`(不用 position:fixed) | | scroll-view 方向 | `direction="vertical"` 不是 `scroll-y` | ### 规律三:退出登录必须三清 1. **清 Supabase 内存 session**:`await supa.signOut()` 2. **清所有 storage key**:token × 3 + user_id + 商家相关 key × 4 3. **清 store 内存态**:`setIsLoggedIn(false)` + `setUserProfile({...})` 缺任何一步,App 启动时的恢复逻辑都可能把用户重新送回商家端。 ### 规律四:onShow 中刷新列表必须先重置分页 ```typescript onShow() { this.page = 1 // 必须 this.hasMore = true // 必须 this.list = [] // 必须 this.loadData() } ``` ### 规律五:多处批量替换后的必检项 ```powershell # 检查三段式结构完整性 Select-String -Path "xxx.uvue" -Pattern "^|^|^" # 检查关键 import 是否存在 Select-String -Path "xxx.uvue" -Pattern "import supa|type MemberLevel" ``` --- ## 第二轮未解决问题(需后续跟进) | 序号 | 问题 | 当前状态 | 建议 | | ---- | -------------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- | | P-01 | 消息中心/聊天功能结构未完善 | 页面存在但功能简陋,未实现会话列表/未读标记/订单关联 | 参考正规电商商家端重新设计消息架构 | | P-02 | 扫码功能未实现 | `scan.uvue` 页面存在但功能为占位符 | 接入 `uni.scanCode` API | | P-03 | 会员客户列表加载时出现"暂无客户"再显示数据(loading/empty 状态混淆) | 已知但未在本轮修复 | 添加 `isLoading` 状态,loading 中不显示 empty 状态 | | P-04 | uni-app SDK AAR 依赖缺失(延续自第一轮 §5.7) | 涉及 `uni_showModal` 等 API 的 Kotlin 符号缺失 | 从 HBuilderX 本地资源包补充 AAR 到 app/libs/ | | P-05 | getPushClientId 推送 CID 注册未实现(延续自第一轮 §5.12) | 待用正确的 UTS 签名重新实现 | 参考 §5.12 中的修复模板 | --- _第二轮记录更新:2026-04-21_ --- # 第三轮消费者端安卓适配补充记录(2026-05-13) > **本轮时间**:2026-05-13 > **调试阶段**:消费者端主路径页面安卓真机适配补充 > **背景**:前两轮主要覆盖商家端编译、构建与运行问题;本轮补充处理消费者端首页主链路中的列表滚动与视觉不一致问题,重点是 `购物车`、`我的`、`我的订单` 三个页面。 --- ## 第三轮问题总览 | 序号 | 问题名称 | 页面/模块 | 问题类型 | 是否已解决 | 备注 | | ----- | ---------------------------------- | --------------- | -------- | ---------- | --------------------------------------------------------- | | R3-01 | 购物车页面无法上下滑动 | consumer/cart | 安卓布局 | ✅ 已解决 | `100vh + overflow:hidden + scroll-view height:0` 组合失效 | | R3-02 | 我的页面无法上下滑动 | consumer/my | 安卓布局 | ✅ 已解决 | 与购物车同源,scroll-view 未拿到明确高度 | | R3-03 | 我的订单页面无法上下滑动 | consumer/orders | 安卓布局 | ✅ 已解决 | 已计算 viewportHeight,但未绑定到滚动容器 | | R3-04 | 我的页面安卓配色与小程序结果不一致 | consumer/my | 安卓视觉 | ✅ 已解决 | 渐变与叠层卡片在 Android 原生渲染器中回退异常 | --- ## R3-01/R3-02/R3-03:消费者端主列表页面无法上下滑动(根容器高度链断裂) ### 现象 - `pages/main/cart.uvue`:购物车商品列表和推荐商品区域在安卓端无法下滑 - `pages/main/profile.uvue`:我的页面钱包、订单、猜你喜欢区域无法下滑 - `pages/mall/consumer/orders.uvue`:我的订单列表只能显示首屏内容,后续订单无法通过手势下滑查看 ### 出现位置 - `d:\骅锋\mall\pages\main\cart.uvue` - `d:\骅锋\mall\pages\main\profile.uvue` - `d:\骅锋\mall\pages\mall\consumer\orders.uvue` ### 问题类型 安卓端布局 / scroll-view 高度计算 ### 根因分析 三个页面的结构本质一致: ```css /* ❌ 旧写法的共同模式 */ .page { height: 100vh; min-height: 100vh; display: flex; flex-direction: column; overflow: hidden; } .content-scroll { flex: 1; height: 0; } ``` 在小程序 / H5 中,这类写法通常仍能滚动,因为浏览器会把 `flex: 1` 和 `height: 0` 结合成可收缩的滚动区域;但在 uni-app x 的 Android 原生渲染链路中,`scroll-view` 必须拿到**明确的像素高度**。一旦父容器只给出 `100vh`、内部再靠 `height: 0` 收缩,原生布局引擎就可能把滚动区计算成非 scrollable 容器,最终表现为: - 页面内容正常渲染 - 可见区域能显示首屏内容 - 手指上下滑动无响应 `orders.uvue` 的情况更隐蔽:源码里已经计算了 `viewportHeight`,但该值仅用于虚拟列表可视区估算,没有实际绑定到 `scroll-view` 容器,所以安卓端仍然拿不到真实可滚动高度。 ### 修复方式 修复原则统一为:**继续保留原有布局结构,但只在 Android 端为页面根容器和 scroll-view 注入明确像素高度**,避免影响小程序现有表现。 #### 购物车 / 我的页面修复模式 ```typescript const pageWindowHeight = systemInfo.windowHeight ?? systemInfo.screenHeight ?? 0; // #ifdef APP-ANDROID isAndroidApp.value = true; // #endif const pageStyle = computed((): string => { if (!isAndroidApp.value) return ""; if (pageWindowHeight.value <= 0) return ""; return ( "height:" + pageWindowHeight.value + "px;min-height:" + pageWindowHeight.value + "px;" ); }); const contentStyle = computed((): string => { if (!isAndroidApp.value) return ""; const contentHeight = pageWindowHeight.value - fixedHeaderHeight; return "height:" + contentHeight + "px;min-height:" + contentHeight + "px;"; }); ``` 模板中把样式显式绑定到页面容器和 `scroll-view`: ```html ``` #### 我的订单页面修复模式 `orders.uvue` 已有 `viewportHeight`,所以只需让这个值真正进入滚动容器: ```typescript pageWindowHeight.value = systemInfo.windowHeight ?? systemInfo.screenHeight ?? 0; const windowHeight = pageWindowHeight.value; const calculatedViewport = windowHeight - statusBarHeight.value - 44 - 48; viewportHeight.value = calculatedViewport > 0 ? calculatedViewport : windowHeight; // #ifdef APP-ANDROID isAndroidApp.value = true; // #endif ``` ```typescript const ordersContentStyle = computed((): string => { if (!isAndroidApp.value) return ""; if (viewportHeight.value <= 0) return ""; return ( "height:" + viewportHeight.value + "px;min-height:" + viewportHeight.value + "px;" ); }); ``` ### 涉及文件 - `d:\骅锋\mall\pages\main\cart.uvue` - `d:\骅锋\mall\pages\main\profile.uvue` - `d:\骅锋\mall\pages\mall\consumer\orders.uvue` ### 经验总结 > **规律**:消费者端主列表页在 Android 上如果出现“首屏能显示、但完全不能滚动”,优先检查是否存在 `height: 100vh + overflow: hidden + scroll-view height: 0/flex:1` 组合。对 uni-app x Android 来说,scroll-view 最稳妥的做法不是依赖 Web 风格的收缩布局,而是给出明确的像素高度。 > > **规范**:有固定头部、筛选条、底部结算栏的页面,Android 端要么使用完整的 `height: 100%` 高度链,要么直接通过 `windowHeight - 固定区域高度` 计算 scroll-view 像素高度;不要只把高度算在 JS 里而不绑定到模板。 --- ## R3-04:我的页面安卓端配色与小程序不一致(渐变与叠层卡片回退异常) ### 现象 `pages/main/profile.uvue` 在微信小程序中顶部是明显的红色渐变,会员条是深棕金色;但在安卓真机中,顶部和钱包会员区出现发白、发灰、层次减弱的问题,视觉结果与小程序明显不一致。 ### 出现位置 `d:\骅锋\mall\pages\main\profile.uvue` ### 问题类型 安卓端视觉兼容 / CSS 渐变回退 ### 根因分析 问题不在数据,而在样式实现方式:页面头部和会员条使用了复杂叠层视觉,核心依赖如下写法: ```css /* ❌ 旧写法 */ .jd-profile-top { background: linear-gradient(180deg, #ff3b30 0%, #ff5a36 60%, #f5f5f5 100%); } .wallet-plus-ribbon { background: linear-gradient(90deg, #5a3a21 0%, #75542f 52%, #a68049 100%); } ``` 在小程序端,这类渐变简写可以正常渲染;但在 Android 原生渲染器中,复杂渐变、卡片叠层和浅色蒙版叠加后,容易出现: - 渐变层未按预期渲染 - 背景色回退到默认浅色 - 文本和图标看起来像“发白”“褪色” 这类问题不会报编译错误,属于纯渲染兼容差异。 ### 修复方式 处理原则是:**保留小程序端原视觉,同时为 Android 增加稳定的配色回退类**。 #### 步骤一:把单一 `background` 拆成 `background-color + background-image` ```css .jd-profile-top { background-color: #ff5a36; background-image: linear-gradient( 180deg, #ff3b30 0%, #ff5a36 60%, #f5f5f5 100% ); } .wallet-plus-ribbon { background-color: #8a6638; background-image: linear-gradient( 90deg, #5a3a21 0%, #75542f 52%, #a68049 100% ); } ``` #### 步骤二:只在 Android 端切换到稳定回退样式 ```html ``` ```css .jd-profile-top-android { background-image: none; background-color: #ff5a36; } .wallet-plus-ribbon-android { background-image: none; background-color: #8a6638; } ``` 这样做的结果是: - 小程序端继续保留原来的渐变视觉 - Android 端使用稳定纯色回退,不再出现大面积发白发灰 - 端间通过条件类隔离,不互相影响 ### 涉及文件 `d:\骅锋\mall\pages\main\profile.uvue` ### 经验总结 > **规律**:uni-app x Android 对复杂 `linear-gradient`、多层卡片叠色和透明蒙版的支持不如小程序稳定。遇到“同一份代码,小程序颜色正常、Android 发灰发白”的情况,应优先怀疑渐变和叠层渲染回退,而不是业务数据或主题变量。 > > **规范**:复杂视觉区域采用“两层策略”最稳妥:默认样式保留渐变,小程序/H5 使用完整视觉;Android 端通过 `#ifdef APP-ANDROID` 或运行时 `isAndroidApp` 类名切换到纯色回退,保证颜色稳定。 --- ## 第三轮通用修复规律总结 ### 规律一:消费者端主链路页面的 scroll-view 不要依赖浏览器式自适应高度 | 场景 | 更稳妥的 Android 写法 | | ----------------------------- | ------------------------------------------------------------------ | | 页面只有一个滚动主体 | 根容器显式高度 + scroll-view 显式高度 | | 页面有固定头部 / Tab / 筛选条 | `windowHeight - 固定区域高度` 计算出内容区像素高度 | | 页面内部有虚拟列表 | 虚拟列表 `viewportHeight` 既用于计算,也必须真正绑定到 scroll-view | ### 规律二:端差异优先做“条件运行”而不是直接改公共样式 ```typescript // #ifdef APP-ANDROID isAndroidApp.value = true; // #endif ``` 通过端标记切换模板类名或样式字符串,可以保证: 1. Android 端拿到稳定布局/配色 2. 小程序端保持原有视觉和交互 3. 后续排查时能快速判断某个问题是不是 Android 专用回退逻辑触发 ### 规律三:视觉兼容优先降级到“可控纯色”,不要硬扛复杂渐变 当页面关键区域承担品牌色表达时,Android 端出现颜色失真,优先级应是: 1. 先保证颜色正确 2. 再保证层次接近 3. 最后才追求和小程序完全一致的渐变细节 原因很简单:用户首先感知的是“颜色对不对”,其次才是“渐变够不够精致”。 --- _第三轮记录更新:2026-05-13_ --- ## 13. Delivery 端地址空白问题(PostgreSQL 函数层面) > **记录时间**:2026-05-28 > **涉及表**:`ec_care_tasks`、`hss_service_orders` > **涉及函数**:`public.delivery_build_order_json(...)` > **影响端**:医疗-delivery(配送端订单详情页、路线页) --- ### 13.1 问题现象 Consumer 端下单后,订单详情页地址显示正常。但 Delivery 端的订单详情页(`detail.uvue`)和路线页(`route.uvue`)中,`order.address`、`order.addressDetail` 等地址字段始终为空白。 同时: - `elderGender`(性别)和 `elderAge`(年龄)显示正常 - 只有地址相关字段(`address`、`addressSummary`、`addressDetail`、`fullAddress`、`latitude`、`longitude`)为空 --- ### 13.2 数据层背景 本项目存在**双订单存储**: | 表 | 说明 | 地址字段 | |----|------|----------| | `ec_care_tasks` | Care 路径(新) | `address_snapshot`(JSONB)、`address_snapshot_json`(JSONB) | | `hss_service_orders` | Legacy 路径(旧) | `address_snapshot_json`(JSONB,NOT NULL DEFAULT `'{}'::jsonb`) | Consumer 端创建订单时,根据 `useAddressSnapshot` 参数决定写入 `address_snapshot` 还是 `address_snapshot_json`。`ec_care_tasks` 表两列同时存在,`hss_service_orders` 表只有 `address_snapshot_json` 列。 Delivery 端通过 RPC 调用 `rpc_delivery_order_list` / `rpc_delivery_dashboard` / `rpc_delivery_order_detail` 获取订单,这些 RPC 内部统一调用 `delivery_build_order_json` 函数来构建返回的 JSON 结构。 --- ### 13.3 排查过程 #### 第一轮:怀疑空对象阻断 fallback 最初怀疑 `address_snapshot_json` 列存储了空对象 `'{}'::jsonb`,导致 `COALESCE` 将其视为有效值而跳过 fallback 到 `address_snapshot`。 尝试修复:执行 `20260528_fix_address_fallback.sql`,在 `v_address` 的 `COALESCE` 中加入 `NULLIF` 排除空对象: ```sql v_address JSONB := COALESCE( NULLIF(p_raw -> 'address_snapshot_json', '{}'::jsonb), NULLIF(p_raw -> 'address_snapshot_json', 'null'::jsonb), NULLIF(p_raw -> 'address_snapshot', '{}'::jsonb), NULLIF(p_raw -> 'address_snapshot', 'null'::jsonb), '{}'::jsonb ); ``` **结果**:执行后 Delivery 端地址仍然空白,修复未生效。 #### 第二轮:诊断脚本定位 创建并执行诊断脚本 `20260528_debug_address.sql`,三个诊断查询: **诊断1:确认函数版本** ```sql SELECT proname, CASE WHEN prosrc LIKE '%NULLIF(p_raw -> ''address_snapshot_json'', ''{}''::jsonb)%' THEN 'FIXED' ... END AS address_fallback_status, CASE WHEN prosrc LIKE '%|| jsonb_build_object%' THEN 'SPLIT' ELSE 'SINGLE_BLOCK' END AS function_structure FROM pg_proc WHERE proname = 'delivery_build_order_json'; ``` 结果:`FIXED (含 NULLIF 排除空对象)` + `SPLIT (小块拼接)`。说明函数版本已经是最新修复版。 **诊断2:查看订单地址数据状态** ```sql SELECT id, task_no, COALESCE(address_snapshot_json::TEXT, 'NULL') AS addr_json_raw, COALESCE(address_snapshot::TEXT, 'NULL') AS addr_snap_raw FROM public.ec_care_tasks ORDER BY created_at DESC LIMIT 5; ``` 结果:`address_snapshot_json` 全部是 `NULL`,`address_snapshot` **有完整数据**(包含 `fullAddress`、`detailAddress`、`latitude`、`longitude` 等字段)。 **诊断3:直接测试函数输出** ```sql SELECT delivery_build_order_json(to_jsonb(t), '[]'::jsonb, '[]'::jsonb, '[]'::jsonb, NULL, 'care') ->> 'address' AS test_address FROM public.ec_care_tasks t ORDER BY created_at DESC LIMIT 1; ``` 结果:`test_address` = **空**,但 `test_gender` = "女"、`test_age` = 78(正常)。 #### 第三轮:逐步拆解 COALESCE 为了确认 `COALESCE` + `NULLIF` 的行为,执行逐步测试: ```sql WITH test_data AS (SELECT to_jsonb(t) AS p_raw FROM public.ec_care_tasks t ORDER BY created_at DESC LIMIT 1) SELECT p_raw -> 'address_snapshot_json' AS step1_raw_json_col, -- NULL NULLIF(p_raw -> 'address_snapshot_json', '{}'::jsonb) AS step2, -- NULL NULLIF(p_raw -> 'address_snapshot_json', 'null'::jsonb) AS step3, -- NULL p_raw -> 'address_snapshot' AS step4_raw_snap_col, -- 有完整数据 NULLIF(p_raw -> 'address_snapshot', '{}'::jsonb) AS step5, -- 有完整数据 COALESCE( NULLIF(p_raw -> 'address_snapshot_json', '{}'::jsonb), NULLIF(p_raw -> 'address_snapshot_json', 'null'::jsonb), NULLIF(p_raw -> 'address_snapshot', '{}'::jsonb), NULLIF(p_raw -> 'address_snapshot', 'null'::jsonb), '{}'::jsonb ) AS final_v_address -- NULL ❌ FROM test_data; ``` **关键发现**: - `step5`(单独计算的 `NULLIF`)= **有完整数据的 JSONB 对象** - `final_v_address`(同样的表达式嵌套在 `COALESCE` 中)= **NULL** 这说明 **`COALESCE(NULLIF(...))` 的组合在 PostgreSQL 中对 JSONB 类型存在不可预期的行为**。虽然每个子表达式单独执行都正确,但嵌套在 `COALESCE` 内部时,`v_address` 最终变成了 `NULL`。 #### 第四轮:确认 `to_jsonb` 直接访问正常 为了排除 `to_jsonb(t)` 本身的问题,直接测试: ```sql SELECT to_jsonb(t) -> 'address_snapshot' ->> 'fullAddress' AS test_fullAddress, to_jsonb(t) -> 'address_snapshot' ->> 'detailAddress' AS test_detail FROM public.ec_care_tasks t ORDER BY created_at DESC LIMIT 1; ``` 结果:`test_fullAddress` = "广东省梅州市梅县区扶大镇新城科技园景逸花园 骅锋科技",`test_detail` = "骅锋科技"。 **结论:数据完全正常,问题只在函数内部的 `v_address` 赋值逻辑。** --- ### 13.4 根因分析 PostgreSQL 的 `COALESCE` + `NULLIF` 组合在处理 JSONB 类型时存在**隐式类型推断陷阱**: 1. `p_raw -> 'address_snapshot_json'` 返回 SQL `NULL` 2. `NULLIF(NULL, '{}'::jsonb)` 返回 SQL `NULL` 3. `NULLIF(NULL, 'null'::jsonb)` 返回 SQL `NULL` 4. `p_raw -> 'address_snapshot'` 返回有数据的 JSONB 对象 5. `NULLIF(对象, '{}'::jsonb)` 应该返回该对象 但当这些表达式嵌套在 `COALESCE` 中时,`COALESCE` 的类型统一推断机制与 `NULLIF` 的返回值类型产生了冲突,导致整个表达式最终返回 `NULL` 而非预期的 JSONB 对象。 > 注:此行为在 PostgreSQL 文档中无明确定义,属于 JSONB 类型与 `COALESCE`/`NULLIF` 交互的边界情况。同样的逻辑对 `TEXT`、`INTEGER` 等基础类型工作正常。 --- ### 13.5 修复方案 **核心改动**:将 `v_address` 的赋值从 `COALESCE(NULLIF(...))` 改为 `CASE WHEN jsonb_typeof(...) = 'object'`,完全绕过 `COALESCE` + `NULLIF` 的陷阱。 ```sql -- 旧写法(有bug) v_address JSONB := COALESCE( NULLIF(p_raw -> 'address_snapshot_json', '{}'::jsonb), NULLIF(p_raw -> 'address_snapshot_json', 'null'::jsonb), NULLIF(p_raw -> 'address_snapshot', '{}'::jsonb), NULLIF(p_raw -> 'address_snapshot', 'null'::jsonb), '{}'::jsonb ); -- 新写法(可靠) v_address JSONB := CASE WHEN jsonb_typeof(p_raw -> 'address_snapshot_json') = 'object' AND p_raw -> 'address_snapshot_json' != '{}'::jsonb AND p_raw -> 'address_snapshot_json' != 'null'::jsonb THEN p_raw -> 'address_snapshot_json' WHEN jsonb_typeof(p_raw -> 'address_snapshot') = 'object' AND p_raw -> 'address_snapshot' != '{}'::jsonb AND p_raw -> 'address_snapshot' != 'null'::jsonb THEN p_raw -> 'address_snapshot' ELSE '{}'::jsonb END; ``` **修复文件**:`mall_sql/migrations/20260528_fix_address_v2.sql` **验证**: ```sql SELECT delivery_build_order_json(to_jsonb(t), '[]'::jsonb, '[]'::jsonb, '[]'::jsonb, NULL, 'care') ->> 'address' AS test_address FROM public.ec_care_tasks t ORDER BY created_at DESC LIMIT 1; -- 结果:"广东省梅州市梅县区扶大镇新城科技园景逸花园 骅锋科技" ✅ ``` --- ### 13.6 经验教训 | # | 教训 | |---|------| | 1 | **不要用 `COALESCE(NULLIF(...))` 处理 JSONB 类型**。PostgreSQL 中 JSONB 与 `COALESCE`/`NULLIF` 的组合行为不稳定,应改用 `CASE WHEN` + `jsonb_typeof()` | | 2 | **数据层诊断优先于前端排查**。当 Delivery 端显示空白而 Consumer 端正常时,首先应检查 RPC 函数对同一条数据的实际输出,而不是怀疑前端渲染逻辑 | | 3 | **逐步拆解复杂表达式**。`step5` 单独执行正确但 `final_v_address` 为 NULL,说明问题出在表达式嵌套层面,而非数据本身 | | 4 | **保留诊断脚本归档**。`20260528_debug_address.sql` 可作为后续类似问题的排查模板 | --- _第四轮记录更新:2026-05-28_