Files
guilixu-admin/.workbuddy/memory/2026-08-05.md
T
gxwebsoft 3bc856fc95 feat(shopGoods): 优化商品列表加载及封面图性能
- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL
- shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力
- shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用
- 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度
- search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求
- 新增商品封面图样式,保证图片大小固定且样式统一
2026-08-06 17:19:15 +08:00

91 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 个请求:pageShopGoodsdatasource)、getCountsearch.vue reload)、listShopGoodsCategoryindex.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 整体**;仅当前模块。
## 后端 /datagetCount)实现细节(影响合并决策)
- `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 等共享业务在两端都有活代码,改动需两端同步。