3bc856fc95
- image.ts 中 getCompressedImageUrl 增加缓存,避免列表重渲染时重复计算压缩 URL - shopGoods 页面用原生 img 标签替换 antdv a-image,支持浏览器懒加载减少首屏网络压力 - shopGoods 页面数据加载流程优化,取消表格自动首屏加载,改为手动触发,合并请求避免重复调用 - 首屏分类数据延迟加载,不阻塞首屏渲染,提升页面响应速度 - search.vue 中用 onMounted 替代 watch immediate,防止首次入场重复触发请求 - 新增商品封面图样式,保证图片大小固定且样式统一
8.9 KiB
8.9 KiB
2026-08-05 商品管理页(shopGoods)加载优化分析
关键结论(修正 2026-08-04 误判)
- 后端
mp-java的ShopGoodsMapper.xml已支持sceneType参数:on_sale→AND a.status = 0pending→AND a.status != 0sold_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()实际执行:count(status=0 AND stock>0)→ totalNum(出售中)count(status>0)→ totalNum2(待上架)count(stock=0)→ totalNum3(已售罄)update shop_goods SET status=1 WHERE stock=0← 写副作用!每次调 /data 都会把已售罄商品自动下架。
/page用 MyBatis-PlusselectPageRel,内部已含 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、暗色占位背景)。
- 原因:antdv
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写入。- 注:
loadingref 仍保留(handleExport 用它做防重入,非死变量,上轮"loading 死变量"判断有误)。
验证
- 已跑
vite build后台验证可编译(待结果)。
后端优化已执行(2026-08-05,用户确认"现在改")
改 mp-java 共 6 处:
ShopGoodsMapper.xml:新增selectCountSummary——CASE WHEN聚合一条 SQL 返回 totalNum(出售中=status=0且stock>0)/totalNum2(待上架=status>0)/totalNum3(已售罄=stock=0),替代原 3 次 count;含param.merchantId可选条件,多租户插件自动加 tenant_id。ShopGoodsMapper.java:加selectCountSummary+downSoldOutGoods(@InterceptorIgnore(tenantLine=true)跨租户UPDATE shop_goods SET status=1 WHERE stock=0 AND status!=1,加status!=1避免重复写)。ShopGoodsService.java/Impl:getCountSummary(param)(聚合统计,Map→int 安全转换)+downSoldOutGoods()。ShopGoodsController.data():改为调getCountSummary(param),删除 3 次 count 与 update 写操作(data 接口不再写库)。- 新建
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/LambdaUpdateWrapperimport 可能 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 等共享业务在两端都有活代码,改动需两端同步。