142 lines
6.6 KiB
Java
142 lines
6.6 KiB
Java
package com.yunhai.cms.service.impl;
|
||
|
||
import com.yunhai.cms.entity.CmsBanner;
|
||
import com.yunhai.cms.entity.CmsSite;
|
||
import com.yunhai.cms.enums.LinkTypeEnum;
|
||
import com.yunhai.cms.event.SiteContentChangedEvent;
|
||
import com.yunhai.cms.model.dto.BannerSaveRequest;
|
||
import com.yunhai.cms.model.dto.DeleteRequest;
|
||
import com.yunhai.cms.model.vo.BannerVo;
|
||
import com.yunhai.cms.repository.CmsArticleRepository;
|
||
import com.yunhai.cms.repository.CmsBannerRepository;
|
||
import com.yunhai.cms.repository.CmsCategoryRepository;
|
||
import com.yunhai.platform.apistd.BizException;
|
||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||
import com.yunhai.platform.utils.model.BoaUser;
|
||
import lombok.RequiredArgsConstructor;
|
||
import org.springframework.context.ApplicationEventPublisher;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
import org.springframework.util.StringUtils;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
|
||
/**
|
||
* 轮播/推荐位管理(方案 §9.1 轮播管理)。保存时校验 link_type/link_value 匹配。
|
||
*/
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class BannerService {
|
||
|
||
private final CmsBannerRepository bannerRepository;
|
||
private final CmsArticleRepository articleRepository;
|
||
private final CmsCategoryRepository categoryRepository;
|
||
private final SiteService siteService;
|
||
private final ApplicationEventPublisher eventPublisher;
|
||
private final AttachmentService attachmentService;
|
||
|
||
public List<BannerVo> list(BoaUser user, Integer siteId, String position) {
|
||
siteService.requireManageable(user, siteId);
|
||
List<CmsBanner> banners = StringUtils.hasText(position)
|
||
? bannerRepository.findBySiteIdAndPositionOrderBySortNoAscIdAsc(siteId, position)
|
||
: bannerRepository.findBySiteIdOrderByPositionAscSortNoAscIdAsc(siteId);
|
||
List<BannerVo> result = new ArrayList<>();
|
||
for (CmsBanner b : banners) {
|
||
result.add(BannerVo.of(b));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
@Transactional
|
||
public BannerVo save(BoaUser user, BannerSaveRequest req) {
|
||
CmsSite site = siteService.requireManageable(user, req.getSiteId());
|
||
LinkTypeEnum linkType = LinkTypeEnum.ofCode(req.getLinkType());
|
||
if (linkType == null) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "跳转类型非法:" + req.getLinkType() + "(仅 1=文章 2=栏目 3=外链)");
|
||
}
|
||
validateLink(site.getId(), linkType, req.getLinkValue());
|
||
|
||
CmsBanner banner;
|
||
if (req.getId() == null) {
|
||
banner = new CmsBanner();
|
||
banner.setSiteId(site.getId());
|
||
} else {
|
||
banner = bannerRepository.findById(req.getId())
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "轮播不存在"));
|
||
if (!banner.getSiteId().equals(site.getId())) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "无权修改该轮播");
|
||
}
|
||
}
|
||
banner.setPosition(StringUtils.hasText(req.getPosition()) ? req.getPosition() : "home");
|
||
banner.setTitle(req.getTitle());
|
||
banner.setImageUrl(req.getImageUrl());
|
||
banner.setLinkType(linkType.getCode());
|
||
banner.setLinkValue(req.getLinkValue());
|
||
banner.setSortNo(req.getSortNo() == null ? 0 : req.getSortNo());
|
||
banner.setStartTime(req.getStartTime());
|
||
banner.setEndTime(req.getEndTime());
|
||
if (banner.getStartTime() != null && banner.getEndTime() != null
|
||
&& banner.getEndTime().isBefore(banner.getStartTime())) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "下线时间不能早于上线时间");
|
||
}
|
||
banner.setStatus(req.getStatus() == null ? 1 : req.getStatus());
|
||
// docs/20 D-3/D-6:topic/event 位扩展字段透传(其余位一并落库,渲染端按位消费)
|
||
banner.setDescription(req.getDescription());
|
||
banner.setCtaText(req.getCtaText());
|
||
banner.setCtaUrl(req.getCtaUrl());
|
||
banner.setEventDate(req.getEventDate());
|
||
banner.setLocation(req.getLocation());
|
||
banner = bannerRepository.save(banner);
|
||
// B2 附件引用:先解引用再回写新图(编辑换图时旧图回未使用);幂等
|
||
attachmentService.release("banner", banner.getId());
|
||
attachmentService.markUsed(site.getId(), req.getImageUrl(), "banner", banner.getId());
|
||
eventPublisher.publishEvent(new SiteContentChangedEvent(site.getId(), "banner"));
|
||
return BannerVo.of(banner);
|
||
}
|
||
|
||
@Transactional
|
||
public void delete(BoaUser user, DeleteRequest req) {
|
||
CmsSite site = siteService.requireManageable(user, req.getSiteId());
|
||
CmsBanner banner = bannerRepository.findById(req.getId())
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "轮播不存在"));
|
||
if (!banner.getSiteId().equals(site.getId())) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "无权删除该轮播");
|
||
}
|
||
bannerRepository.delete(banner);
|
||
attachmentService.release("banner", banner.getId());
|
||
eventPublisher.publishEvent(new SiteContentChangedEvent(site.getId(), "banner"));
|
||
}
|
||
|
||
/**
|
||
* link_type/link_value 匹配校验:1=文章 2=栏目(须存在且属于本站,禁止跨站引用)
|
||
* 3=外链(必须 http/https 开头)。
|
||
*/
|
||
private void validateLink(Integer siteId, LinkTypeEnum linkType, String linkValue) {
|
||
if (linkType == LinkTypeEnum.EXTERNAL) {
|
||
if (!StringUtils.hasText(linkValue)
|
||
|| !(linkValue.startsWith("http://") || linkValue.startsWith("https://"))) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "外链地址必须以 http(s):// 开头");
|
||
}
|
||
return;
|
||
}
|
||
if (!StringUtils.hasText(linkValue)) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "跳转目标不能为空");
|
||
}
|
||
Integer targetId;
|
||
try {
|
||
targetId = Integer.valueOf(linkValue.trim());
|
||
} catch (NumberFormatException e) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "跳转目标必须是数字 ID");
|
||
}
|
||
if (linkType == LinkTypeEnum.ARTICLE) {
|
||
articleRepository.findByIdAndSiteId(targetId, siteId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "跳转文章不存在"));
|
||
} else if (linkType == LinkTypeEnum.CATEGORY) {
|
||
categoryRepository.findById(targetId)
|
||
.filter(c -> c.getSiteId().equals(siteId))
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "跳转栏目不存在"));
|
||
}
|
||
}
|
||
}
|