package cc.mrbird.febs.firewall.service.impl; import cc.mrbird.febs.common.entity.FebsResponse; import cc.mrbird.febs.common.entity.QueryRequest; import cc.mrbird.febs.common.utils.RedisUtils; import cc.mrbird.febs.firewall.dto.CountryVo; import cc.mrbird.febs.firewall.entity.FirewallCountryRule; import cc.mrbird.febs.firewall.entity.FirewallIpWhite; import cc.mrbird.febs.firewall.entity.FirewallSite; import cc.mrbird.febs.firewall.mapper.FirewallCountryRuleMapper; import cc.mrbird.febs.firewall.mapper.FirewallIpWhiteMapper; import cc.mrbird.febs.firewall.mapper.FirewallSiteMapper; import cc.mrbird.febs.firewall.nginx.IpWhitelistNginxConfigGenerator; import cc.mrbird.febs.firewall.nginx.NginxConfigGenerator; import cc.mrbird.febs.firewall.service.IFirewallRuleService; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; /** * 防火墙规则 Service 实现 * * @author auto-generated * @date 2026-07-31 */ @Slf4j @Service @RequiredArgsConstructor public class FirewallRuleServiceImpl implements IFirewallRuleService { private final FirewallCountryRuleMapper countryRuleMapper; private final FirewallIpWhiteMapper ipWhiteMapper; private final FirewallSiteMapper siteMapper; private final NginxConfigGenerator generator; private final IpWhitelistNginxConfigGenerator ipWhitelistGenerator; private final RedisUtils redisUtils; /** Redis 中 Nginx reload 任务队列 key */ private static final String NGINX_RELOAD_QUEUE_KEY = "firewall:nginx_reload_queue"; // ==================== 站点管理 ==================== @Override public IPage siteList(FirewallSite site, QueryRequest request) { Page page = new Page<>(request.getPageNum(), request.getPageSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .like(StrUtil.isNotBlank(site.getSiteName()), FirewallSite::getSiteName, site.getSiteName()) .like(StrUtil.isNotBlank(site.getDomain()), FirewallSite::getDomain, site.getDomain()) .eq(site.getStatus() != null, FirewallSite::getStatus, site.getStatus()) .orderByDesc(FirewallSite::getCreatedTime); return siteMapper.selectPage(page, wrapper); } @Override @Transactional(rollbackFor = Exception.class) public FebsResponse saveSite(FirewallSite site) { if (site.getId() != null) { // 更新 FirewallSite exist = siteMapper.selectById(site.getId()); if (exist == null) { return new FebsResponse().fail().message("站点不存在"); } siteMapper.updateById(site); log.info("防火墙站点已更新: id={}, name={}", site.getId(), site.getSiteName()); } else { // 新增 siteMapper.insert(site); // 回填 nginxConf(如果没填则用默认值) if (StrUtil.isBlank(site.getNginxConf())) { site.setNginxConf("/etc/nginx/conf.d/firewall_country_" + site.getId() + ".conf"); siteMapper.updateById(site); } log.info("防火墙站点已新增: id={}, name={}", site.getId(), site.getSiteName()); } return new FebsResponse().success().message("保存成功"); } @Override @Transactional(rollbackFor = Exception.class) public FebsResponse deleteSite(Long id) { FirewallSite site = siteMapper.selectById(id); if (site == null) { return new FebsResponse().fail().message("站点不存在"); } // 删除站点关联的国家规则 countryRuleMapper.delete(new LambdaQueryWrapper() .eq(FirewallCountryRule::getSiteId, id)); // 删除站点关联的IP白名单 ipWhiteMapper.delete(new LambdaQueryWrapper() .eq(FirewallIpWhite::getSiteId, id)); // 删除Nginx配置文件(国家黑名单 + IP白名单) if (StrUtil.isNotBlank(site.getNginxConf())) { cn.hutool.core.io.FileUtil.del(site.getNginxConf()); } cn.hutool.core.io.FileUtil.del(IpWhitelistNginxConfigGenerator.getConfPath(id)); siteMapper.deleteById(id); log.info("防火墙站点已删除: id={}, name={}", id, site.getSiteName()); return new FebsResponse().success().message("删除成功"); } // ==================== 国家规则管理 ==================== @Override public IPage countryRuleList(FirewallCountryRule rule, QueryRequest request) { Page page = new Page<>(request.getPageNum(), request.getPageSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(rule.getSiteId() != null, FirewallCountryRule::getSiteId, rule.getSiteId()) .eq(rule.getEnabled() != null, FirewallCountryRule::getEnabled, rule.getEnabled()) .like(StrUtil.isNotBlank(rule.getCountryName()), FirewallCountryRule::getCountryName, rule.getCountryName()) .orderByDesc(FirewallCountryRule::getCreatedTime); return countryRuleMapper.selectPage(page, wrapper); } @Override @Transactional(rollbackFor = Exception.class) public FebsResponse saveCountryRuleSingle(FirewallCountryRule rule) { if (rule.getSiteId() == null) { return new FebsResponse().fail().message("请选择站点"); } if (StrUtil.isBlank(rule.getCountryCode())) { return new FebsResponse().fail().message("国家编码不能为空"); } rule.setCountryCode(rule.getCountryCode().toUpperCase()); if (rule.getId() != null) { countryRuleMapper.updateById(rule); log.info("防火墙国家规则已更新: id={}, siteId={}, country={}", rule.getId(), rule.getSiteId(), rule.getCountryCode()); } else { // 检查同站点下是否已存在相同国家编码 Integer count = countryRuleMapper.selectCount(new LambdaQueryWrapper() .eq(FirewallCountryRule::getSiteId, rule.getSiteId()) .eq(FirewallCountryRule::getCountryCode, rule.getCountryCode())); if (count > 0) { return new FebsResponse().fail().message("该站点下已存在相同国家编码: " + rule.getCountryCode()); } countryRuleMapper.insert(rule); log.info("防火墙国家规则已新增: siteId={}, country={}", rule.getSiteId(), rule.getCountryCode()); } // 保存后自动生成Nginx配置并发送reload信号 regenerateNginxConfig(rule.getSiteId()); return new FebsResponse().success().message("保存成功,Nginx 配置已更新"); } @Override @Transactional(rollbackFor = Exception.class) public FebsResponse deleteCountryRule(Long id) { FirewallCountryRule rule = countryRuleMapper.selectById(id); if (rule == null) { return new FebsResponse().fail().message("规则不存在"); } Long siteId = rule.getSiteId(); countryRuleMapper.deleteById(id); log.info("防火墙国家规则已删除: id={}, siteId={}, country={}", id, siteId, rule.getCountryCode()); // 删除后自动生成Nginx配置并发送reload信号 regenerateNginxConfig(siteId); return new FebsResponse().success().message("删除成功,Nginx 配置已更新"); } // ==================== 内部方法 ==================== /** 重新生成Nginx配置并发送reload信号 */ private void regenerateNginxConfig(Long siteId) { try { generator.generate(siteId); ipWhitelistGenerator.generate(siteId); redisUtils.lSet(NGINX_RELOAD_QUEUE_KEY, String.valueOf(siteId)); log.info("Nginx reload 信号已入队: siteId={}", siteId); } catch (Exception e) { log.error("Nginx 配置生成失败: siteId={}", siteId, e); } } // ==================== 原有方法 ==================== @Override public List getCountries(Long siteId) { List rules = countryRuleMapper.selectList( new LambdaQueryWrapper() .eq(FirewallCountryRule::getSiteId, siteId) .eq(FirewallCountryRule::getEnabled, 1) ); List result = new ArrayList<>(); for (FirewallCountryRule rule : rules) { CountryVo vo = new CountryVo(); vo.setCode(rule.getCountryCode()); vo.setName(rule.getCountryName()); result.add(vo); } return result; } @Override @Transactional(rollbackFor = Exception.class) public void saveCountryRule(Long siteId, List countries) { // 1. 删除旧规则 countryRuleMapper.delete(new LambdaQueryWrapper() .eq(FirewallCountryRule::getSiteId, siteId)); // 2. 批量插入新规则 for (String country : countries) { FirewallCountryRule rule = new FirewallCountryRule(); rule.setSiteId(siteId); rule.setCountryCode(country.toUpperCase()); rule.setCountryName(country); rule.setEnabled(1); countryRuleMapper.insert(rule); } log.info("防火墙国家规则已更新: siteId={}, countries={}", siteId, countries); // 3. 生成 Nginx 配置文件 generator.generate(siteId); ipWhitelistGenerator.generate(siteId); // 4. 发送 reload 信号 redisUtils.lSet(NGINX_RELOAD_QUEUE_KEY, String.valueOf(siteId)); log.info("Nginx reload 信号已入队: siteId={}", siteId); } @Override public void triggerNginxReload(Long siteId) { generator.generate(siteId); ipWhitelistGenerator.generate(siteId); redisUtils.lSet(NGINX_RELOAD_QUEUE_KEY, String.valueOf(siteId)); log.info("手动触发 Nginx reload: siteId={}", siteId); } // ==================== IP白名单管理 ==================== @Override public IPage ipWhiteList(FirewallIpWhite ipWhite, QueryRequest request) { Page page = new Page<>(request.getPageNum(), request.getPageSize()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(ipWhite.getSiteId() != null, FirewallIpWhite::getSiteId, ipWhite.getSiteId()) .eq(ipWhite.getEnabled() != null, FirewallIpWhite::getEnabled, ipWhite.getEnabled()) .like(StrUtil.isNotBlank(ipWhite.getIp()), FirewallIpWhite::getIp, ipWhite.getIp()) .like(StrUtil.isNotBlank(ipWhite.getRemark()), FirewallIpWhite::getRemark, ipWhite.getRemark()) .orderByDesc(FirewallIpWhite::getCreatedTime); return ipWhiteMapper.selectPage(page, wrapper); } @Override @Transactional(rollbackFor = Exception.class) public FebsResponse saveIpWhite(FirewallIpWhite ipWhite) { if (ipWhite.getSiteId() == null) { return new FebsResponse().fail().message("请选择站点"); } if (StrUtil.isBlank(ipWhite.getIp())) { return new FebsResponse().fail().message("IP地址不能为空"); } ipWhite.setIp(ipWhite.getIp().trim()); if (ipWhite.getId() != null) { ipWhiteMapper.updateById(ipWhite); log.info("防火墙IP白名单已更新: id={}, siteId={}, ip={}", ipWhite.getId(), ipWhite.getSiteId(), ipWhite.getIp()); } else { // 检查同站点下是否已存在相同IP Integer count = ipWhiteMapper.selectCount(new LambdaQueryWrapper() .eq(FirewallIpWhite::getSiteId, ipWhite.getSiteId()) .eq(FirewallIpWhite::getIp, ipWhite.getIp())); if (count > 0) { return new FebsResponse().fail().message("该站点下已存在相同IP: " + ipWhite.getIp()); } ipWhiteMapper.insert(ipWhite); log.info("防火墙IP白名单已新增: siteId={}, ip={}", ipWhite.getSiteId(), ipWhite.getIp()); } // 保存后自动生成Nginx配置并发送reload信号 ipWhitelistGenerator.generate(ipWhite.getSiteId()); redisUtils.lSet(NGINX_RELOAD_QUEUE_KEY, String.valueOf(ipWhite.getSiteId())); return new FebsResponse().success().message("保存成功,Nginx 配置已更新"); } @Override @Transactional(rollbackFor = Exception.class) public FebsResponse deleteIpWhite(Long id) { FirewallIpWhite ipWhite = ipWhiteMapper.selectById(id); if (ipWhite == null) { return new FebsResponse().fail().message("IP白名单不存在"); } Long siteId = ipWhite.getSiteId(); ipWhiteMapper.deleteById(id); log.info("防火墙IP白名单已删除: id={}, siteId={}, ip={}", id, siteId, ipWhite.getIp()); // 删除后自动生成Nginx配置并发送reload信号 ipWhitelistGenerator.generate(siteId); redisUtils.lSet(NGINX_RELOAD_QUEUE_KEY, String.valueOf(siteId)); return new FebsResponse().success().message("删除成功,Nginx 配置已更新"); } }