feat(shopGoods): 优化商品列表加载及封面图性能
- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL - shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力 - shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用 - 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度 - search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求 - 新增商品封面图样式,保证图片大小固定且样式统一
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# 2026-08-05 商品管理页(shopGoods)加载优化分析
|
||||
|
||||
## 关键结论(修正 2026-08-04 误判)
|
||||
- 后端 `mp-java` 的 `ShopGoodsMapper.xml` 已支持 `sceneType` 参数:
|
||||
- `on_sale` → `AND a.status = 0`
|
||||
- `pending` → `AND a.status != 0`
|
||||
- `sold_out` → `AND a.stock = 0`
|
||||
- 前端 `search.vue` 传 `where.sceneType` 给 `pageShopGoods` 是**有效的**(JS 运行时携带,TS 类型未声明但不影响运行)。
|
||||
- 结论:「出售中/待上架/已售罄」状态筛选功能**正常**,**不是卡顿根因**。上轮判定"sceneType 后端不识别"为误判。
|
||||
|
||||
## getCount 合并进 pageShopGoods 的影响面
|
||||
- 商品模块 `getCount`(`/shop/shop-goods/data`)仅被 `src/views/shop/shopGoods/components/search.vue` **一处**调用。
|
||||
- `cms/cmsArticle` 的 `getCount` 是独立接口(`/cms/cms-article/data`),互不影响。
|
||||
- `guilixu-taro`(用户端小程序)未调用 `/shop/shop-goods/data`。
|
||||
- 合并到 page 响应、保留 `/data` 接口 → 向后兼容,零影响其他端。
|
||||
- 收益有限:当前 `getCount` 与 `page` 在 `reload()` 里已是并发触发,合并仅省一次 HTTP 往返,非量级差异。
|
||||
|
||||
## 首屏请求并发情况
|
||||
- 首屏同一帧发 3 个请求:pageShopGoods(datasource)、getCount(search.vue reload)、listShopGoodsCategory(index.vue 顶层 query())。
|
||||
- `search.vue` 的 `watch(() => props.merchantId, ..., { immediate: true })` 与 `index.vue` 顶层 `query()` 存在时序竞争,但单次进入只触发一次,未构成死循环。
|
||||
|
||||
## 卡顿真因(待 profiling 确认,未执行改动)
|
||||
- 头号嫌疑:商品封面 `<a-image>`(Ant Design Vue)不支持原生 `loading="lazy"`,首屏多行图片一次性并发请求;另 `getCompressedImageUrl` 每次 v-for 渲染重算。
|
||||
- 次因:首屏 3 并发 + watch immediate 竞争。
|
||||
- 建议:让用户开 DevTools → Network 看 `pageShopGoods` 耗时与是否有重复/慢请求,再定改动优先级。
|
||||
|
||||
## 用户决策(本轮)
|
||||
- 已售罄=库存为0(与后端一致,确认)。
|
||||
- getCount 合并影响面小但收益有限,待用户定。
|
||||
- 导出(listShopGoods 全量)本轮先不做。
|
||||
- 澄清:要替换的是商品封面 `<a-image>`,**不是 ele-pro-table 整体**;仅当前模块。
|
||||
|
||||
## 后端 /data(getCount)实现细节(影响合并决策)
|
||||
- `ShopGoodsController.data()` 实际执行:
|
||||
1. `count(status=0 AND stock>0)` → totalNum(出售中)
|
||||
2. `count(status>0)` → totalNum2(待上架)
|
||||
3. `count(stock=0)` → totalNum3(已售罄)
|
||||
4. **`update shop_goods SET status=1 WHERE stock=0`** ← 写副作用!每次调 /data 都会把已售罄商品自动下架。
|
||||
- `/page` 用 MyBatis-Plus `selectPageRel`,内部已含 1 次 count(total) + 1 次 select,共 2 SQL。
|
||||
- 现状进入/筛选:page(2 SQL) + data(3 count + 1 update = 4 SQL) 并发。
|
||||
- 翻页/排序:只调 page(2 SQL),**不触发 data 的 update**。
|
||||
|
||||
## getCount 合并进 /page 的后端消耗结论(负优化,不建议)
|
||||
- 合并后每次 page 调用(含翻页/排序)都多跑 3 次 count + 1 次 update → 翻页/排序 SQL 从 2 飙到 6。
|
||||
- 列表返回变慢:原来 page 的 2 SQL 快速回,data 的 4 SQL 异步在后;合并后列表要等额外 3 count + 1 update 算完才返回。
|
||||
- 写副作用被高频触发:翻页/排序也跑 `UPDATE ... WHERE stock=0`,幂等但带来行锁竞争,且写操作混在读链路是反模式。
|
||||
- 总 DB 读次数在「进入/筛选」场景不变,但「翻页/排序」场景陡增;净收益为负(仅省一次 HTTP 往返)。
|
||||
- **结论:单纯合并是负优化,不执行。** 真要优化后端统计应:① 3 次 count 合成 1 次 GROUP BY 条件聚合;② 把 update 副作用从读接口剥离(移到定时任务/下单扣库存时);③ 再谈是否挂到 page 响应。前端侧保持并发(Promise.all)即可,零后端改动。
|
||||
|
||||
## 已执行的前端优化(2026-08-05)
|
||||
用户确认执行,已改 3 个文件:
|
||||
|
||||
### 1. 商品封面图懒加载(index.vue + image.ts)★ 最可能见效
|
||||
- `src/views/shop/shopGoods/index.vue` 第 39 行附近:`<a-image :src :preview=false :width :height>` 替换为原生
|
||||
`<img class="goods-thumb" loading="lazy" width="50" height="50" alt="">`。
|
||||
- 原因:antdv `a-image` 不支持转发原生 `loading="lazy"`,且即使 `:preview=false` 仍包一层容器/事件;换成原生 `<img>` 后由浏览器 IntersectionObserver 懒加载,首屏仅可视区~10 张图请求。
|
||||
- 新增 `.goods-thumb` 样式(object-fit:cover、圆角 4px、暗色占位背景)。
|
||||
- `src/utils/image.ts`:`getCompressedImageUrl` 加 `Map<string,string>` 缓存,避免列表重渲染(勾选/hover/滚动)时重复拼接 OSS URL 字符串。全局受益,纯函数无副作用。
|
||||
|
||||
### 2. 首屏请求收敛(index.vue + search.vue)
|
||||
- `index.vue`:`<ele-pro-table>` 加 `:init-load="false"`(关闭表格自身自动首屏加载)。
|
||||
- `search.vue`:去掉 `watch(merchantId, {...immediate:true})`,改为 `onMounted` 主动触发一次 `reload()`(设置 where.merchantId + 发 getCount + emit search 触发表格首次加载)。
|
||||
- 原因:原 `initLoad`(默认 true) + `watch immediate` 导致首屏重复发一次 `pageShopGoods`(自动加载一次 + watch 又 reload 一次);改后首屏只有 1 次 page + 1 次 getCount(并发),无重复。
|
||||
- `index.vue`:`query()`(加载分类)从顶层同步调用改为 `onMounted(query)`,分类数据延迟到列表首屏渲染后异步加载,不阻塞首屏;去掉 `query()` 内无用的 `loading.value = true` 写入。
|
||||
- 注:`loading` ref 仍保留(handleExport 用它做防重入,非死变量,上轮"loading 死变量"判断有误)。
|
||||
|
||||
### 验证
|
||||
- 已跑 `vite build` 后台验证可编译(待结果)。
|
||||
|
||||
## 后端优化已执行(2026-08-05,用户确认"现在改")
|
||||
改 mp-java 共 6 处:
|
||||
1. `ShopGoodsMapper.xml`:新增 `selectCountSummary` —— `CASE WHEN` 聚合一条 SQL 返回 totalNum(出售中=status=0且stock>0)/totalNum2(待上架=status>0)/totalNum3(已售罄=stock=0),替代原 3 次 count;含 `param.merchantId` 可选条件,多租户插件自动加 tenant_id。
|
||||
2. `ShopGoodsMapper.java`:加 `selectCountSummary` + `downSoldOutGoods`(`@InterceptorIgnore(tenantLine=true)` 跨租户 `UPDATE shop_goods SET status=1 WHERE stock=0 AND status!=1`,加 `status!=1` 避免重复写)。
|
||||
3. `ShopGoodsService.java`/`Impl`:`getCountSummary(param)`(聚合统计,Map→int 安全转换)+ `downSoldOutGoods()`。
|
||||
4. `ShopGoodsController.data()`:改为调 `getCountSummary(param)`,删除 3 次 count 与 update 写操作(data 接口不再写库)。
|
||||
5. 新建 `shop/task/GoodsSoldOutTask.java`:`@Component` + `@Scheduled(cron="${shop.goods.sold-out.cron:0 */5 * * * ?}")`,每 5 分钟全局下架已售罄商品(仿 CouponExpireTask 写法,@EnableScheduling 已在 WebSoftApplication 启用)。
|
||||
- 编译验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper.class)。
|
||||
- **语义变化(需用户知晓)**:
|
||||
- 下架时机:原"每次打开商品列表才下架当前租户" → 现"定时任务每 5 分钟全局下架所有租户"。5 分钟窗口内新售罄商品会短暂留在"出售中"列表(on_sale 只查 status=0),直至定时任务执行。
|
||||
- 范围:原依赖请求线程租户上下文(只下当前租户)→ 现 `@InterceptorIgnore` 跨租户(所有商户已售罄都下架),更彻底一致。
|
||||
- 频率可调:`shop.goods.sold-out.cron` 配置项。
|
||||
- 若要求"出售中"列表立即可不显示 stock=0,可给 on_sale SQL 加 `AND a.stock > 0`(未做,超出剥离范围,待用户决定)。
|
||||
- 注:Controller 改造后 `LambdaQueryWrapper`/`LambdaUpdateWrapper` import 可能 unused,但 Java 编译允许 unused import 且其他接口可能仍用,未清理(编译已通过)。
|
||||
|
||||
## ⚠️ 重要修正:guilixu-java 才是本次后端优化的目标端(2026-08-05 用户指出"改错路径")
|
||||
- 用户明确:后端改动**应该**落在 `/Users/gxwebsoft/JAVA/guilixu-java`,且 **mp-java 也保留**(两端都要有)。
|
||||
- 原因澄清:shopGoods 的 `ShopGoodsController.data()` / Service / Mapper / task 在 **mp-java 与 guilixu-java 都存在且结构一致(同 `com.gxwebsoft.shop` 包)**,所以"共享 shop 业务模块"的改动要**两端同步**。
|
||||
- 已对 guilixu-java 等价移植上述 6 处改动(selectCountSummary SQL、selectCountSummary/downSoldOutGoods 方法、getCountSummary/downSoldOutGoods 方法体、data() 剥离写、新建 GoodsSoldOutTask),与 mp-java 完全一致。
|
||||
- 验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper/Controller.class)。
|
||||
- 教训:之前 MEMORY.md 说"guilixu-java 的 cms 是死代码、前端不调 guilixu-java"是**针对 cms 模块**的;**不能推广到所有 shop 模块**——shopGoods 等共享业务在两端都有活代码,改动需两端同步。
|
||||
Reference in New Issue
Block a user