# 脱离 GitHub 与 Pages 的 Java/C# 自托管方案 本文是给公司 Java/C# 后端、运维和测试人员的交接说明。GitHub Actions 和 GitHub Pages 只是当前演示环境,不是系统运行时依赖。生产环境可以完全不使用 GitHub、Pages、Python 常驻进程、数据库或大模型服务。 ## 一、先区分两个节点 不要让对外 Web 服务器负责抓取外网: ```text 更新节点(可访问外网) -> 抓取官方源、解析、去重、翻译、校验 -> 生成 JSON、HTML、Sitemap、JSON-LD、robots.txt、llms.txt -> 生成完整 dist/ 发布包 -> 原子推送到 Web 节点 Web 节点(只提供静态文件) -> IIS / Nginx / 对象存储 / CDN -> 不运行 Python、Java、C#、Node、数据库或模型 ``` 更新节点可以是公司内网 Windows Server、Linux Server、公司 CI Runner 或一台具备外网出口的云主机。Web 节点即使完全不能访问外网也没有问题。 ## 二、时间字段不能混用 - `generated_at`:本次 JSON 生成完成的 UTC 时刻,例如 `2026-08-17T12:30:21Z`。页面“更新时间”应展示它,并按访问者时区格式化。 - `as_of_date`:业务判断日期,当前按 `Asia/Shanghai` 计算,例如 `2026-08-17`。它用于日历过滤、备货窗口和“今日必看”,不是精确更新时间。 - `verified_at` / `checked_at`:单条来源最近一次核验时间。 更新程序禁止把 `generated_at` 固定为当天 `00:00:00Z`,也不能用 `as_of_date` 代替它。生成失败时保留上一版完整 JSON,不能把时间改新但把内容留空。 ### GitHub Actions 成功不等于线上版本成功 资源检查绿色只代表 `dist/` 可以构建,不代表 Cloudflare Pages、IIS 或 Nginx 已经切到这次制品。发布后必须核对公开 JSON 的 `generated_at`、`as_of_date` 和本次构建日志;本仓库提供 `scripts/verify_public_snapshot.py`,可在 Gitea Runner 或 Windows/Linux 更新节点复用。 ## 三、Java/C# 要复刻的最小流水线 ### 独立站实际命令 ```text python scripts/update_all.py --site-url https://intel.example.com ``` `update_all.py` 会先抓取、清洗、翻译并生成公开 JSON,再调用 `build_site.py` 生成静态 SEO 页面;稳定版可以用 Java `ScheduledExecutorService` 或 .NET `BackgroundService` 编排同样的阶段,但不要让 Web 服务器承担抓取任务。 ### 1. 抓取层 - Java:`java.net.http.HttpClient`、OkHttp 或公司统一 HTTP 客户端。 - C#:`HttpClientFactory` + `HttpClient`。 - 每个源独立超时、有限重试、User-Agent、响应大小上限和内容类型检查。 - HTML 解析使用 Java Jsoup 或 C# AngleSharp;JSON 使用 Jackson 或 `System.Text.Json`。 - 记录最终 URL、HTTP 状态、抓取时间、正文哈希、证据词和错误原因。 ### 2. 规范化与去重层 必须保留以下字段语义: ```text stable id source_id / source / authority url / source_url title / title_original / title_english source_language published_at / verified_at content_hash start_date / end_date / as_of_date needs_translation / translation_url / translation_fallback_url ``` 日历至少按“规范化节日名 + 日期 + 国家/地区 + 来源类型”去重;跨天假期合并为一张卡,在 `related_dates` 或 `date_variants` 保存不同国家日期。不要用标题模糊相似度把不同国家的“独立日”“国庆日”强行合并。 ### 3. 翻译层 当前项目没有 LLM、多模态理解、生图或提示词链路。翻译只是普通 HTTP 适配器: ```text 原文 -> 内容指纹缓存 -> Google 公共翻译(可用时) -> Yandex 网页翻译入口(Google 不可用时) -> 保留上一版中文或原文 ``` 中文官网、中文报名页、中文平台页不生成翻译按钮。CPSC、ZATCA 等 Google 不稳定域名可以保留 Yandex 回退;两边都不支持的域名隐藏按钮。翻译失败不得清空已验证的中文内容。 ### 4. 校验与发布层 在替换正式目录前必须检查: - 权威源成功数等于目标数;核心栏目非空。 - JSON 可解析、数据结构版本正确、`generated_at` 是合法 UTC ISO 8601。 - 所有公开 URL 为 `http/https`,没有内部缓存、日志、代理凭据或抓取状态文件。 - `sitemap.xml`、canonical、JSON-LD、robots.txt 和 `llms.txt` 与本次 `site_url` 一致。 - 任何一个关键步骤失败,都不替换旧版 `dist/`。 ## 四、中国内地网络的三种部署方案 ### 方案 A:公司外网更新节点,内网 Web 节点 更新节点放在具备合规外网出口的云主机或公司代理网络中。它运行定时任务,成功后把 `dist/` 和公开 JSON 作为制品上传到内网 Web 节点。Web 节点只需要访问内网制品仓库,不需要 VPN。 适合:抓取源较多、公司有统一代理或云主机、希望 Web 服务器最稳定的情况。 ### 方案 B:公司 Gitea + Runner,不使用 Pages 1. 代码放在公司 Gitea。 2. 在有外网出口的 Windows/Linux 机器安装 Gitea Runner。 3. Runner 执行更新、测试、构建和制品上传。 4. IIS/Nginx 服务器只接收成功的 `dist/` 压缩包并原子解压切换。 Gitea 只负责代码和流水线,不要求启用 Pages。Runner 可以部署在外网可访问区,Web 服务器部署在内网。 ### 方案 C:公司 GitHub 账号 + 自托管 Runner 可以使用公司 GitHub 账号保存代码,但不要依赖 Pages。自托管 Runner 在具备外网访问能力的服务器上执行: ```text git checkout 更新数据 运行测试 构建 dist/ 上传制品 部署到 IIS/Nginx ``` GitHub 只作为 Git 和 CI 控制面;真正的静态文件部署到公司服务器、对象存储或 CDN。 ## 五、没有 VPN 时怎么办 不要把“所有服务器必须安装 VPN”当成唯一方案,按网络边界选择: - **HTTPS 代理**:为更新节点配置 `HTTP_PROXY/HTTPS_PROXY`,代理凭据放在系统密钥库或 CI Secret,不写入仓库。 - **统一出口网关**:由网络团队提供固定出口、域名白名单和审计日志,应用只访问允许的官方域名。 - **远程更新 Worker**:在可访问外网的云主机执行抓取,向内网制品仓库上传构建包;内网服务器只下载制品。 - **分源策略**:国内可直连源由内网更新;境外源交给外网 Worker;两边通过 JSON 制品合并。 - **人工制品交接**:极端情况下由外网机器生成带 SHA-256 的 `dist.zip`,通过公司文件系统或制品库转入内网,部署端验证哈希后发布。 抓取节点无法访问某个源时,应保留上一版条目并记录源失败;不能因为单个国外站点不可达而清空整站数据。 ## 六、Windows Server 部署 ### 更新任务 - 使用 Windows Task Scheduler 或 Windows Service/Worker Service,每日固定时间运行 `update_all`。 - 工作目录使用独立服务账号;代理、翻译和 Git 凭据使用系统环境变量或凭据管理器。 - 更新过程先写 `runtime/update-*` 临时目录,成功构建后再替换正式目录。 ### IIS 静态发布 推荐目录结构: ```text D:\sites\sansheng\releases\20260817-123021\dist\ D:\sites\sansheng\current -> 当前发布目录 ``` 部署步骤:解压到新版本目录、验证 `index.html/data/*.json/sitemap.xml`、切换 IIS 站点物理路径或目录联接、保留最近 3-5 个版本。不要在 IIS 正在读取时直接覆盖正在服务的 `dist/`。 ## 七、Linux Server 部署 - 使用 `systemd timer`、容器 CronJob 或公司 CI 调度更新节点。 - Web 节点使用 Nginx,静态根目录指向版本目录;通过软链接原子切换 `current`。 - 用 `rsync --delete` 或制品仓库同步完整目录,不要只复制部分 JSON。 - 发布后执行 HTTP 冒烟检查:首页、JSON、Sitemap、robots、一个详情页和一个图片资源。 - 保留最近 3-5 个版本目录,回滚只需把软链接切回上一版本并 reload Nginx。 ## 八、Git 与无 Git 运行方式 生产更新不要求服务器执行 `git pull`。推荐顺序: 1. GitHub/Gitea/公司 Git 只保存源代码。 2. Runner 或更新服务构建不可变制品 `dist-YYYYMMDD-HHMMSS.zip`。 3. 制品库保存 SHA-256、构建日志和版本号。 4. Web 节点只接收已验证制品。 如果公司不允许生产服务器连接 Git,使用制品上传即可;如果公司需要审计,保留提交 SHA、数据 `generated_at`、构建日志和部署人。 ## 九、回滚与故障处理 - 抓取失败:保留上一版 JSON 和 HTML,标记源失败,不发布半成品。 - 翻译服务失败:保留上一版中文,仍提供官方原文链接。 - 构建失败:不切换 `current`,继续服务上一版。 - 发布后发现问题:切回上一版本目录,随后用固定快照重建。 - 代码回滚必须连同公开 JSON、翻译缓存契约和前端构建结果一起回滚,避免字段版本不匹配。 ## 十、交接验收 Java/C# 重构完成后至少通过: ```text 同一份输入快照 -> 输出 JSON 字段和数量稳定 相同内容指纹 -> 不重复翻译 单源失败 -> 旧内容保留 构建失败 -> dist 不替换 Windows/Linux -> 可定时运行、可查看日志、可回滚 无 VPN Web 节点 -> 仍能提供完整静态站 generated_at -> 精确 UTC 时刻,不是当天 00:00 的占位值 as_of_date -> 按 Asia/Shanghai 的业务日期计算 ```