From aeb0696f457ca85b62d15ab6586c5779cf5cd97b Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Fri, 31 Jul 2026 16:56:21 +0800
Subject: [PATCH] feat(firewall): 添加访问日志功能

---
 src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java |  323 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 323 insertions(+), 0 deletions(-)

diff --git a/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java b/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java
new file mode 100644
index 0000000..af37bcf
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/firewall/service/impl/FirewallRuleServiceImpl.java
@@ -0,0 +1,323 @@
+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.FirewallAccessLog;
+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.FirewallAccessLogMapper;
+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 FirewallAccessLogMapper accessLogMapper;
+    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<FirewallSite> siteList(FirewallSite site, QueryRequest request) {
+        Page<FirewallSite> page = new Page<>(request.getPageNum(), request.getPageSize());
+        LambdaQueryWrapper<FirewallSite> wrapper = new LambdaQueryWrapper<FirewallSite>()
+                .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<FirewallCountryRule>()
+                .eq(FirewallCountryRule::getSiteId, id));
+        // 删除站点关联的IP白名单
+        ipWhiteMapper.delete(new LambdaQueryWrapper<FirewallIpWhite>()
+                .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<FirewallCountryRule> countryRuleList(FirewallCountryRule rule, QueryRequest request) {
+        Page<FirewallCountryRule> page = new Page<>(request.getPageNum(), request.getPageSize());
+        LambdaQueryWrapper<FirewallCountryRule> wrapper = new LambdaQueryWrapper<FirewallCountryRule>()
+                .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<FirewallCountryRule>()
+                    .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<CountryVo> getCountries(Long siteId) {
+        List<FirewallCountryRule> rules = countryRuleMapper.selectList(
+                new LambdaQueryWrapper<FirewallCountryRule>()
+                        .eq(FirewallCountryRule::getSiteId, siteId)
+                        .eq(FirewallCountryRule::getEnabled, 1)
+        );
+
+        List<CountryVo> 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<String> countries) {
+        // 1. 删除旧规则
+        countryRuleMapper.delete(new LambdaQueryWrapper<FirewallCountryRule>()
+                .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<FirewallIpWhite> ipWhiteList(FirewallIpWhite ipWhite, QueryRequest request) {
+        Page<FirewallIpWhite> page = new Page<>(request.getPageNum(), request.getPageSize());
+        LambdaQueryWrapper<FirewallIpWhite> wrapper = new LambdaQueryWrapper<FirewallIpWhite>()
+                .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<FirewallIpWhite>()
+                    .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 配置已更新");
+    }
+
+    // ==================== 访问日志 ====================
+
+    @Override
+    public void recordAccess(FirewallAccessLog accessLog) {
+        accessLogMapper.insert(accessLog);
+    }
+
+    @Override
+    public IPage<FirewallAccessLog> accessLogList(FirewallAccessLog log, QueryRequest request) {
+        Page<FirewallAccessLog> page = new Page<>(request.getPageNum(), request.getPageSize());
+        LambdaQueryWrapper<FirewallAccessLog> wrapper = new LambdaQueryWrapper<FirewallAccessLog>()
+                .eq(log.getSiteId() != null, FirewallAccessLog::getSiteId, log.getSiteId())
+                .eq(log.getIsBlocked() != null, FirewallAccessLog::getIsBlocked, log.getIsBlocked())
+                .eq(StrUtil.isNotBlank(log.getCountryCode()), FirewallAccessLog::getCountryCode, log.getCountryCode())
+                .like(StrUtil.isNotBlank(log.getIp()), FirewallAccessLog::getIp, log.getIp())
+                .orderByDesc(FirewallAccessLog::getCreatedTime);
+        return accessLogMapper.selectPage(page, wrapper);
+    }
+}

--
Gitblit v1.9.1