import java.util.function.Supplier; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; // ───────────────────────────────────────────── // Command 接口 // ───────────────────────────────────────────── interface Command { String getName(); void execute(String[] args, List
articles); } // ───────────────────────────────────────────── // HelpCommand // ───────────────────────────────────────────── class HelpCommand implements Command { private final ConsoleView view; HelpCommand(ConsoleView v) { this.view = v; } @Override public String getName() { return "help"; } @Override public void execute(String[] args, List
articles) { view.printInfo("可用命令:"); view.printInfo(" crawl 抓取 URL(别名: c)"); view.printInfo(" list 列出所有文章"); view.printInfo(" analyze 策略命中统计"); view.printInfo(" history 历史命令记录"); view.printInfo(" help 显示帮助"); view.printInfo(" exit 退出"); } } // ───────────────────────────────────────────── // ListCommand // ───────────────────────────────────────────── class ListCommand implements Command { private final ConsoleView view; private final ArticleRepository repo; ListCommand(ConsoleView v, ArticleRepository r) { view = v; repo = r; } @Override public String getName() { return "list"; } @Override public void execute(String[] args, List
ignored) { view.display(repo.getAll()); } } // ───────────────────────────────────────────── // CrawlCommand(W11升级:异常体系 + 指数退避 + 断路器) // ───────────────────────────────────────────── class CrawlCommand implements Command { private final ConsoleView view; private final ArticleRepository repo; private final StrategySelector selector; // 选做:断路器,连续失败3次熔断,冷却10秒 private final CircuitBreaker breaker = new CircuitBreaker(3, 10_000); private static final Pattern URL_PAT = Pattern.compile("^https?://[^\\s/$.?#].[^\\s]*$"); CrawlCommand(ConsoleView v, ArticleRepository r, StrategySelector s) { view = v; repo = r; selector = s; } @Override public String getName() { return "crawl"; } public String getAlias() { return "c"; } @Override public void execute(String[] args, List
ignored) { if (args.length < 2 || args[1].isBlank()) { view.printError("用法:crawl "); return; } String url = args[1].trim(); // 必做1:URL 格式校验改为抛出 UrlFormatException(Unchecked) try { validateUrl(url); } catch (UrlFormatException e) { view.printError(e.getMessage()); return; } if (repo.findByUrl(url) != null) { view.printWarning("已抓取过,跳过:" + url); return; } ParseStrategy strategy = selector.select(url); if (strategy == null) { view.printError("无可用策略:" + url); return; } // 必做2:指数退避重试 + 选做:断路器保护 final ParseStrategy finalStrategy = strategy; boolean success = RetryUtils.retryWithFastFail(() -> breaker.call(() -> { Article article = finalStrategy.parse(url); repo.add(article); view.printSuccess("已抓取 [" + finalStrategy.getName() + "]:" + article.getTitle()); return true; }), 3 ); if (!success) { view.printError("抓取失败(已重试/熔断):" + url); if (breaker.getState() == CircuitBreaker.State.OPEN) { view.printWarning("断路器已熔断,请稍后再试。"); } } } /** 必做1:集中校验,校验失败抛 UrlFormatException */ private void validateUrl(String url) { if (!URL_PAT.matcher(url).matches()) { throw new UrlFormatException(url); } } } // ───────────────────────────────────────────── // AnalyzeCommand // ───────────────────────────────────────────── class AnalyzeCommand implements Command { private final ConsoleView view; private final ArticleRepository repo; private final StrategySelector selector; AnalyzeCommand(ConsoleView v, ArticleRepository r, StrategySelector s) { view = v; repo = r; selector = s; } @Override public String getName() { return "analyze"; } @Override public void execute(String[] args, List
ignored) { List
all = repo.getAll(); if (all.isEmpty()) { view.printWarning("暂无文章。"); return; } Map hits = new HashMap<>(); int unmatched = 0; for (Article a : all) { ParseStrategy s = selector.select(a.getUrl()); if (s == null) unmatched++; else hits.merge(s.getName(), 1, Integer::sum); } view.printBold("── 策略分析报告(共 " + all.size() + " 篇)──"); for (ParseStrategy s : selector.getAll()) { view.printInfo(String.format(" %-25s priority=%-4d 命中 %d 篇", s.getName(), s.getPriority(), hits.getOrDefault(s.getName(), 0))); } if (unmatched > 0) view.printWarning("无策略匹配:" + unmatched + " 篇"); } } // ───────────────────────────────────────────── // HistoryCommand // ───────────────────────────────────────────── class HistoryCommand implements Command { private final ConsoleView view; private final List history; HistoryCommand(ConsoleView v, List h) { view = v; history = h; } @Override public String getName() { return "history"; } @Override public void execute(String[] args, List
ignored) { if (history.isEmpty()) { view.printInfo("暂无历史记录。"); return; } view.printBold("── 历史命令(共 " + history.size() + " 条)──"); for (int i = 0; i < history.size(); i++) view.printInfo(String.format("%3d %s", i + 1, history.get(i))); } } // ───────────────────────────────────────────── // ExitCommand // ───────────────────────────────────────────── class ExitCommand implements Command { private final ConsoleView view; ExitCommand(ConsoleView v) { this.view = v; } @Override public String getName() { return "exit"; } @Override public void execute(String[] args, List
ignored) { view.printSuccess("Bye!"); System.exit(0); } }