Files
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

8.9 KiB
Raw Permalink Blame History

2026-08-05 商品管理页(shopGoods)加载优化分析

关键结论(修正 2026-08-04 误判)

  • 后端 mp-javaShopGoodsMapper.xml 已支持 sceneType 参数:
    • on_saleAND a.status = 0
    • pendingAND a.status != 0
    • sold_outAND a.stock = 0
  • 前端 search.vuewhere.sceneTypepageShopGoods有效的(JS 运行时携带,TS 类型未声明但不影响运行)。
  • 结论:「出售中/待上架/已售罄」状态筛选功能正常不是卡顿根因。上轮判定"sceneType 后端不识别"为误判。

getCount 合并进 pageShopGoods 的影响面

  • 商品模块 getCount/shop/shop-goods/data)仅被 src/views/shop/shopGoods/components/search.vue 一处调用。
  • cms/cmsArticlegetCount 是独立接口(/cms/cms-article/data),互不影响。
  • guilixu-taro(用户端小程序)未调用 /shop/shop-goods/data
  • 合并到 page 响应、保留 /data 接口 → 向后兼容,零影响其他端。
  • 收益有限:当前 getCountpagereload() 里已是并发触发,合并仅省一次 HTTP 往返,非量级差异。

首屏请求并发情况

  • 首屏同一帧发 3 个请求:pageShopGoodsdatasource)、getCountsearch.vue reload)、listShopGoodsCategoryindex.vue 顶层 query())。
  • search.vuewatch(() => 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.tsgetCompressedImageUrlMap<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.vuequery()(加载分类)从顶层同步调用改为 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/ImplgetCountSummary(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 等共享业务在两端都有活代码,改动需两端同步。