refactor(shop-goods): 优化商品统计逻辑并新增已售罄商品定时下架任务
- 将商品数量统计由多次 count 查询改为单条 SQL 聚合查询 - 在 Service 层新增 getCountSummary 方法封装统计逻辑 - 从统计接口剥离写操作,新增定时任务 GoodsSoldOutTask 定期下架库存为0的商品 - 添加 Mapper 中对应的 selectCountSummary 和 downSoldOutGoods 方法及 SQL - 避免查询接口中进行写库操作,提升性能与读写分离 - 优化 CmsWebsiteSetting 查询接口,添加对逻辑删除孤儿记录的自愈机制 - 新增 Mapper 方法 selectByWebsiteIdIgnoreLogic 和 reviveById 用于记录复活
This commit is contained in:
@@ -129,35 +129,7 @@ public class ShopGoodsController extends BaseController {
|
||||
@Operation(summary = "统计信息")
|
||||
@GetMapping("/data")
|
||||
public ApiResult<Map<String, Integer>> data(ShopGoodsParam param) {
|
||||
Map<String, Integer> data = new HashMap<>();
|
||||
final LambdaQueryWrapper<ShopGoods> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
if (param.getMerchantId() != null) {
|
||||
wrapper.eq(ShopGoods::getMerchantId,param.getMerchantId());
|
||||
}
|
||||
|
||||
long totalNum = shopGoodsService.count(
|
||||
wrapper.eq(ShopGoods::getStatus,0).gt(ShopGoods::getStock,0)
|
||||
);
|
||||
data.put("totalNum", Math.toIntExact(totalNum));
|
||||
wrapper.clear();
|
||||
|
||||
long totalNum2 = shopGoodsService.count(
|
||||
wrapper.gt(ShopGoods::getStatus,0)
|
||||
);
|
||||
data.put("totalNum2", Math.toIntExact(totalNum2));
|
||||
wrapper.clear();
|
||||
|
||||
long totalNum3 = shopGoodsService.count(
|
||||
wrapper.eq(ShopGoods::getStock,0)
|
||||
);
|
||||
data.put("totalNum3", Math.toIntExact(totalNum3));
|
||||
wrapper.clear();
|
||||
|
||||
// 下架已售罄的商品
|
||||
shopGoodsService.update(new LambdaUpdateWrapper<ShopGoods>().eq(ShopGoods::getStock,0).set(ShopGoods::getStatus,1));
|
||||
|
||||
return success(data);
|
||||
return success(shopGoodsService.getCountSummary(param));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品Mapper
|
||||
@@ -48,4 +49,23 @@ public interface ShopGoodsMapper extends BaseMapper<ShopGoods> {
|
||||
@Update("UPDATE shop_goods SET sales = IFNULL(sales, 0) + #{saleCount} WHERE goods_id = #{goodsId}")
|
||||
int addSaleCount(@Param("goodsId") Integer goodsId, @Param("saleCount") Integer saleCount);
|
||||
|
||||
/**
|
||||
* 商品数量统计(出售中=status=0且stock>0 / 待上架=status>0 / 已售罄=stock=0)
|
||||
* 一条 SQL 聚合,替代 3 次 count 查询
|
||||
*
|
||||
* @param param 查询参数
|
||||
* @return 包含 totalNum/totalNum2/totalNum3 的统计 Map
|
||||
*/
|
||||
Map<String, Object> selectCountSummary(@Param("param") ShopGoodsParam param);
|
||||
|
||||
/**
|
||||
* 下架已售罄(库存为0)的商品,忽略租户隔离(全局下架)
|
||||
* 由定时任务调用,从统计读接口剥离出的写操作
|
||||
*
|
||||
* @return 影响的行数
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Update("UPDATE shop_goods SET status = 1 WHERE stock = 0 AND status != 1")
|
||||
int downSoldOutGoods();
|
||||
|
||||
}
|
||||
|
||||
@@ -148,4 +148,18 @@
|
||||
<include refid="selectSql"></include>
|
||||
</select>
|
||||
|
||||
<!-- 商品数量统计:出售中/待上架/已售罄,一条 SQL 聚合替代 3 次 count -->
|
||||
<select id="selectCountSummary" resultType="java.util.Map">
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN a.status = 0 AND a.stock > 0 THEN 1 ELSE 0 END), 0) AS totalNum,
|
||||
COALESCE(SUM(CASE WHEN a.status > 0 THEN 1 ELSE 0 END), 0) AS totalNum2,
|
||||
COALESCE(SUM(CASE WHEN a.stock = 0 THEN 1 ELSE 0 END), 0) AS totalNum3
|
||||
FROM shop_goods a
|
||||
<where>
|
||||
<if test="param.merchantId != null">
|
||||
AND a.merchant_id = #{param.merchantId}
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.gxwebsoft.shop.entity.ShopGoods;
|
||||
import com.gxwebsoft.shop.param.ShopGoodsParam;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品Service
|
||||
@@ -49,4 +50,19 @@ public interface ShopGoodsService extends IService<ShopGoods> {
|
||||
*/
|
||||
boolean addSaleCount(Integer goodsId, Integer saleCount);
|
||||
|
||||
/**
|
||||
* 商品数量统计(出售中/待上架/已售罄),一条 SQL 聚合替代 3 次 count
|
||||
*
|
||||
* @param param 查询参数
|
||||
* @return 包含 totalNum/totalNum2/totalNum3 的统计 Map
|
||||
*/
|
||||
Map<String, Integer> getCountSummary(ShopGoodsParam param);
|
||||
|
||||
/**
|
||||
* 下架已售罄(库存为0)的商品,供定时任务调用
|
||||
*
|
||||
* @return 影响的行数
|
||||
*/
|
||||
int downSoldOutGoods();
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品Service实现
|
||||
@@ -72,4 +74,26 @@ public class ShopGoodsServiceImpl extends ServiceImpl<ShopGoodsMapper, ShopGoods
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getCountSummary(ShopGoodsParam param) {
|
||||
Map<String, Object> summary = baseMapper.selectCountSummary(param);
|
||||
Map<String, Integer> data = new HashMap<>(3);
|
||||
data.put("totalNum", toInt(summary.get("totalNum")));
|
||||
data.put("totalNum2", toInt(summary.get("totalNum2")));
|
||||
data.put("totalNum3", toInt(summary.get("totalNum3")));
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int downSoldOutGoods() {
|
||||
return baseMapper.downSoldOutGoods();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将聚合统计结果安全转为 int(数据库 NULL 或缺失视为 0)
|
||||
*/
|
||||
private int toInt(Object value) {
|
||||
return value == null ? 0 : ((Number) value).intValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
43
src/main/java/com/gxwebsoft/shop/task/GoodsSoldOutTask.java
Normal file
43
src/main/java/com/gxwebsoft/shop/task/GoodsSoldOutTask.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.gxwebsoft.shop.task;
|
||||
|
||||
import com.gxwebsoft.shop.service.ShopGoodsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 已售罄商品自动下架定时任务
|
||||
*
|
||||
* <p>从 {@code ShopGoodsController.data()} 统计读接口中剥离出的写操作:
|
||||
* 原实现每次打开商品列表都会执行 {@code UPDATE ... SET status=1 WHERE stock=0},
|
||||
* 属于在查询接口里写库的反模式,且翻页/排序若合并进 page 会产生高频写。
|
||||
* 此处改为独立定时任务,避免读链路与写操作耦合。</p>
|
||||
*
|
||||
* <p>默认每 5 分钟执行一次,可通过配置 {@code shop.goods.sold-out.cron} 调整。</p>
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2026-08-05
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GoodsSoldOutTask {
|
||||
|
||||
@Autowired
|
||||
private ShopGoodsService shopGoodsService;
|
||||
|
||||
/**
|
||||
* 定时下架已售罄(库存为 0)的商品,忽略租户隔离(全局下架所有商户)
|
||||
*/
|
||||
@Scheduled(cron = "${shop.goods.sold-out.cron:0 */5 * * * ?}")
|
||||
public void downSoldOutGoods() {
|
||||
try {
|
||||
int count = shopGoodsService.downSoldOutGoods();
|
||||
if (count > 0) {
|
||||
log.info("定时下架已售罄商品完成,下架数量: {}", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("定时下架已售罄商品任务执行失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user