diff --git a/hiver-admin/src/main/resources/application.yml b/hiver-admin/src/main/resources/application.yml index 28c298a9..9b432fcc 100644 --- a/hiver-admin/src/main/resources/application.yml +++ b/hiver-admin/src/main/resources/application.yml @@ -77,6 +77,13 @@ spring: port: 6379 # 超时时间 Duration类型 3秒 timeout: 3S + # 增加 Jedis 连接池配置 + jedis: + pool: + max-active: 20 # 最大连接数,建议从默认的 8 调大到 20 或更高 + max-wait: 2000ms # 获取连接的最大等待时间,避免无限期阻塞 + max-idle: 10 # 最大空闲连接数 + min-idle: 1 # 最小空闲连接数 # Elasticsearch data: elasticsearch: diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index 82584f4c..e916a29e 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • - 26, 2026 11:40:47 + 27, 2026 15:09:48
  • @@ -84,7 +84,7 @@

    passTest

    -

    11:40:47 / 0.015 secs

    +

    15:09:49 / 0.015 secs

    @@ -92,8 +92,8 @@
    #test-id=1
    passTest
    -04.26.2026 11:40:47 -04.26.2026 11:40:47 +04.27.2026 15:09:49 +04.27.2026 15:09:49 0.015 secs
    @@ -104,7 +104,7 @@ Pass - 11:40:47 + 15:09:49 Test passed @@ -128,13 +128,13 @@

    Started

    -

    26, 2026 11:40:47

    +

    27, 2026 15:09:48

    Ended

    -

    26, 2026 11:40:47

    +

    27, 2026 15:09:49

    diff --git a/hiver-core/src/main/java/cc/hiver/core/common/utils/SecurityUtil.java b/hiver-core/src/main/java/cc/hiver/core/common/utils/SecurityUtil.java index 8af8ee37..28235c16 100644 --- a/hiver-core/src/main/java/cc/hiver/core/common/utils/SecurityUtil.java +++ b/hiver-core/src/main/java/cc/hiver/core/common/utils/SecurityUtil.java @@ -303,7 +303,7 @@ public class SecurityUtil { token = IdUtil.simpleUUID(); tokenUser = new TokenUser(user, tokenProperties.getStorePerms(), saved); // 单设备登录 之前的token失效 - if (tokenProperties.getSdl()) { + /*if (tokenProperties.getSdl()) { final String oldToken = redisTemplateHelper.get(SecurityConstant.USER_TOKEN + user.getUsername() + "type:" + user.getType()); if (CharSequenceUtil.isNotBlank(oldToken)) { redisTemplateHelper.delete(SecurityConstant.TOKEN_PRE + oldToken); @@ -318,7 +318,7 @@ public class SecurityUtil { redisTemplateHelper.delete(regionKey); } - } + }*/ // 是否记住账号/保存登录 if (saved) { redisTemplateHelper.set(SecurityConstant.USER_TOKEN + user.getUsername() + "type:" + user.getType(), token, tokenProperties.getSaveLoginTime(), TimeUnit.DAYS); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/CommentController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/CommentController.java index 2de939f6..2bbe1414 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/CommentController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/CommentController.java @@ -86,66 +86,68 @@ public class CommentController { }else{ e.setLevel(ShopConstant.SHOP_STATUS_NORMAL); } - if(e.getShopId().toUpperCase().startsWith("W")){ - //更新配送员评分 - Worker worker = workerServiceImpl.findByWorkerId(e.getShopId()); - Integer oldCount = worker.getOrderCount() == null ? 0 : worker.getOrderCount(); - BigDecimal oldScore = worker.getScore(); - BigDecimal score = e.getScore(); - - // 1. 更新单量 - int newCount = oldCount + 1; - - // 2. 计算新评分:(旧总分 + 本次评分) / 新单量 - BigDecimal newScore = oldScore.multiply(new BigDecimal(oldCount)) - .add(score) - .divide(new BigDecimal(newCount), 2, RoundingMode.HALF_UP); // 保留2位小数 - DecimalFormat df = new DecimalFormat("00.00"); - - String formattedResult = df.format(newScore); - worker.setScore(new BigDecimal(formattedResult)); - - // 3. 保存更新 - workerServiceImpl.update(worker); - - //更新缓存 - WorkerRedisVo workerRedisVo = workerRedisCacheUtil.get(worker.getRegion(), worker.getWorkerId()); - if(workerRedisVo != null){ - workerRedisVo.setWorker(worker); - workerRedisCacheUtil.update(worker.getRegion(), workerRedisVo); - } - }else{ - //更新店铺评分 - Shop shop = shopService.findById(e.getShopId()); - Integer oldSaleCount = shop.getSaleCount() == null ? 0 : shop.getSaleCount(); - BigDecimal oldScore = shop.getShopScore(); - BigDecimal score = e.getScore(); - - int newCount = oldSaleCount + 1; - - // 2. 计算新评分:(旧总分 + 本次评分) / 新单量 - BigDecimal newScore = oldScore.multiply(new BigDecimal(oldSaleCount)) - .add(score) - .divide(new BigDecimal(newCount), 2, RoundingMode.HALF_UP); // 保留2位小数 - DecimalFormat df = new DecimalFormat("00.00"); - - String formattedResult = df.format(newScore); - shop.setShopScore(new BigDecimal(formattedResult)); - shopService.update(shop); - - String shopCacheKey = "SHOP_CACHE:" + shop.getRegionId(); - String shopJson = (String) redisTemplateHelper.hGet(shopCacheKey, shop.getId()); - if (org.apache.commons.lang3.StringUtils.isNotBlank(shopJson)) { - ShopCacheDTO cacheDTO = JSONUtil.toBean(shopJson, ShopCacheDTO.class); - if (cacheDTO.getShop() != null) { - cacheDTO.getShop().setShopScore(new BigDecimal(formattedResult)); - redisTemplateHelper.hPut(shopCacheKey, shop.getId(), JSONUtil.toJsonStr(cacheDTO)); + if(e.getLevel() == ShopConstant.SHOP_STATUS_LOCK){ + if(e.getShopId().toUpperCase().startsWith("W")){ + //更新配送员评分 + Worker worker = workerServiceImpl.findByWorkerId(e.getShopId()); + Integer oldCount = worker.getOrderCount() == null ? 0 : worker.getOrderCount(); + BigDecimal oldScore = worker.getScore(); + BigDecimal score = e.getScore(); + + // 1. 更新单量 + int newCount = oldCount + 1; + + // 2. 计算新评分:(旧总分 + 本次评分) / 新单量 + BigDecimal newScore = oldScore.multiply(new BigDecimal(oldCount)) + .add(score) + .divide(new BigDecimal(newCount), 2, RoundingMode.HALF_UP); // 保留2位小数 + DecimalFormat df = new DecimalFormat("00.00"); + + String formattedResult = df.format(newScore); + worker.setScore(new BigDecimal(formattedResult)); + + // 3. 保存更新 + workerServiceImpl.update(worker); + + //更新缓存 + WorkerRedisVo workerRedisVo = workerRedisCacheUtil.get(worker.getRegion(), worker.getWorkerId()); + if(workerRedisVo != null){ + workerRedisVo.setWorker(worker); + workerRedisCacheUtil.update(worker.getRegion(), workerRedisVo); + } + }else{ + //更新店铺评分 + Shop shop = shopService.findById(e.getShopId()); + Integer oldSaleCount = shop.getSaleCount() == null ? 0 : shop.getSaleCount(); + BigDecimal oldScore = shop.getShopScore(); + BigDecimal score = e.getScore(); + + int newCount = oldSaleCount + 1; + + // 2. 计算新评分:(旧总分 + 本次评分) / 新单量 + BigDecimal newScore = oldScore.multiply(new BigDecimal(oldSaleCount)) + .add(score) + .divide(new BigDecimal(newCount), 2, RoundingMode.HALF_UP); // 保留2位小数 + DecimalFormat df = new DecimalFormat("00.00"); + + String formattedResult = df.format(newScore); + shop.setShopScore(new BigDecimal(formattedResult)); + shopService.update(shop); + + String shopCacheKey = "SHOP_CACHE:" + shop.getRegionId(); + String shopJson = (String) redisTemplateHelper.hGet(shopCacheKey, shop.getId()); + if (org.apache.commons.lang3.StringUtils.isNotBlank(shopJson)) { + ShopCacheDTO cacheDTO = JSONUtil.toBean(shopJson, ShopCacheDTO.class); + if (cacheDTO.getShop() != null) { + cacheDTO.getShop().setShopScore(new BigDecimal(formattedResult)); + redisTemplateHelper.hPut(shopCacheKey, shop.getId(), JSONUtil.toJsonStr(cacheDTO)); + } } } - } - //放进缓存 小于4分 - if(e.getScore().compareTo(new BigDecimal(4)) < 0){ - commentCacheUtil.put(e.getShopId(),e); + //放进缓存 小于4分 + if(e.getScore().compareTo(new BigDecimal(4)) < 0){ + commentCacheUtil.put(e.getShopId(),e); + } } }); if(comment != null && StringUtils.isNotBlank(comment.get(0).getParentId())){ diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallAdPositionController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallAdPositionController.java new file mode 100644 index 00000000..bc0e71ba --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/MallAdPositionController.java @@ -0,0 +1,66 @@ +package cc.hiver.mall.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.vo.Result; +import cc.hiver.mall.entity.MallAdPosition; +import cc.hiver.mall.pojo.query.MallAdPositionQuery; +import cc.hiver.mall.service.mybatis.MallAdPositionService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/hiver/mall/adPosition") +@Api(tags = "广告位接口") +public class MallAdPositionController { + + @Autowired + private MallAdPositionService mallAdPositionService; + + @PostMapping("/add") + @ApiOperation(value = "新增广告位") + public Result add(@RequestBody MallAdPosition adPosition) { + mallAdPositionService.addAdPosition(adPosition); + return ResultUtil.success("新增成功"); + } + + @PostMapping("/update") + @ApiOperation(value = "修改广告位") + public Result update(@RequestBody MallAdPosition adPosition) { + mallAdPositionService.updateAdPosition(adPosition); + return ResultUtil.success("修改成功"); + } + + @PostMapping("/delete/{id}") + @ApiOperation(value = "删除广告位") + public Result delete(@PathVariable String id) { + mallAdPositionService.deleteAdPosition(id); + return ResultUtil.success("删除成功"); + } + + @GetMapping("/detail/{id}") + @ApiOperation(value = "获取广告位详情") + public Result detail(@PathVariable String id) { + MallAdPosition detail = mallAdPositionService.getAdPositionDetail(id); + return new ResultUtil().setData(detail); + } + + @RequestMapping(value = "/list", method = RequestMethod.POST) + @ApiOperation(value = "分页查询广告位列表") + public Result> list(@RequestBody MallAdPositionQuery query) { + return new ResultUtil>().setData(mallAdPositionService.selectPageVO(query)); + } + + @GetMapping("/region") + @ApiOperation(value = "根据区域ID获取广告位列表(缓存优先)") + public Result> getByRegionId(@RequestParam(value = "regionId", required = true) String regionId , + @RequestParam(value = "paths", required = true) List paths) { + return new ResultUtil>().setData(mallAdPositionService.getAdsByRegionId(regionId,paths)); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java index cecf29c8..681dab27 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopController.java @@ -251,7 +251,7 @@ public class ShopController { } } //已禁用 - if(s.getStatus() < 1 && CharSequenceUtil.isBlank(shop.getShopIcon())){ + if(s.getStatus() != null && s.getStatus() < 1 && CharSequenceUtil.isBlank(shop.getShopIcon())){ continue; } if (CharSequenceUtil.isNotBlank(shop.getShopType())) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java index 61905063..5e582e0b 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerController.java @@ -81,9 +81,9 @@ public class WorkerController { @RequestMapping(value = "/getByCondition", method = RequestMethod.POST) @ApiOperation(value = "多条件分页获取订单列表") - public Result> getByCondition(@RequestBody WorkerQueryVO worker) { - Page page = (Page) workerService.findByCondition(worker); - return new ResultUtil>().setData(page); + public Result> getByCondition(@RequestBody WorkerQueryVO worker) { + org.springframework.data.domain.Page page = workerService.findByCondition(worker); + return new ResultUtil>().setData(page); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallAdPositionMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallAdPositionMapper.java new file mode 100644 index 00000000..30c248b1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallAdPositionMapper.java @@ -0,0 +1,17 @@ +package cc.hiver.mall.dao.mapper; + +import cc.hiver.mall.entity.MallAdPosition; +import cc.hiver.mall.pojo.query.MallAdPositionQuery; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MallAdPositionMapper extends BaseMapper { + IPage selectPageVO(IPage page, @Param("q") MallAdPositionQuery q); + + List selectByRegionId(@Param("regionId") String regionId); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallAdProductMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallAdProductMapper.java new file mode 100644 index 00000000..abbab751 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/mapper/MallAdProductMapper.java @@ -0,0 +1,17 @@ +package cc.hiver.mall.dao.mapper; + +import cc.hiver.mall.entity.MallAdProduct; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MallAdProductMapper extends BaseMapper { + List selectByAdId(@Param("adId") String adId); + + List selectByAdIds(@Param("adIds") List adIds); + + int deleteByAdId(@Param("adId") String adId); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallAdPosition.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallAdPosition.java new file mode 100644 index 00000000..f2241c2d --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallAdPosition.java @@ -0,0 +1,56 @@ +package cc.hiver.mall.entity; + +import cc.hiver.core.base.HiverBaseEntity; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel(value = "广告位表") +@TableName(value = "t_mall_ad_position", autoResultMap = true) +public class MallAdPosition extends HiverBaseEntity { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "放置位置:如 home_banner, home_popup, category_top 等") + private String position; + + @ApiModelProperty(value = "商家名称") + private String merchantName; + + @ApiModelProperty(value = "商家id") + private String merchantId; + + @ApiModelProperty(value = "商家主图") + private String merchantImage; + + @ApiModelProperty(value = "广告图片") + private String adImage; + + @ApiModelProperty(value = "广告标题") + private String title; + + @ApiModelProperty(value = "跳转链接") + private String linkUrl; + + @ApiModelProperty(value = "跳转链接参数(JSON格式)") + private String linkParams; + + @ApiModelProperty(value = "区域id") + private String regionId; + + @ApiModelProperty(value = "排序值(越小越靠前)") + private Integer sortOrder; + + @ApiModelProperty(value = "状态:0-下架,1-上架") + private Integer status; + + @ApiModelProperty(value = "广告关联商品列表") + @TableField(exist = false) + private List productList; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallAdProduct.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallAdProduct.java new file mode 100644 index 00000000..a7d6a0af --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallAdProduct.java @@ -0,0 +1,39 @@ +package cc.hiver.mall.entity; + +import cc.hiver.core.base.HiverBaseEntity; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel(value = "广告位商品表") +@TableName(value = "t_mall_ad_product", autoResultMap = true) +public class MallAdProduct extends HiverBaseEntity { + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "广告位id") + private String adId; + + @ApiModelProperty(value = "商品图片") + private String productImage; + + @ApiModelProperty(value = "商品名称") + private String productName; + + @ApiModelProperty(value = "商品原价") + private BigDecimal originalPrice; + + @ApiModelProperty(value = "商品拼团价格") + private BigDecimal groupPrice; + + @ApiModelProperty(value = "几人团") + private Integer groupMembers; + + @ApiModelProperty(value = "排序值") + private Integer sortOrder; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallAdPositionQuery.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallAdPositionQuery.java new file mode 100644 index 00000000..c3cfc83c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/query/MallAdPositionQuery.java @@ -0,0 +1,32 @@ +package cc.hiver.mall.pojo.query; + +import cc.hiver.core.base.HiverBasePageQuery; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 广告位分页查询对象 + */ +@ApiModel("广告位分页查询对象") +@Data +public class MallAdPositionQuery extends HiverBasePageQuery { + + @ApiModelProperty(value = "放置位置") + private String position; + + @ApiModelProperty(value = "商家名称") + private String merchantName; + + @ApiModelProperty(value = "商家id") + private String merchantId; + + @ApiModelProperty(value = "广告标题") + private String title; + + @ApiModelProperty(value = "区域id") + private String regionId; + + @ApiModelProperty(value = "状态:0-下架,1-上架") + private Integer status; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallAdPositionService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallAdPositionService.java new file mode 100644 index 00000000..e9fe2d9b --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/mybatis/MallAdPositionService.java @@ -0,0 +1,41 @@ +package cc.hiver.mall.service.mybatis; + +import cc.hiver.mall.entity.MallAdPosition; +import cc.hiver.mall.pojo.query.MallAdPositionQuery; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +public interface MallAdPositionService extends IService { + + /** + * 新增广告位(含商品列表) + */ + void addAdPosition(MallAdPosition adPosition); + + /** + * 修改广告位(含商品列表) + */ + void updateAdPosition(MallAdPosition adPosition); + + /** + * 删除广告位(级联删除商品) + */ + void deleteAdPosition(String id); + + /** + * 获取广告位详情(含商品列表) + */ + MallAdPosition getAdPositionDetail(String id); + + /** + * 分页查询广告位 + */ + IPage selectPageVO(MallAdPositionQuery query); + + /** + * 根据 regionId 获取该区域所有广告位(优先缓存) + */ + List getAdsByRegionId(String regionId,List paths); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallAdPositionServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallAdPositionServiceImpl.java new file mode 100644 index 00000000..d2ab99fb --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallAdPositionServiceImpl.java @@ -0,0 +1,191 @@ +package cc.hiver.mall.serviceimpl.mybatis; + +import cc.hiver.mall.dao.mapper.MallAdPositionMapper; +import cc.hiver.mall.dao.mapper.MallAdProductMapper; +import cc.hiver.mall.entity.MallAdPosition; +import cc.hiver.mall.entity.MallAdProduct; +import cc.hiver.mall.pojo.query.MallAdPositionQuery; +import cc.hiver.mall.service.mybatis.MallAdPositionService; +import cc.hiver.mall.utils.AdPositionCacheUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class MallAdPositionServiceImpl extends ServiceImpl + implements MallAdPositionService { + + @Autowired + private MallAdPositionMapper mallAdPositionMapper; + + @Autowired + private MallAdProductMapper mallAdProductMapper; + + @Autowired + private AdPositionCacheUtil adPositionCacheUtil; + + @Override + @Transactional(rollbackFor = Exception.class) + public void addAdPosition(MallAdPosition adPosition) { + // 1. 保存广告位主表 + this.save(adPosition); + + // 2. 保存关联商品 + if (adPosition.getProductList() != null && !adPosition.getProductList().isEmpty()) { + for (MallAdProduct product : adPosition.getProductList()) { + product.setAdId(adPosition.getId()); + mallAdProductMapper.insert(product); + } + } + + // 3. 更新缓存:将新广告位加入对应 regionId 的缓存 + if (StringUtils.isNotBlank(adPosition.getRegionId())) { + // 重新组装完整对象(包含productList) + adPosition.setProductList(mallAdProductMapper.selectByAdId(adPosition.getId())); + adPositionCacheUtil.put(adPosition.getRegionId(), adPosition); + } + + log.info("新增广告位成功: id={}, regionId={}", adPosition.getId(), adPosition.getRegionId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAdPosition(MallAdPosition adPosition) { + // 获取旧数据,用于处理 regionId 变更时清除旧缓存 + MallAdPosition oldAd = this.getById(adPosition.getId()); + + // 1. 更新广告位主表 + this.updateById(adPosition); + + // 2. 先删除旧的商品关联,再重新插入 + mallAdProductMapper.deleteByAdId(adPosition.getId()); + if (adPosition.getProductList() != null && !adPosition.getProductList().isEmpty()) { + for (MallAdProduct product : adPosition.getProductList()) { + product.setAdId(adPosition.getId()); + mallAdProductMapper.insert(product); + } + } + + // 3. 更新缓存 + if (oldAd != null && StringUtils.isNotBlank(oldAd.getRegionId())) { + // 如果 regionId 发生变化,先删除旧 regionId 下的缓存 + if (!oldAd.getRegionId().equals(adPosition.getRegionId())) { + adPositionCacheUtil.remove(oldAd.getRegionId(), adPosition.getId()); + } + } + if (StringUtils.isNotBlank(adPosition.getRegionId())) { + adPosition.setProductList(mallAdProductMapper.selectByAdId(adPosition.getId())); + adPositionCacheUtil.update(adPosition.getRegionId(), adPosition); + } + + log.info("修改广告位成功: id={}, regionId={}", adPosition.getId(), adPosition.getRegionId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAdPosition(String id) { + MallAdPosition adPosition = this.getById(id); + if (adPosition == null) { + return; + } + + // 1. 删除关联商品 + mallAdProductMapper.deleteByAdId(id); + + // 2. 删除广告位主表 + this.removeById(id); + + // 3. 删除缓存 + if (StringUtils.isNotBlank(adPosition.getRegionId())) { + adPositionCacheUtil.remove(adPosition.getRegionId(), id); + } + + log.info("删除广告位成功: id={}, regionId={}", id, adPosition.getRegionId()); + } + + @Override + public MallAdPosition getAdPositionDetail(String id) { + MallAdPosition adPosition = this.getById(id); + if (adPosition == null) { + return null; + } + // 查询关联商品 + List productList = mallAdProductMapper.selectByAdId(id); + adPosition.setProductList(productList); + return adPosition; + } + + @Override + public IPage selectPageVO(MallAdPositionQuery query) { + IPage page = new Page<>(query.getPageNum(), query.getPageSize()); + IPage result = mallAdPositionMapper.selectPageVO(page, query); + + // 为每条广告位填充商品列表 + if (result.getRecords() != null && !result.getRecords().isEmpty()) { + List adIds = result.getRecords().stream() + .map(MallAdPosition::getId) + .collect(Collectors.toList()); + List allProducts = mallAdProductMapper.selectByAdIds(adIds); + Map> productMap = allProducts.stream() + .collect(Collectors.groupingBy(MallAdProduct::getAdId)); + for (MallAdPosition ad : result.getRecords()) { + ad.setProductList(productMap.getOrDefault(ad.getId(), Collections.emptyList())); + } + } + return result; + } + + @Override + public List getAdsByRegionId(String regionId,List paths) { + if (StringUtils.isBlank(regionId)) { + return Collections.emptyList(); + } + + // 1. 优先查询缓存 + List cached = adPositionCacheUtil.getAll(regionId); + List cachedReturn = new ArrayList<>(); + if (cached != null && !cached.isEmpty()) { + for(MallAdPosition ad : cached){ + if(paths.contains(ad.getPosition())){ + cachedReturn.add(ad); + } + } + return cachedReturn; + } + + // 2. 缓存不存在,查询数据库 + List dbList = mallAdPositionMapper.selectByRegionId(regionId); + if (dbList != null && !dbList.isEmpty()) { + // 填充商品列表 + List adIds = dbList.stream() + .map(MallAdPosition::getId) + .collect(Collectors.toList()); + List allProducts = mallAdProductMapper.selectByAdIds(adIds); + Map> productMap = allProducts.stream() + .collect(Collectors.groupingBy(MallAdProduct::getAdId)); + for (MallAdPosition ad : dbList) { + ad.setProductList(productMap.getOrDefault(ad.getId(), Collections.emptyList())); + if(paths.contains(ad.getPosition())){ + cachedReturn.add(ad); + } + } + + // 3. 写入缓存 + adPositionCacheUtil.putAll(regionId, dbList); + log.info("从数据库加载广告位并写入缓存: regionId={}, count={}", regionId, dbList.size()); + } + return cachedReturn; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/AdPositionCacheUtil.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/AdPositionCacheUtil.java new file mode 100644 index 00000000..caa4cddd --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/AdPositionCacheUtil.java @@ -0,0 +1,231 @@ +package cc.hiver.mall.utils; + +import cc.hiver.core.common.redis.RedisTemplateHelper; +import cc.hiver.mall.entity.MallAdPosition; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 广告位缓存工具类 + *

    + * 底层使用 Redis Hash 结构,按 regionId 维度管理广告位缓存。 + *

    + *   Key   = AD_POSITION:{regionId}
    + *   Field = adPositionId
    + *   Value = MallAdPosition 的 JSON 序列化(含 productList)
    + * 
    + * + * @author system + */ +@Slf4j +@Component +public class AdPositionCacheUtil { + + /** Redis Key 前缀 */ + private static final String KEY_PREFIX = "AD_POSITION:"; + + @Autowired + private RedisTemplateHelper redisTemplateHelper; + + // ================================================================ + // Key 构建 + // ================================================================ + + private String buildKey(String regionId) { + return KEY_PREFIX + regionId; + } + + // ================================================================ + // 存放(put) + // ================================================================ + + /** + * 存放单个广告位到缓存 + * + * @param regionId 区域ID + * @param adPosition 广告位(必须包含有效的 id) + */ + public void put(String regionId, MallAdPosition adPosition) { + if (StringUtils.isBlank(regionId) || adPosition == null || StringUtils.isBlank(adPosition.getId())) { + log.info("AdPositionCacheUtil.put 参数无效, regionId={}, adPosition={}", regionId, adPosition); + return; + } + try { + String key = buildKey(regionId); + String json = JSONUtil.toJsonStr(adPosition); + redisTemplateHelper.hPut(key, adPosition.getId(), json); + log.info("缓存广告位: regionId={}, adId={}", regionId, adPosition.getId()); + } catch (Exception e) { + log.info("缓存广告位失败: regionId={}, adId={}", regionId, adPosition.getId(), e); + } + } + + /** + * 批量存放广告位到缓存(通常用于缓存预热 / 首次加载) + * + * @param regionId 区域ID + * @param ads 广告位列表 + */ + public void putAll(String regionId, List ads) { + if (StringUtils.isBlank(regionId) || ads == null || ads.isEmpty()) { + return; + } + try { + String key = buildKey(regionId); + Map map = new java.util.LinkedHashMap<>(ads.size()); + for (MallAdPosition ad : ads) { + if (ad != null && StringUtils.isNotBlank(ad.getId())) { + map.put(ad.getId(), JSONUtil.toJsonStr(ad)); + } + } + if (!map.isEmpty()) { + redisTemplateHelper.hPutAll(key, map); + log.info("批量缓存广告位: regionId={}, count={}", regionId, map.size()); + } + } catch (Exception e) { + log.info("批量缓存广告位失败: regionId={}", regionId, e); + } + } + + // ================================================================ + // 删除(remove) + // ================================================================ + + /** + * 根据 regionId 和广告位ID 从缓存中删除指定广告位 + * + * @param regionId 区域ID + * @param adId 广告位ID + */ + public void remove(String regionId, String adId) { + if (StringUtils.isBlank(regionId) || StringUtils.isBlank(adId)) { + log.info("AdPositionCacheUtil.remove 参数无效, regionId={}, adId={}", regionId, adId); + return; + } + try { + String key = buildKey(regionId); + redisTemplateHelper.hDelete(key, adId); + log.info("删除广告位缓存: regionId={}, adId={}", regionId, adId); + } catch (Exception e) { + log.info("删除广告位缓存失败: regionId={}, adId={}", regionId, adId, e); + } + } + + /** + * 清除指定区域的所有广告位缓存 + * + * @param regionId 区域ID + */ + public void removeAll(String regionId) { + if (StringUtils.isBlank(regionId)) { + return; + } + try { + redisTemplateHelper.delete(buildKey(regionId)); + log.info("清除全部广告位缓存: regionId={}", regionId); + } catch (Exception e) { + log.info("清除全部广告位缓存失败: regionId={}", regionId, e); + } + } + + // ================================================================ + // 更新(update) + // ================================================================ + + /** + * 更新缓存中的广告位信息(覆盖写入) + * + * @param regionId 区域ID + * @param adPosition 更新后的广告位 + */ + public void update(String regionId, MallAdPosition adPosition) { + if (StringUtils.isBlank(regionId) || adPosition == null || StringUtils.isBlank(adPosition.getId())) { + log.info("AdPositionCacheUtil.update 参数无效, regionId={}, adPosition={}", regionId, adPosition); + return; + } + try { + put(regionId, adPosition); + log.info("更新广告位缓存: regionId={}, adId={}", regionId, adPosition.getId()); + } catch (Exception e) { + log.info("更新广告位缓存失败: regionId={}, adId={}", regionId, adPosition.getId(), e); + } + } + + // ================================================================ + // 查询(get) + // ================================================================ + + /** + * 获取指定区域的全部广告位缓存 + * + * @param regionId 区域ID + * @return 缓存的广告位列表,缓存不存在时返回 null(调用方可据此判断是否需要回源查库) + */ + public List getAll(String regionId) { + if (StringUtils.isBlank(regionId)) { + return null; + } + try { + String key = buildKey(regionId); + Map entries = redisTemplateHelper.hGetAll(key); + if (entries == null || entries.isEmpty()) { + return null; + } + List result = new ArrayList<>(entries.size()); + for (Object value : entries.values()) { + if (value != null) { + result.add(JSONUtil.toBean(value.toString(), MallAdPosition.class)); + } + } + return result; + } catch (Exception e) { + log.info("获取广告位缓存失败: regionId={}", regionId, e); + return null; + } + } + + /** + * 获取缓存中的单个广告位 + * + * @param regionId 区域ID + * @param adId 广告位ID + * @return 缓存的广告位,不存在时返回 null + */ + public MallAdPosition get(String regionId, String adId) { + if (StringUtils.isBlank(regionId) || StringUtils.isBlank(adId)) { + return null; + } + try { + String key = buildKey(regionId); + Object value = redisTemplateHelper.hGet(key, adId); + if (value == null) { + return null; + } + return JSONUtil.toBean(value.toString(), MallAdPosition.class); + } catch (Exception e) { + log.info("获取单个广告位缓存失败: regionId={}, adId={}", regionId, adId, e); + return null; + } + } + + /** + * 判断该区域的广告位缓存是否已存在 + * + * @param regionId 区域ID + * @return true 表示缓存存在 + */ + public boolean exists(String regionId) { + if (StringUtils.isBlank(regionId)) { + return false; + } + Boolean hasKey = redisTemplateHelper.hasKey(buildKey(regionId)); + return hasKey != null && hasKey; + } +} diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/CommentMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/CommentMapper.xml index 07ddb901..cd6ddf93 100644 --- a/hiver-modules/hiver-mall/src/main/resources/mapper/CommentMapper.xml +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/CommentMapper.xml @@ -38,10 +38,13 @@ and picture != '' - + and has_answer = #{comment.hasAnswer,jdbcType=Integer} - + + and has_answer is null + + and score 4 order by create_time desc diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/MallAdPositionMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/MallAdPositionMapper.xml new file mode 100644 index 00000000..84bf355c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/MallAdPositionMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + id, position, merchant_name, merchant_id, merchant_image, + ad_image, title, link_url, link_params, region_id, + sort_order, status, create_time, update_time + + + + + + + diff --git a/hiver-modules/hiver-mall/src/main/resources/mapper/MallAdProductMapper.xml b/hiver-modules/hiver-mall/src/main/resources/mapper/MallAdProductMapper.xml new file mode 100644 index 00000000..dc0e22ef --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/resources/mapper/MallAdProductMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + id, ad_id, product_image, product_name, original_price, + group_price, group_members, sort_order, create_time, update_time + + + + + + + + DELETE FROM t_mall_ad_product WHERE ad_id = #{adId} + + +