07_web · M6.1 最小可发布部署方案
07_web · M6.1 最小可发布部署方案
摘要
本方案定义 Xisound 官网 M6.1 阶段的最小可发布架构与实施方案:官网保持纯静态(Cloudflare Pages 免备案上线),同级新建独立 xisound-api/ 仓库承载后端(ASP.NET Core 8 + EF Core + SQLite/PostgreSQL 双栈 + Docker),本地端到端跑通留资闭环(表单提交 → SQLite 存储 → 企微推送 → 邮件通知 · 空凭据降级日志)。
v2.0 为已交付定稿版,反映实际落地状态(63706c9 后端首提交 + 32b3b40 前端 LeadForm fetch + 6/6 端到端验证通过)。v1.0 对比版已归档至 07web-m6.1-deploy-plan-v1.0-archive.md 留存决策脉络。
本文档与既有规划的关系
AlgoDepartment/07_web/CLAUDE.md· 规划 M6 完整里程碑(M6.1 已标记 ✅ + M6.2/M6.3/M6.4 拆分待启)AlgoDepartment/07_web/doc/M6-deployment-guide.md· 本地开发 agent 副本(顶部 banner 指向本文档作为权威)AlgoDepartment/06_docs/官网部署.md· M6 战略级建议书 v1.0(4 阶段路线图)- 归档:
07web-m6.1-deploy-plan-v1.0-archive.md· v1.0 对比版(待拍板阶段的决策对比表)
1. 现状与目标
1.1 M6.1 交付现状(2026-05-06)
| 维度 | 交付状态 |
|---|---|
| 前端官网 | 纯静态保持(不引入 Astro adapter)· 35 页 build · commit 32b3b40 |
| 后端仓库 | 独立 xisound-api/(ASP.NET Core 8 · 19 文件 · 1501 行)· root commit 63706c9 |
| 数据库 | SQLite(./data/app.db · EF Core · M6.2 切 PostgreSQL 一行配置) |
| 邮件 | Resend(M4→M1 · 空 RESEND_API_KEY 时降级日志) |
| 企微 | 群机器人(W2 · 空 WEWORK_WEBHOOK_URL 时降级日志) |
| CORS | 允许 localhost:4321/4322 + www.joysnd.com + xisound-website.pages.dev |
| 管理端 | GET /admin/leads · X-Admin-Key header 保护 · 分页 + UTM 筛选 |
| 限流 | 内存 ConcurrentDictionary · 5 次/分钟/IP(M6.2 升 Redis) |
| 部署 | Docker Compose(api + postgres/redis prod profile)· VPS 上线延后 M6.2 |
| 端到端验证 | 6/6 通过:dotnet restore + dotnet build + /health 200 + POST 202 + GET admin 200 + no-op 降级日志全中 |
| 文档站(M6.1.1) | 独立仓 mengliliusha/xisound-docs · GitHub Actions 自动 mkdocs build → gh-pages 分支 · CF Pages 发布 docs.joysnd.com · 详见 §5.4 与 docs-site-deployment.md |
1.2 M6.1 目标(一句话)
已达成
本地 dotnet run(后端 :5000)+ npm run dev(前端 :4321)· 打开官网留资表单提交一条线索 · 三个副作用全部跑通:
1. 写入 ./data/app.db(SQLite · leads 表)
2. 推送企微群消息(WEWORK_WEBHOOK_URL 空 → 日志 [WeworkBot-NoOp])
3. 发送运营通知邮件(RESEND_API_KEY 空 → 日志 [ResendEmail-NoOp])
1.3 不在 M6.1 范围(M6.2-M6.4 延后)
| 延后里程碑 | 范围 | 依赖前置 |
|---|---|---|
| M6.2 | 腾讯云香港 VPS 部署 · api.joysnd.com 上线 · PostgreSQL 持久化 · nginx/certbot · GitHub Actions CI/CD · UptimeRobot + Sentry |
腾讯云账号 + 支付方式 + CF DNS api 子域 |
| M6.3 | 账号系统(JWT /api/auth/*)· 删 xisound-website/src/lib/auth.ts 前端假账号 · /api/downloads/list + R2 签名 URL · /admin/leads 升级 JWT + Excel 导出 |
M6.2 完成 |
| M6.4 | Resend 添加 mail.joysnd.com 域名验证(SPF/DKIM/DMARC) · From 换 noreply@joysnd.com · 飞书多维表格同步(可选)· 阿里云 DirectMail 国内双通道(需 ICP 备案) |
Resend 账号 + 域名 DNS 管理权 |
2. 架构总览
2.1 M6.1 交付后的运行拓扑
graph TB
subgraph Static["静态官网层"]
CF[Cloudflare Pages]
W1[www.joysnd.com]
W2[xisound-website.pages.dev]
end
subgraph Backend["后端服务层 · 本地"]
API[dotnet run · :5000]
DB[(SQLite · data/app.db)]
Admin[/admin/leads · X-Admin-Key/]
end
subgraph Downstream["下游通知"]
Resend[Resend 邮件]
Wework[企微群机器人]
end
W1 --> CF
W2 --> CF
CF -->|表单提交 · CORS fetch| API
API -->|EF Core| DB
API -.空 Key 降级日志.-> Resend
API -.空 URL 降级日志.-> Wework
API --> Admin
class CF,W1,W2 xyL4
class API,Admin xyL3
class DB xyL0
class Resend,Wework xyL5
2.2 留资闭环数据流
sequenceDiagram
autonumber
participant User as 访客浏览器
participant Web as xisound-website (L4)
participant Api as xisound-api (L3)
participant Db as SQLite (L0)
participant Ops as 运营通知 (L5)
User->>Web: 填写表单 · 点击提交
Web->>Web: trackLead('submit') 埋点
Web->>Api: POST /api/lead · CORS
Api->>Api: DataAnnotations 校验 + 限流 5/min
Api->>Db: EF Core SaveChanges
Db-->>Api: leadId 返回
Api-->>Web: 202 Accepted · {ok, leadId}
Web->>Web: trackLead('success') 埋点
Web->>User: 切换成功态 · 显示感谢页
Api->>Ops: 异步 Wework 推送 · 空 URL 走日志
Api->>Ops: 异步 Resend 邮件 · 空 Key 走日志
2.3 关键架构决策(与 v1.0 对比版的差异)
| 维度 | v1.0 对比版(初版 A2) | v2.0 定稿(实际交付 S0+S1-code) | 变化原因 |
|---|---|---|---|
| 官网部署 | @astrojs/node hybrid adapter |
Cloudflare Pages 纯静态(不引入 adapter) | 对齐 06_docs/官网部署.md 阶段 0 · 免备案 · 保留 M5.8.1 性能优化成果(JS bundle 5.2KB) |
| 后端技术栈 | Astro endpoint(一栈通 TypeScript) | 独立 ASP.NET Core 8 仓库 | 对齐团队 C# 主栈 · 与 04_development 后端产品线统一 |
| 数据库 driver | better-sqlite3(Node 原生扩展) |
EF Core + Microsoft.Data.Sqlite + Npgsql(双栈兼容) | SQLite/PostgreSQL 零代码切换 · M6.2 升 Postgres 仅改 Database:Provider |
| 仓库结构 | 单仓库(07_web/xisound-website) | 双独立仓库(xisound-website + xisound-api) | 独立 CI/CD · 前后端各自演进 · 对齐 06_docs/官网部署.md §14 表格 |
| 部署目标 | 本地 + 生产二合一 | 本地跑通 + 部署延后 M6.2 | VPS 采购 / 域名 NS 切 / CI/CD 配置是独立路径 · 单轮混做易踩坑 |
3. 前端改造(xisound-website)
3.1 改动清单
xisound-website/
├── .env.example ← 新增 · PUBLIC_API_BASE_URL 等环境变量
└── src/components/resource/
└── LeadFormCapture.astro ← 修改 · setTimeout mock → fetch ${PUBLIC_API_BASE_URL}/api/lead
未改动项(M5.8.1 保留):astro.config.mjs output: 'static' · 0 client:* 指令 · src/lib/auth.ts 保留(M6.3 再删)· src/pages/api/lead.ts static stub 保留(CF Pages 无后端时的 UI 降级)
3.2 LeadFormCapture.astro 核心逻辑
// xisound-website/src/components/resource/LeadFormCapture.astro · <script> 段核心
import { trackLead } from '../../lib/analytics';
const API_BASE = (import.meta.env.PUBLIC_API_BASE_URL ?? '').replace(/\/+$/, '');
// 提交时
try {
if (API_BASE) {
// M6.1 真实后端:POST 到 xisound-api
const res = await fetch(`${API_BASE}/api/lead`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...payload, stage: 'resource-download', consent: Boolean(payload.consent) }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} else {
// 无后端降级:本地 mock(仅 UI 演示)
console.info('[lead/mock] PUBLIC_API_BASE_URL 为空 · 走降级模拟');
await new Promise((resolve) => setTimeout(resolve, 800));
}
trackLead('success', { audience, topic });
} catch (err) {
trackLead('error', { audience, message: err.message });
}
3.3 环境变量(.env.example 摘要)
# M6.1 · 留资 API
PUBLIC_API_BASE_URL=
# 本地:http://localhost:5000 · 生产(M6.2 后):https://api.joysnd.com · 留空则降级 mock
# M7 · 埋点(M6.1 不启用)
PUBLIC_BAIDU_ID=
PUBLIC_GA4_ID=
4. 后端架构(xisound-api)
4.1 项目结构
xisound-api/
├── XiSound.Api.sln
├── Dockerfile # 多阶段构建 · 非 root 用户 · runtime ~220MB
├── docker-compose.yml # api + postgres(prod profile) + redis(prod profile)
├── .env.example # 所有密钥占位
├── .gitignore # 排除 data/ · .env · bin/ · obj/
├── README.md # 5 分钟起步指引
└── src/XiSound.Api/
├── XiSound.Api.csproj # EF Core + Sqlite + Npgsql + Swagger
├── Program.cs # Minimal API bootstrap + CORS + DI
├── appsettings.json # 生产默认值
├── appsettings.Development.json # 本地覆写(AdminKey=dev-key)
├── Properties/launchSettings.json
├── Models/Lead.cs # 留资实体(UTM 扁平化 + 请求元数据)
├── Data/AppDbContext.cs # SQLite/PostgreSQL 双栈
├── Dtos/LeadDto.cs # DataAnnotations 校验 + ToEntity 映射
├── Services/
│ ├── EmailServices.cs # IEmailService + ResendEmailService(空 Key no-op)
│ └── WeworkServices.cs # IWeworkNotifier + WeworkBotNotifier(空 URL no-op)
├── Middleware/AdminKeyMiddleware.cs # /admin/* · X-Admin-Key header 保护
└── Controllers/
├── LeadsController.cs # POST /api/lead · GET /admin/leads · GET /admin/leads/{id}
└── HealthController.cs # GET /health · GET /health/ready
4.2 API 合约
POST /api/lead · 公开入口
{
"stage": "resource-download",
"resourceTitle": "声学设计方案白皮书",
"name": "张三",
"phone": "13800138000",
"email": "zhangsan@example.com",
"company": "某声学公司",
"role": "声学工程师",
"audience": "oem-new",
"topic": "acoustic-design",
"need": "希望了解 XiStudio 的定价",
"consent": true,
"utm": { "source": "baidu", "medium": "cpc", "campaign": "brand" }
}
| 状态码 | 含义 |
|---|---|
202 Accepted |
入库成功 · 异步推送 · { ok, leadId, message } |
400 Bad Request |
字段校验失败 · RFC7807 ValidationProblemDetails |
429 Too Many Requests |
同 IP 超过 5 次/分钟 |
500 Internal Server Error |
服务端异常 |
GET /admin/leads · 管理端(需 X-Admin-Key)
| Query 参数 | 类型 | 说明 |
|---|---|---|
limit |
int | 1-500 · 默认 50 |
offset |
int | 默认 0 |
stage |
string | 按来源阶段筛选 |
status |
string | 按处理状态筛选(pending / notified / contacted / closed) |
utmSource |
string | 按 UTM 来源筛选 |
返回字段按 createdAt DESC 排序 · 含 Id / CreatedAt / Stage / Status / Name / Phone / Email / Company / Role / Audience / Topic / ResourceTitle / Need / Utm × 3 / IpAddress。
GET /health · GET /health/ready
无需鉴权 · 用于 UptimeRobot / Docker healthcheck / K8s probe。
4.3 留资入库流程
// LeadsController.Submit · 核心逻辑
[HttpPost("/api/lead")]
public async Task<IActionResult> Submit([FromBody] LeadDto dto, CancellationToken ct)
{
// 1. 限流
if (!CheckRateLimit(GetClientIp()))
return StatusCode(429, new { ok = false, error = "rate_limited" });
// 2. 业务校验(Phone/Email 至少一项)
if (dto.ValidateContacts() is { } err)
{
ModelState.AddModelError(nameof(dto.Phone), err.ErrorMessage);
return ValidationProblem(ModelState);
}
// 3. 存库 + 填充请求元数据
var lead = dto.ToEntity();
lead.IpAddress = GetClientIp();
lead.UserAgent = Request.Headers.UserAgent.ToString();
_db.Leads.Add(lead);
await _db.SaveChangesAsync(ct);
// 4. 异步推送(不阻塞响应 · 失败不影响主流程)
_ = Task.Run(async () => {
try { await _wework.SendLeadAsync(lead); } catch (Exception ex) { _logger.LogError(ex, "..."); }
try { await _email.SendLeadNotifyAsync(lead); } catch (Exception ex) { _logger.LogError(ex, "..."); }
});
return Accepted(new { ok = true, leadId = lead.Id, message = "已收到您的留资" });
}
5. 部署方案
5.1 阶段 0 · Cloudflare Pages 官网上线(Day 1-3 · 用户手工)
5.1.1 前置条件
- 已有域名
joysnd.com - GitHub 仓库
mengliliusha/xisound-website已 push(M6.1 完成 · 17 次 commit · 最新32b3b40) - Cloudflare 账号(免费 · 没有 → https://dash.cloudflare.com/sign-up 注册)
- 域名注册商登录凭据(用于改 NS)
5.1.2 步骤 1 · 域名 NS 切换到 Cloudflare(A → B → C → D 四步走)
本步骤是阶段 0 最关键、也是最容易踩坑的一步 —— NS 切换本质上是把域名的"解析大权"从原注册商移交给 Cloudflare。执行前请务必看完 §5.1.2.E 的 QQ 企业邮箱保护警告(用户若在用 QQ 企业邮箱 / 阿里企业邮 / 网易企业邮等任何第三方邮箱托管,NS 切换后必须保留原 MX/SPF 记录)。
5.1.2.A · 在 Cloudflare 添加站点并获取 NS 记录
- 登录 Cloudflare Dashboard(
https://dash.cloudflare.com) - 右上角 Add a Site → 输入
joysnd.com→ 点击 Continue - 选择 Free 计划 → Continue
- CF 会自动扫描现有 DNS 记录(含已有的 MX/SPF/TXT 记录)· 完成后展示一个带 两条 NS 记录 的大框,形如:
其中 cora / eric 等前缀是 CF 随机分配的人名占位 · 每个账号获取到的前缀不同,请以 CF 界面实际展示的为准。
找不到 NS 记录? CF 可能把它放在不同位置 · 两种方法都能找到:
- 方法 1(最常见):进入
joysnd.com的 Zone 概览页(Overview)· 页面顶部会有"Cloudflare Nameservers"的大框 + 两条 NS - 方法 2(备选):左侧菜单 DNS → Records → 滚到页面底部(或点 DNS → Settings 标签页)· 也能看到这两条 NS
请把两条 NS 记下来(或保持浏览器 tab 打开)· §5.1.2.B 要用。
5.1.2.B · 在腾讯云域名注册商改 NS
⚠️ 关键认知:这一步不在 Cloudflare 做 · 必须去你买
joysnd.com的那个平台(注册商)操作。 Cloudflare 只是"借用"你的域名做解析 · 域名的所有权依然在腾讯云。
腾讯云操作路径(2024-2026 · 用户实测):
- 登录腾讯云控制台(
https://console.cloud.tencent.com) - 顶部导航 → 云产品 → 域名注册 → 我的域名
- 域名列表中找到
joysnd.com→ 点击右侧 管理 - 左侧菜单 → DNS 服务器 → 点击 修改
- 界面会出现 NS 记录输入框 · 把原有的腾讯云 DNS(通常是
ns1.dnspod.net/ns2.dnspod.net· 或dns1.qcloud.com)完全删除 - 逐行填入 §5.1.2.A 获取的两条 CF NS:
- 点击 确定 / 保存 · 腾讯云可能要求短信验证(发到你注册账号的手机号)· 输入验证码完成
腾讯云"新老版面"差异提示
腾讯云控制台在 2025 年做过 UI 改版 · 有些用户可能看到: - "DNS 修改" 菜单项(旧版名字) - "自定义 DNS" 标签(部分特殊域名) - "DNS 管理" 二级菜单(合并后的入口) 本质都是同一功能 · 只要能看到当前 NS 并能替换即可。
5.1.2.C · 回到 Cloudflare 点"Check nameservers"触发验证
- 在腾讯云改完 NS 后 · 回到 Cloudflare Dashboard 的
joysnd.com概览页(Overview) - 页面上会有一个按钮 · 文案为 Check nameservers 或 检查名称服务器(取决于 CF 界面语言设置)
- 点击它 · CF 会主动查询全球 DNS 系统 · 确认 NS 是否已切换到
cora/eric.ns.cloudflare.com
状态流转:
| 阶段 | 状态文案 | 含义 | 时长 |
|---|---|---|---|
| 刚改完 NS | Pending Nameserver Update ⏳ |
DNS 还没生效 · CF 查不到新 NS | 几分钟 - 24 小时 |
| 生效后 | Active ✅ |
NS 已全球传播 · CF 完全接管解析 | - |
生效后 CF 会给你的注册邮箱发一封邮件:Your site is now active on Cloudflare,这时才可以进入 §5.1.3 创建 CF Pages 项目。
别等"Active"太久
CF 的 Check nameservers 按钮可以反复点 · 每次触发一次查询。 大陆地区 DNS 传播通常 10 分钟 - 2 小时 · 国际线路可能更快。 如果 24 小时后还是 Pending · 请回 §5.1.2.B 检查腾讯云那边 NS 是否真的保存了。
5.1.2.D · 本地 nslookup 验证(推荐但非必需)
在本地 PowerShell / CMD 跑:
生效后的预期输出:
Server: your-dns-server.local
Address: 192.168.x.x
Non-authoritative answer:
joysnd.com nameserver = cora.ns.cloudflare.com
joysnd.com nameserver = eric.ns.cloudflare.com
未生效时可能看到:
说明 DNS 还在全球传播 · 耐心等待 · 或清本地 DNS 缓存:
5.1.2.E · QQ 企业邮箱等第三方邮箱的保护(重要 · 可能踩坑)
切换 NS 不会自动断邮件 · 但错误操作会让邮箱挂掉
如果你已经在用 QQ 企业邮箱 / 阿里企业邮 / 网易企业邮 / Google Workspace 等第三方邮箱托管服务 · joysnd.com 已经有 MX/SPF/TXT 记录在生效。
好消息:CF 在 §5.1.2.A 扫描现有 DNS 时会自动保留这些记录 · NS 切换后邮箱继续能正常收发。
坏消息:如果在 CF DNS 面板手贱点亮了 MX/SPF/TXT 记录的代理云朵(橙色) · 邮箱会立即挂掉(收不到邮件 · 发出去的邮件被退回)· 这是新手最容易踩的坑。
必须遵守的 3 条规则:
- MX 记录(所有
mailto:xxx@joysnd.com投递路径)· 代理状态必须是"仅 DNS"灰色云朵 · ❌ 绝不能点亮橙色云朵 - SPF TXT 记录(
v=spf1 include:spf.mail.qq.com ...)· 代理状态必须是"仅 DNS"灰色云朵 · 同上 - DKIM TXT 记录(
_domainkey.xxx开头)· 代理状态必须是"仅 DNS"灰色云朵 · 同上
原因:CF 代理(橙色云朵)只作用于 HTTP/HTTPS 流量(即 www.joysnd.com 这类网站访问)· MX/SMTP/SPF 是邮件协议 · 走 CF 代理会被过滤导致邮件丢失。
QQ 企业邮箱典型配置(参考 · NS 切换前已存在请勿删除):
| 类型 | 名称 | 内容 | TTL | 代理状态(关键) |
|---|---|---|---|---|
| MX | @(根域) |
mxbiz1.qq.com(优先级 5) |
Auto | 🔘 仅 DNS |
| MX | @(根域) |
mxbiz2.qq.com(优先级 10) |
Auto | 🔘 仅 DNS |
| TXT | @(根域) |
v=spf1 include:spf.mail.qq.com ~all |
Auto | 🔘 仅 DNS |
点亮代理云朵的"撤销"方法
如果不小心把 MX 记录点亮成橙色 · 邮箱会在几秒内挂掉。 补救:CF DNS 面板找到那条记录 · 点击橙色云朵让它变回灰色("仅 DNS")· 邮件服务通常 1-5 分钟内恢复。 如果已经有邮件在此期间丢失 · 一般无法找回(发件方会收到退信 · 可以请他们重发)。
5.1.3 步骤 2 · Cloudflare Pages 创建项目(⚠️ 最容易踩坑的一步)
切勿进错成 Workers 项目 · 这是最常见的陷阱
Cloudflare 在 2024 年 9 月把 Workers 和 Pages 整合到同一菜单"Workers & Pages" · 新版 UI 非常容易点进去就创建了 Workers 项目(而不是 Pages)。两者的区别:
| 维度 | Cloudflare Pages | Cloudflare Workers |
|---|---|---|
| 定位 | 静态站托管 + Git 自动部署 | 边缘计算 + Serverless 函数 |
| 适合 | ✅ Astro / Next / Hugo / 纯 HTML | JS/TS 函数 / API / SSR |
| 构建配置 | Framework preset + Build + Output dir | Wrangler + wrangler.toml |
| 部署命令 | 无(CF 自动部署) | 需要 npx wrangler deploy |
| Zero-config | ✅ 是 | ❌ 否(需配置) |
踩坑识别信号:如果你创建项目后 · 构建配置页出现 "部署命令 (Deploy command)" 或 "版本命令 (Version command)" 或 "兼容性日期 (Compatibility date)" 字段 · 你进错 Workers 了 · 本项目 不需要 wrangler。
补救方案:删除错建的 Workers 项目(Settings → Delete project)· 重新回 Workers & Pages 主页 · 重新选 Pages 标签创建。
正确创建流程(逐步):
- CF Dashboard 左侧菜单 → Workers & Pages
- 点击右上角蓝色 Create application 按钮
- 顶部会出现两个 tab 标签:
┌─────────────────────────────────┐
│ [ Workers ] [ Pages ] │
└─────────────────────────────────┘
↑
本项目务必点这里(默认可能停在 Workers)
- 确认切到 Pages 标签后 · 看到两个选项卡片:
- Connect to Git ← ✅ 点这个(本项目的流程)
- Upload assets ← 不要点(是手动上传压缩包的方式)
- 首次使用需授权 GitHub:
- 点击 Connect GitHub
- 跳转到 GitHub 授权页 → 登录
mengliliusha账号 - 选择授权范围:
- All repositories(授权所有仓库 · 最省事)
- 或 Only select repositories → 勾选
xisound-website(最小权限)
- 点击 Install & Authorize → 自动跳回 CF
- 回到 CF 项目创建页 · 会看到 GitHub 仓库列表 · 选择
mengliliusha/xisound-website→ 点击 Begin setup
关于"Git 存储库 URL"字段
如果界面显示可填的 "Git 存储库 URL" 输入框,这个字段不用手动填。只要前面步骤已通过 GitHub OAuth 授权 + 从列表选择仓库,CF 会自动识别并填充此字段。 手动填的场景:只有连接非 GitHub 来源(比如 GitLab 自托管 / 自建 Gitea)时才需要。本项目仓库在 GitHub 官方,走 OAuth 流程即可,忽略这个字段 / 保持它自动填充的默认值。
如何删除错建的 Workers 项目(踩坑后的补救)
如果已经错建成 Workers 项目(访问 .pages.dev URL 报 404 或显示 Worker 错误页):
- CF Dashboard → Workers & Pages → 找到错建的项目 → 点进去
- 右上角 / 左侧菜单 → Settings 标签
- 滚到最底部 → Danger Zone / 危险操作区 → 点击 Delete project
- 弹窗要求输入项目名(如
xisound-website)确认删除 - 回 Workers & Pages 主页 · 重新执行上面的正确创建流程
删除 + 重建整个过程约 5 分钟 · 不会影响 GitHub 仓库和代码。
5.1.4 步骤 3 · 构建配置(Set up builds and deployments)
点击 Begin setup 后进入构建配置页 · 逐字段填写:
| 字段 | 值 | 说明 |
|---|---|---|
| Project name | xisound-website |
默认与仓库同名 · 保持即可 · 决定 .pages.dev 子域名 |
| Production branch | main |
默认 main · 保持即可 |
| Framework preset | Astro(下拉选择) | CF 会自动识别并推荐 · 手动确认选中 Astro |
| Build command | npm run build |
Astro preset 自动填好 · 不用改 |
| Build output directory | /dist |
Astro preset 自动填好 · 不是 dist/,前面有斜杠 |
| Root directory | 留空(代表 /) |
仓库根就是 Astro 项目 · 不要填 xisound-website |
| Environment variables | 见下一节 5.1.5 | 点 Add variable 添加 3 个 |
Node 版本关注
Cloudflare Pages 默认 Node 18 · 本项目 package.json engines 声明 >=22.12.0。如果首次构建报 Node 版本错,添加环境变量:
NODE_VERSION = 22 (加到 5.1.5 的环境变量里)
5.1.5 步骤 4 · 环境变量配置
点击 Add variable 依次添加:
| Variable name | Value | 说明 |
|---|---|---|
NODE_VERSION |
22 |
修复 CF 默认 Node 18 与本项目 >=22.12.0 的不兼容 |
PUBLIC_API_BASE_URL |
留空(空字符串) | M6.1 无后端 · 前端 LeadForm 会降级 mock 演示 · M6.2 部署后改填 https://api.joysnd.com |
PUBLIC_BAIDU_ID |
留空 | M7 埋点激活时填入 |
PUBLIC_GA4_ID |
留空 | M7 埋点激活时填入 |
留空 vs 不添加
CF Pages 环境变量里留空 = 有 key 无 value = 空字符串。本项目 LeadFormCapture 逻辑 (import.meta.env.PUBLIC_API_BASE_URL ?? '').replace(/\/+$/, '') 会把 undefined / '' 都视为无后端,降级到 mock。两种处理等价。
推荐:至少把 NODE_VERSION 和 PUBLIC_API_BASE_URL 添加进去(即使后者留空)· 后续 M6.2 改值时不用重跑 "Add variable" 流程。
5.1.6 步骤 5 · 首次部署
- 检查所有字段正确后 · 点击底部 Save and Deploy(蓝色大按钮)
- CF 会立即触发首次构建 · 跳转到部署页面
- 观察构建日志(时长约 1-3 分钟):
Cloning repository· clonemengliliusha/xisound-websitemain 分支Installing dependencies·npm install(淘宝镜像不生效 · CF 走 npm 官方源 · 可能慢)Building application·npm run build· Astro 产出 35 页到dist/Uploading build output· CF 上传到全球 edge- 构建完成会显示:
- ✅ Success! Your project is ready
- Deployment URL:
https://xisound-website.pages.dev(或类似子域 · CF 首次分配) - 点击 Deployment URL 打开 · 首屏应该看到 Xisound 首页(M5.8.1 Hero + 14 产品 + 四方案入口)
构建成功验收清单
- 部署页面显示 ✅ Success
-
https://xisound-website.pages.dev返回首页 200 - 首页 Hero 渲染正常(Trinity 渐变 + 极光青点缀)
- 打开
/products总览页 · 六层 14 产品渲染 - 打开任一资源页 · 填写留资表单 · 提交 · 看到"已收到"感谢页(走 mock 降级是预期行为)
- 浏览器 Console 看到
[lead/mock] PUBLIC_API_BASE_URL 为空 · 走降级模拟日志
5.1.7 步骤 6 · DNS 配置(自定义域名)
回到 Cloudflare DNS 面板(左侧菜单 → joysnd.com → DNS → Records),添加:
| 类型 | 名称 | 内容 | 代理状态 | TTL |
|---|---|---|---|---|
| CNAME | www |
xisound-website.pages.dev |
🟠 Proxied | Auto |
| A | @ |
192.0.2.1(占位 IP · 仅用于 Page Rules 激活) |
🟠 Proxied | Auto |
| CNAME | docs |
(延后 M6.3 · 暂不添加) | - | - |
| CNAME | download |
(延后 M6.2 · 暂不添加) | - | - |
| CNAME | api |
(延后 M6.2 · VPS IP 或 CNAME) | - | - |
根域 @ 为什么用占位 IP
根域 A 记录必须指向一个 IP(CNAME flattening 是 CF 付费特性)。用 192.0.2.1 是 RFC 5737 文档保留地址 · 永远不可达。真实流量会被 5.1.9 的 Page Rule 301 拦截到 www · 所以 A 记录的 IP 不会被命中。
5.1.8 步骤 7 · 绑定 www.joysnd.com 自定义域
- CF Dashboard → Workers & Pages → 选中刚创建的
xisound-website项目 - 左侧 Custom domains 标签
- 点击 Set up a custom domain
- 输入
www.joysnd.com→ 点击 Continue - CF 检测到 5.1.7 已添加的 www CNAME → 点击 Activate domain
- CF 自动签发 HTTPS 证书(3-5 分钟 · 期间 Status 会从 Verifying → Active)
- 验证:打开
https://www.joysnd.com· 应返回官网首页(与.pages.dev同内容) - 浏览器地址栏左侧应显示🔒绿锁 · 证书颁发者 Cloudflare Inc ECC CA-3
5.1.8B 步骤 7.5 · 绑定 docs.joysnd.com(MkDocs 文档站 · M6.1.1)
M6.1.1 新增
本节为 M6.1.1 新增内容 · 在 §5.1.8 绑定官网自定义域之后执行。完整文档站部署手册见 docs-site-deployment.md。
此处为精简操作步骤,对齐 §5.1.3-§5.1.6 的 CF Pages 创建流程,但目标仓库为 mengliliusha/xisound-docs、监听分支为 gh-pages。
5.1.8B.1 前置条件
-
mengliliusha/xisound-docs仓库已首次 push(本地06_docs/site-build/嵌套 git · 用户已git remote add origin) - GitHub Actions 首次构建通过(约 100-140 秒)· gh-pages 分支已自动生成
- Cloudflare Dashboard 中
joysnd.comzone 状态 Active(5.1.2 已完成)
首次 push 命令:
cd D:\work\25_claude\workspace\AlgoDepartment\06_docs\site-build
# 如果还未 git init
git init -b main
# 确认远程已加
git remote -v
# origin git@github.com:mengliliusha/xisound-docs.git (fetch/push)
# 首次全量提交
git add .
git commit -m "feat(docs): initial commit · M6.1.1 site build with GitHub Actions deploy"
git push -u origin main
5.1.8B.2 创建 CF Pages 项目(监听 gh-pages)
- CF Dashboard 左侧 → Workers & Pages → 右上角 Create application
- 切到 Pages 标签(非 Workers · 详见 §5.1.3 踩坑提醒)
- Connect to Git → 选择
mengliliusha/xisound-docs(首次需 GitHub OAuth 授权) - 项目配置:
| 字段 | 值 | 说明 |
|---|---|---|
| Project name | xisound-docs |
保持默认 · 决定 .pages.dev 子域名 |
| Production branch | gh-pages |
⚠️ 关键 · 不是 main · gh-pages 是 Actions 自动推的产物分支 |
| Framework preset | None / 留空 | 产物已是静态 HTML · 无需再构建 |
| Build command | 留空 | Actions 已构建好 |
| Build output directory | / |
gh-pages 根目录就是 _site/ 内容 |
| Root directory (advanced) | 留空 | — |
- 点击 Save and Deploy
- 如果此时 gh-pages 分支还不存在:CF Pages 会报错 "No branch named gh-pages found"
- 回 GitHub →
mengliliusha/xisound-docs/actions→ 手动 Run workflow(选 main 分支)· 等 ~2 分钟 workflow 完成自动创建 gh-pages 分支 -
CF Pages → xisound-docs 项目 → Deployments → Retry deployment
-
等约 1 分钟部署成功 · 获得 CF 分配的子域 · 形如
xisound-docs-abc.pages.dev
5.1.8B.3 DNS 配置(docs 子域)
CF Dashboard → joysnd.com → DNS → Records → Add record:
| 类型 | 名称 | 内容 | 代理状态 | TTL |
|---|---|---|---|---|
| CNAME | docs |
xisound-docs-abc.pages.dev(5.1.8B.2 拿到的子域) |
🟠 Proxied | Auto |
不能是 .joysnd.com 的 CNAME
CNAME 目标必须填 CF Pages 分配的 .pages.dev 子域,不要填 docs.joysnd.com 自指(会导致解析环路)。
5.1.8B.4 绑定 docs.joysnd.com 自定义域
- CF Dashboard → Workers & Pages → 选中
xisound-docs项目 - 左侧 Custom domains → 点击 Set up a custom domain
- 输入
docs.joysnd.com→ Continue - CF 检测到 5.1.8B.3 已添加的 CNAME → 点击 Activate domain
- HTTPS 证书自动签发(3-5 分钟 · Verifying → Active)
- 验证:打开
https://docs.joysnd.com· 应返回文档站首页(Xisound · 算法部文档中心) - 浏览器地址栏左侧应显示🔒绿锁
5.1.8B.5 CF SSL 模式确认
CF Dashboard → SSL/TLS → Overview → Mode 应为 Full(CF Pages 自带证书 · 不需要 Full (strict))。
5.1.8B.6 xisound-website 环境变量补齐(生产)
CF Pages → xisound-website 项目 → Settings → Environment variables → Production → Edit variables:
| Variable name | Value |
|---|---|
PUBLIC_DOCS_BASE_URL |
https://docs.joysnd.com |
PUBLIC_INTERNAL_DOCS_BASE_URL |
https://docs.joysnd.com |
Save → Deployments → Retry latest deployment(或 push 新 commit 触发)。
5.1.8B.7 M6.1.1 部署验收清单
-
https://docs.joysnd.com返回首页 200 · HTTPS 证书 Active - 首页 Hero 渲染正常 · Mermaid 图正常 · 搜索功能可用
- 随机打开一个内页(如
/D3-architecture/deployment-topology/docs-site-deployment/)· 内容正常 -
https://www.joysnd.com/docs文档门户两个卡片 note 字段已去敏感化 -
https://www.joysnd.com/docs-public/所有外链跳转到docs.joysnd.com(不再是 127.0.0.1:8000) -
https://www.joysnd.com/docs/internal-gate页面不再显示明文凭证 "账号 xisound · 密码 DevPreview2026" - 使用前端假账号
xisound / DevPreview2026登录仍能成功(auth.ts 守卫保留) - 登录后访问
/docs-internal· 外链跳转到docs.joysnd.com - 作者侧修改 md →
git push main→ 90 秒到 3 分钟内docs.joysnd.com可见新内容
5.1.8B.8 故障排查
| 症状 | 可能原因 | 解决 |
|---|---|---|
| CF Pages 创建项目报 "No branch named gh-pages" | Actions 首次未跑 / 首次失败 | GitHub Actions → Run workflow 手动触发 |
docs.joysnd.com 打开报 525 / SSL 错 |
CF SSL 模式设为 Full (strict) 但 CF Pages 无源站证书 | 改为 Full(非 strict)· 或重绑定自定义域 |
| 作者 push 后文档站内容没更新 | Actions 构建失败 / CF Pages 缓存 | 先查 Actions log · 再 CF Pages Purge Cache |
| 搜索功能不工作 | mkdocs-material search plugin 未启用 | 检查 mkdocs.yml plugins 段包含 - search: |
完整故障树见 docs-site-deployment.md §7。
5.1.9 步骤 8 · 根域 joysnd.com → www.joysnd.com 301 重定向
- CF Dashboard →
joysnd.comzone → 左侧 Rules → Page Rules - 点击 Create Page Rule
- 填写:
- If the URL matches:
joysnd.com/*(注意没有 www · 也没有 http/https 前缀) - Then the settings are:
- 下拉选 Forwarding URL
- Status code:301 - Permanent Redirect
- Destination URL:
https://www.joysnd.com/$1
- 点击 Save and Deploy
- 验证:
5.1.10 阶段 0 最终验收
-
https://xisound-website.pages.dev返回首页 200 -
https://www.joysnd.com返回首页 200 · 证书有效(🔒绿锁) -
https://joysnd.com301 →https://www.joysnd.com -
http://www.joysnd.com自动升级 HTTPS(CF 默认 Always Use HTTPS) - Lighthouse 性能 > 90(M5.8.1 bundle 5.2KB · 应该轻松过)
- 留资表单提交显示感谢页 + Console 看到
[lead/mock]日志(预期 · M6.2 接入后端后消失)
5.1.11 后续自动化(无需配置)
- 每次
git push origin main→ CF 自动触发新部署(~2 分钟完成) - Preview deployments:任何 PR / 非 main 分支 push → 自动生成预览 URL(形如
xxx-abc.xisound-website.pages.dev)· 可分享给 review 人 - 构建缓存:CF 自动缓存
node_modules/,后续构建缩短到 ~30 秒
环境变量后续更新路径
M6.2 VPS 部署完成后 · 修改 PUBLIC_API_BASE_URL 为 https://api.joysnd.com:
CF Dashboard → xisound-website 项目 → Settings → Environment variables → Edit → 填入新值 → Save → 手动触发 Retry deployment 即生效。
CF Pages Free 计划限额
- 每月 500 次构建(足够 M6 阶段 · 一天 ~15 次 push 不到限)
- 每月 100 GB 带宽(官网 5.2KB bundle + 121 占位 SVG · 支撑 100 万次访问)
- 静态文件不限大小
- 升级 Pro 计划 $20/月 · M8 社区期再评估
5.2 阶段 1 · 后端代码交付(M6.1 ✅ 本轮已完成)
M6.1 交付物
仓库:AlgoDepartment/xisound-api/(独立 .NET 8 仓库 · root commit 63706c9 · 待 push 到 mengliliusha/xisound-api)
规模:19 文件 · 1501 行代码
验证:6/6 本地端到端验证全部通过(详见 §6)
5.2.1 后端交付清单
| 模块 | 路径 | 状态 |
|---|---|---|
| 项目脚手架 | XiSound.Api.sln + src/XiSound.Api/XiSound.Api.csproj (net8.0) |
✅ |
| Program.cs | Minimal API + CORS + DI + AdminKey middleware | ✅ |
| 配置文件 | appsettings.json + appsettings.Development.json(SQLite 默认) |
✅ |
| 数据层 | Models/Lead.cs + Data/AppDbContext.cs(EF Core · SQLite/PostgreSQL 双栈) |
✅ |
| DTO + 校验 | Dtos/LeadDto.cs(DataAnnotations + Phone/Email 业务校验) |
✅ |
| 邮件服务 | Services/EmailServices.cs(IEmailService + ResendEmailService · M4 降级 no-op 日志) |
✅ |
| 企微服务 | Services/WeworkServices.cs(IWeworkNotifier + WeworkBotNotifier · W2 降级 no-op 日志) |
✅ |
| 管理端保护 | Middleware/AdminKeyMiddleware.cs(X-Admin-Key header · 空则 503 拒绝) |
✅ |
| 留资入口 | Controllers/LeadsController.cs(POST /api/lead + GET /admin/leads + 限流 5/min) |
✅ |
| 健康检查 | Controllers/HealthController.cs(GET /health · GET /health/ready) |
✅ |
| 容器化 | Dockerfile(多阶段 · 非 root 用户 · runtime ~220 MB) |
✅ |
| 编排 | docker-compose.yml(api + postgres/redis prod profile) |
✅ |
| 密钥占位 | .env.example(所有 Resend/Wework/Db 字段) |
✅ |
| 文档 | README.md(5 分钟起步) |
✅ |
5.2.2 关键设计决策
| # | 决策 | 选择 | 理由 |
|---|---|---|---|
| B3 | 后端语言 | .NET 8 | Team C# 主栈 · 与 04_development 后端产品线统一 |
| B6 | 数据库 | SQLite(M6.1 本地)→ PostgreSQL(M6.2 部署) | EF Core 双 provider 预置 · 切换仅需改配置 |
| B7 | 仓库结构 | 独立 xisound-api/ 仓库 |
前后端各自 CI/CD · 对齐 06_docs/官网部署.md §14 |
| M4 | 邮件 | Resend 起步 · 空 Key 降级日志 | 免费 3000 封/月 · 国际节点 · 无需备案 |
| W2 | 企微通知 | 群机器人 · 空 URL 降级日志 | 免费 · 集成简单 · 无需企业微信企业号认证 |
| C1+C3 | CRM 同步 | 数据库 + 企微机器人双通道 | 数据库兜底 · 企微实时通知 |
5.2.3 前瞻设计(为 M6.2-M6.4 预留)
- EF Core 双 provider:
Database:Provider = "sqlite" | "postgres"切换一行配置 · 无代码改动 IEmailService接口抽象:M6.4 接阿里云 DirectMail 国内双通道 · 只需新加实现类AdminKeyMiddleware与 JWT 同 pipeline:M6.3 升级 JWT 时并存 · 不互相干扰- 限流内存实现 → Redis 升级:
ConcurrentDictionary当前内存实现 · M6.2 Redis 容器上线后切IDistributedCache即可 EnsureCreated→ migrations:M6.1 本地用db.Database.EnsureCreated()· M6.2 Postgres 时切正式 migrations(dotnet ef migrations add Init)
5.2.4 commit 速查
| 仓库 | Commit | 变更 |
|---|---|---|
xisound-website |
32b3b40 |
feat(M6.1): wire LeadForm to xisound-api with env base URL |
xisound-api(新建) |
63706c9 |
feat(M6.1): ASP.NET Core 8 backend scaffold with lead API + admin + Resend/Wework no-op |
本地跑通命令与 6/6 验证结果见 §6.
5.3 阶段 3 · VPS 生产部署(M6.2 ⏳ 下一里程碑)
权威简化版 · 完整 TODO 见 TODOLIST
本节为 M6.2 VPS 部署的核心路径与架构决策。详细分阶段步骤(前置准备 / VPS 基础环境 / 服务编排 / DNS 集成 / CI/CD / 监控 / 验证)见:
👉 AlgoDepartment/07_web/xisound-website/TODOLIST.md §3 · P0 M6.2 章节 · 35+ TODO 项
预计工期:3-5 个工作日
非运维背景?先看账号采购零基础指南
如果你对 VPS / 腾讯云产品线 / Resend / 企微机器人不熟悉 · 先阅读:
👉 M6.2 · VPS 账号申请与采购零基础指南
覆盖:VPS 是什么 · 腾讯云4 种镜像类型详解(应用模板 / 操作系统 / 容器 / 自定义) · Resend 免费版限额 · 企微机器人限流规则 · 常见坑与解决方案。
本节(§5.3)假设你已完成账号采购(阶段 0 前置准备) · 从阶段 1 VPS 基础环境开始。
5.3.1 部署架构(M6.2 目标态)
graph TB
subgraph Edge["Cloudflare 边缘"]
User[访客浏览器]
CF[CF CDN + DDoS 防护]
end
subgraph VPS["腾讯云香港 · Ubuntu 22.04 · 2C4G"]
Nginx[nginx:1.27-alpine<br/>80/443 · 反代 + CORS + 限流]
Certbot[certbot<br/>Let's Encrypt 证书]
ApiCtn[xisound-api<br/>:5000 · ASP.NET Core 8]
PgCtn[(postgres:16<br/>pg_data volume)]
RdsCtn[(redis:7-alpine<br/>AOF persistence)]
end
subgraph Downstream["下游服务"]
Resend[Resend 邮件<br/>mail.joysnd.com M6.4]
Wework[企微群机器人]
Sentry[Sentry .NET SDK]
UptimeRobot[UptimeRobot<br/>5min 探活]
end
User -->|HTTPS| CF
CF -->|api.joysnd.com 443| Nginx
Nginx -->|/health /api/lead /admin/*| ApiCtn
Certbot -.每月续签.-> Nginx
ApiCtn -->|EF Core| PgCtn
ApiCtn -.限流 · 会话.-> RdsCtn
ApiCtn -.入库后异步.-> Resend
ApiCtn -.入库后异步.-> Wework
ApiCtn -.异常采集 10%.-> Sentry
UptimeRobot -.探活.-> Nginx
class User,CF xyL4
class Nginx,Certbot xyL3
class ApiCtn xyL2
class PgCtn,RdsCtn xyL0
class Resend,Wework,Sentry,UptimeRobot xyL5
5.3.2 核心部署步骤(6 阶段)
| 阶段 | 工作内容 | 工期 | 关键输出 |
|---|---|---|---|
| 阶段 0 | 前置准备:腾讯云账号 / VPS 下单(香港 2C4G 80G)· Resend 账号 + API Key · 企微 Webhook · xisound-api 首次 push 到 GitHub | 0.5-1 天 | VPS 公网 IP · 各类密钥 |
| 阶段 1 | VPS 基础环境:SSH ed25519 key · 系统加固(禁 root · UFW · fail2ban · unattended-upgrades)· Docker + Compose | 0.5 天 | 加固的 VPS · Docker 运行正常 |
| 阶段 2 | 服务编排:docker-compose.prod.yml(api + postgres:16 + redis:7 + nginx + certbot)· nginx.conf(反代 + CORS + 限流)· SQLite→PostgreSQL 迁移 · certbot HTTP-01 签发 · .env.production 填密钥 |
1-1.5 天 | VPS 本地 HTTPS 可访问 api |
| 阶段 3 | DNS + 前端集成:CF DNS 加 api A 记录(🟠 Proxied)· CF SSL Mode: Full (strict)· xisound-website 环境变量 PUBLIC_API_BASE_URL = https://api.joysnd.com · Retry deployment |
0.5 天 | https://api.joysnd.com/health 200 |
| 阶段 4 | CI/CD:GitHub Actions workflow(build-push-action 推 GHCR → ssh-action 到 VPS → docker compose pull api + up -d)· GitHub Secrets(VPS_SSH_KEY / VPS_HOST / GHCR_TOKEN) |
1 天 | push main 自动部署 |
| 阶段 5 | 监控 + 报警:UptimeRobot(api / www / docs 三端点 5min 探活)· Sentry .NET SDK(DSN + 10% trace 采样)· 腾讯云主机监控(CPU/内存/磁盘阈值) | 0.5 天 | 异常自动告警 |
| 阶段 6 | 生产验证:端到端留资测试 · SSL Labs A+ · 限流 429 · CORS 白名单 · PostgreSQL 每日备份 cron + 恢复演练 | 0.5 天 | M6.2 验收通过 |
5.3.3 数据库迁移策略
graph LR
Local[M6.1 本地<br/>SQLite · data/app.db<br/>EnsureCreated]
Migrate[dotnet ef migrations<br/>add InitialPostgres]
Prod[M6.2 生产<br/>PostgreSQL 16<br/>正式 migrations]
Local -.appsettings 改 Provider.-> Migrate
Migrate -->|dotnet ef database update| Prod
class Local xyL0
class Migrate xyL2
class Prod xySuccess
迁移决策:
- M6.1 本地数据量小(验证数据约 1-10 条)· 生产启动时直接 truncate 重新开始(推荐)
- 如需保留 · 用 sqlite3 data/app.db .dump 导出 SQL · 手动适配 Postgres 语法(AUTOINCREMENT → SERIAL)
5.3.4 M6.2 验收清单
-
https://api.joysnd.com/health返回 200 · SSL Labs A+ 评级 - 生产端到端留资提交成功 · 后端入库 + 企微推送 + 邮件通知三项全中
- GitHub Actions 自动部署流水线跑通 · push main → 2-3 分钟后生产更新
- UptimeRobot 三个端点(api/www/docs)全部 ✅ Up
- Sentry 能捕获到测试异常
- PostgreSQL 自动备份已跑一次 · 恢复验证成功
- 限流测试:连续 POST 6 次
/api/lead· 第 6 次返回 429 - CORS 测试:
www.joysnd.com允许 · 第三方域拒绝 - 文档更新:07_web/CLAUDE.md 里程碑加 M6.2 ✅ 行 · TODOLIST.md 相关项勾选
5.3.5 成本估算
| 项目 | 月费 | 年费 | 说明 |
|---|---|---|---|
| 腾讯云轻量应用(香港 2C4G 80G 1TB) | ¥60-80 | ¥700-900 | 免备案 · 年付优惠 |
| 域名 joysnd.com(腾讯云) | — | ¥55-65 | 续费 |
| Cloudflare Pages (Free) | ¥0 | ¥0 | 500 构建/月 · 100 GB 带宽/月(远超需求) |
| Resend Free | ¥0 | ¥0 | 3,000 封/月 |
| UptimeRobot Free | ¥0 | ¥0 | 50 monitors · 5min 间隔 |
| Sentry Free | ¥0 | ¥0 | 5,000 errors/月 |
| 合计 | ¥60-80 | ¥755-965 | 第一年总成本 ≤ ¥1000 |
5.3.6 风险与缓解
| 风险 | 可能性 | 缓解 |
|---|---|---|
| VPS 宕机 | 低 | UptimeRobot 5min 告警 · 腾讯云 99.95% SLA |
| 邮件被标记垃圾 | 中 | M6.4 完成 Resend 域名验证(SPF/DKIM/DMARC) |
| 企微机器人限流 20 次/分钟 | 低 | 留资量远低于此 · M6.3 可选引入消息队列缓冲 |
| SQLite 并发写入锁 | 已规避 | M6.2 切 PostgreSQL 即消失 |
| CI/CD 部署失败 | 中 | 健康检查失败自动回滚 · 保留最近 5 版镜像 |
| PostgreSQL 数据丢失 | 低 | 每日 pg_dump + 本地/COS 保留 30 天 · 季度恢复演练 |
下一步: 1. 用户侧完成阶段 0 前置准备(账号 + 采购) 2. 按 TODOLIST.md §3 分阶段执行 3. M6.2 完成后 · 扩展本节为"M6.2 ✅ 已交付"版本(类似 v2.0 对 M6.1 的改写)
5.4 阶段 2 · MkDocs 文档站部署(M6.1.1)
权威手册另行
本节仅为 M6.1.1 文档站部署的摘要。完整的仓库架构、Actions workflow 细节、作者操作手册、本地预览、故障排查见同目录: 👉 MkDocs 文档站部署 · docs.joysnd.com
5.4.1 背景与决策
- 06_docs 全库(含 BP / 融资 / 薪酬等敏感资料)归属私有
openclaw.git,不能整仓推到公共托管 site-build/子目录(MkDocs 源)需要独立成公共仓mengliliusha/xisound-docs,供 GitHub Actions + Cloudflare Pages 消费- Q1 决策 = B:采用 GitHub Actions 自动化方案(作者只 push 源码,产物由 CI 自动构建并推
gh-pages分支)
5.4.2 部署链路(三段式)
graph LR
Author[作者本地<br/>site-build/] -->|git push main| DocsRepo[xisound-docs<br/>main 分支]
DocsRepo -->|Actions<br/>mkdocs build| GhPages[gh-pages 分支<br/>_site/ 产物]
GhPages -->|Webhook 监听| CFPages[CF Pages<br/>xisound-docs 项目]
CFPages --> Domain[docs.joysnd.com]
class Author,DocsRepo xyL4
class GhPages xyL5
class CFPages xyL1
class Domain xySuccess
5.4.3 交付清单(本轮已完成)
| # | 交付物 | 路径 | 状态 |
|---|---|---|---|
| 1 | MkDocs site_url 补齐 |
site-build/mkdocs.yml L14 |
✅ |
| 2 | 依赖锁定 | site-build/requirements.txt |
✅ |
| 3 | GitHub Actions workflow | site-build/.github/workflows/deploy.yml |
✅ |
| 4 | 权威部署手册 | site-build/docs/D3-architecture/deployment-topology/docs-site-deployment.md |
✅ |
| 5 | 07_web 前端环境变量 + 明文凭证提示清理 | xisound-website/.env.example + src/pages/docs/** |
🔄 进行中 |
| 6 | 用户首次 push 到 mengliliusha/xisound-docs |
(用户手动执行) | ⏳ 待办 |
| 7 | CF Pages 项目创建 + 自定义域 docs.joysnd.com 绑定 |
CF Dashboard | ⏳ 待办 |
5.4.4 与 07_web 前端的集成点
| xisound-website 修改 | 目的 |
|---|---|
.env.example 新增 PUBLIC_DOCS_BASE_URL + PUBLIC_INTERNAL_DOCS_BASE_URL |
生产域名 https://docs.joysnd.com · 本地 .env.local 可覆盖 |
src/pages/docs-public/index.astro 环境变量化 |
替换 M5.7 硬编码 http://127.0.0.1:8000 |
src/pages/docs-internal/index.astro 环境变量化 |
同上 |
src/pages/docs/internal-gate.astro 删除明文凭证 UI |
Q2 决策:保留登录功能但页面不再明文显示 xisound/DevPreview2026 |
src/pages/docs/index.astro note 脱敏 + 演进路径重写 |
反映 M6.1.1 已上线现状 |
5.4.5 M6.1.1 验收清单
-
mengliliusha/xisound-docs:main首次 push 后 GitHub Actions 构建通过 -
gh-pages分支自动生成 · 含_site/index.html - CF Pages 项目
xisound-docs创建并连接 gh-pages 分支 -
docs.joysnd.com自定义域绑定成功 · HTTPS 证书 Active - 首页 + 随机内页 + Mermaid + 搜索功能验证通过
-
www.joysnd.com/docs-public/外链全部跳转到docs.joysnd.com -
www.joysnd.com/docs/internal-gate页面无明文凭证 · 登录功能保留 - CF Pages → xisound-website 项目环境变量
PUBLIC_DOCS_BASE_URL补齐并重部署
6. 本地端到端验证(6/6 通过 · 已归档命令与结果)
6.1 启动两端
# 终端 A · 后端
cd AlgoDepartment/xisound-api
cp .env.example .env # 所有密钥可留空 · 走 no-op 降级
dotnet restore
dotnet run --project src/XiSound.Api
# → 监听 http://localhost:5000 · Swagger: /swagger
# 终端 B · 前端
cd AlgoDepartment/07_web/xisound-website
echo "PUBLIC_API_BASE_URL=http://localhost:5000" > .env.local
npm run dev
# → 监听 http://localhost:4321
6.2 6 项验证全部通过
| # | 验证点 | 命令 | 实际结果 |
|---|---|---|---|
| 1 | dotnet restore |
dotnet restore XiSound.Api.sln |
✅ 22.3 秒 · 还原完成 |
| 2 | dotnet build |
dotnet build XiSound.Api.sln --no-restore -c Debug |
✅ 8.3 秒 · 0 warnings · 0 errors |
| 3 | GET /health |
curl http://localhost:5000/health |
✅ HTTP 200 · {"ok":true,"status":"alive","milestone":"M6.1"} |
| 4 | POST /api/lead |
见下方 JSON 示例 | ✅ HTTP 202 · {"ok":true,"leadId":"7252f48a3cc9492d8d5844cd8b394fcb"} |
| 5 | GET /admin/leads |
curl -H "X-Admin-Key: dev-key" http://localhost:5000/admin/leads?limit=5 |
✅ HTTP 200 · total:1 · 字段完整 · UTM 拆分正确 |
| 6 | no-op 降级日志 | Select-String -Pattern "\[Lead\]|\[WeworkBot-NoOp\]|\[ResendEmail-NoOp\]" run.log |
✅ 4 条日志全部命中(含 XiSound.Api started · Lead saved · WeworkBot-NoOp · ResendEmail-NoOp) |
{
"stage": "resource-download",
"resourceTitle": "声学设计方案白皮书",
"name": "张三",
"phone": "13800138000",
"email": "zhangsan@example.com",
"company": "某声学公司",
"role": "声学工程师",
"audience": "oem-new",
"consent": true,
"utm": { "source": "baidu", "medium": "cpc", "campaign": "brand" }
}
7. 配置与环境变量
7.1 xisound-api 配置项
| Key | 说明 | 默认/占位 |
|---|---|---|
Database:Provider |
sqlite / postgres |
sqlite |
ConnectionStrings:Default |
DB 连接串 | Data Source=data/app.db |
Cors:AllowedOrigins |
官网域白名单(逗号分隔) | 含 localhost:4321 + joysnd.com |
Admin:ApiKey |
/admin/* 保护 key · 空则 503 拒绝 |
dev 环境 dev-key · 生产必须覆盖 |
Resend:ApiKey |
Resend API Key · 空则 no-op | 空 |
Resend:FromAddress |
发信地址 | AutoAudio <onboarding@resend.dev> |
Resend:ToOps |
运营通知邮箱 | ops@joysnd.com |
Wework:WebhookUrl |
企微群机器人 · 空则 no-op | 空 |
Lead:RateLimit:WindowSeconds |
限流窗口 | 60 |
Lead:RateLimit:MaxRequests |
窗口内最大请求数 | 5 |
7.2 优先级
环境变量(__ 分隔)> appsettings.{Env}.json > appsettings.json
7.3 降级策略(M4 + W2)
空凭据不报错
Resend:ApiKey 为空 → 打印 [ResendEmail-NoOp] would send to {To} 日志 · 不抛异常
Wework:WebhookUrl 为空 → 打印 [WeworkBot-NoOp] content=... 日志 · 不抛异常
Admin:ApiKey 为空 → /admin/* 所有请求返回 503 Service Unavailable(避免裸奔)
8. 风险与缓解
| 风险 | 缓解策略 |
|---|---|
| SQLite 文件并发写入锁 | M6.1 量小不触发 · M6.2 切 PostgreSQL 即消失 |
CF Pages 跨域 fetch 到 api.joysnd.com 失败 |
Cors:AllowedOrigins 默认已含 https://xisound-website.pages.dev(CF 自动域)+ https://www.joysnd.com |
.env 密钥泄漏 |
.gitignore 已排除 · 首次 commit 前 git status 自查 |
| 限流误伤(内网 NAT 共享 IP) | M6.2 Redis 限流升级时改用 X-Forwarded-For + 账号维度双键 |
| Resend 海外节点国内丢信 | M6.4 切 M2 阿里云 DirectMail 国内线路(需 ICP 备案) |
| 企微机器人限频 20 次/分钟 | M6.1 留资量远低于此 · M6.2 引入消息队列缓冲 |
EF Core EnsureCreated 与 migrations 冲突 |
M6.2 升 Postgres 时切正式 migrations(dotnet ef migrations add Init) |
9. M6.2 - M6.4 延后项展望
| 阶段 | 核心目标 | 依赖前置 | 预计耗时 |
|---|---|---|---|
| M6.2 | VPS 部署上线 · api.joysnd.com · PostgreSQL 持久化 · GitHub Actions CI/CD |
腾讯云账号 + CF DNS | 3-5 天 |
| M6.3 | 账号系统 JWT · 下载签名 URL · 删前端 auth.ts |
M6.2 完成 | 3-5 天 |
| M6.4 | Resend 域名验证 + 飞书同步 + 阿里云 DirectMail 国内双通道 | Resend 账号 + ICP 备案 | 2-3 天 |
M6.1 代码的前瞻设计:
- EF Core 双 provider 已预置 · M6.2 升 Postgres 仅改配置
- IEmailService / IWeworkNotifier 接口抽象 · M6.4 切 DirectMail 只需新加实现
- AdminKeyMiddleware 与 JWT 在同一 pipeline · M6.3 升 JWT 时并存不互相干扰
10. Commit 对应关系
| 仓库 | Commit | 变更 |
|---|---|---|
xisound-website |
32b3b40 |
feat(M6.1): wire LeadForm to xisound-api with env base URL |
xisound-api(新建) |
63706c9 |
feat(M6.1): ASP.NET Core 8 backend scaffold with lead API + admin + Resend/Wework no-op |
11. 相关文档
- D3 Deployment Topology 总览
- M6.1.1 · MkDocs 文档站部署 · docs.joysnd.com · 文档站独立仓 + GitHub Actions + CF Pages 权威手册
- 07web-m6.1-deploy-plan-v1.0-archive.md · v1.0 对比版归档(决策演进留痕)
- D4 · Web-Astro 实现
- D0 · 技术开发文档体系方案
- 源文档:
AlgoDepartment/06_docs/官网部署.mdv1.0 · M6 战略级建议书 - 本地开发副本:
AlgoDepartment/07_web/doc/M6-deployment-guide.md(顶部 banner 指向本文档) - 里程碑表:
AlgoDepartment/07_web/CLAUDE.md§ 里程碑历史
版本历史
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0 | 2026-05-06 | 首次发布 · 方案对比版 · B3/B6/B7 三项决策的作用说明与选项优劣对比 · 已归档为 07web-m6.1-deploy-plan-v1.0-archive.md |
| v2.0 | 2026-05-06 | 定稿版 · 反映实际交付(CF Pages 纯静态 + 独立 xisound-api .NET 8 仓库 + SQLite 双栈)· 6/6 端到端验证通过 · 与 v1.0 决策差异见 §2.3 |
07_web · M6.1 最小可发布部署方案 · v2.0 · 2026-05-06 · © Xisound Inc.