Website 是已被 API Host 与 JobHost 真实组合的公开站点后端模块。它交付内容管理与公开读取、联系表单、匿名遥测、Web Vitals、实验、漏斗、短链接、分析 Mart、Sitemap 和部分隐私接口;它不包含网站前端,也还没有完成 Consent、Lead 管理和定时发布闭环。
1. 模块位置
Website 的手写端点处理需要 HTTP 上下文的公开流程;7 个增长管理请求使用 [GenerateEndpoint]。API Host 注册分析队列、GeoIP、短链、缓存失效和持久化 Adapter;JobHost 执行分析 Mart 与 Sitemap 任务。
2. 代码地图
| 项目 | 当前责任 |
|---|---|
| Contracts | DTO、公开 Port、权限、Feature、集成事件 |
| Domain | Content、Lead、AnalyticsEvent、Experiment、Funnel、ShortLink、Mart |
| Application | 内容/联系/增长 Handler,分析与 Sitemap Executor |
| Infrastructure | ORM Store、HTML sanitizer、队列/DLQ、Redis 旅程、GeoIP、Data Protection、短链、Sitemap |
| API Host | 公开/管理 Endpoint、Visitor 中间件、输出缓存、组合根 |
| JobHost | Quartz 入口、Mart 重建、Sitemap 提交与 publication consumer |
3. 当前能力矩阵
| 能力 | 已有证据 | 当前不能承诺 |
|---|---|---|
| 内容 | 草稿、更新、计划、发布、下线、归档、公开查询、Feed、Sitemap | 到期自动发布、内容版本/审批、预览 token |
| 联系线索 | CAPTCHA、字段保护、保存、通知 | 幂等提交、通知对账、Lead 管理 API |
| 分析 | 有界内存队列、批写、Mart、RUM、漏斗、实验 | Consent、跨崩溃接纳、持久 DLQ、精确一次 |
| 隐私 | 主体/Visitor 查询和软删除 | 匿名占有证明、Consent 记录、法定工作流证据 |
| 短链 | HTTP(S)、随机/自定义码、过期、缓存、二维码 | 目标域策略、撤销/更新、租户所有权、点击可靠性 |
| 治理 | 六权限、五个默认关闭 Feature | Feature 当前未被运行路径消费 |
4. HTTP 表面
手写公开路由包括 /api/public/content*、/api/public/contact*、/sitemap.xml、/api/telemetry/*、公开实验分配、/r/{code} 和隐私自助接口。内容管理路由显式使用 website.content.read/manage。
增长管理生成路由用 IAuthorizedRequest 推导 website.growth.manage 或 website.analytics.read,与目录权限一致。Feature Keys 没有同等执行证据。
5. 公开与租户模型
Content、Lead 和 ShortLink 没有 TenantId,是平台全局数据;分析、Experiment、Funnel 与 Mart 带 TenantId。公开分析使用服务端 Website:PublicTenantId,缺失时默认为字符串 public。
管理分析从 ICurrentTenant 取可信租户。少数内部 privacy/assignment 请求仍携带 TenantId,但公开 Endpoint 负责从配置或认证上下文构造,不能直接暴露这些消息契约。
6. 真实遥测示例
当前流程不查询 Consent;它从中间件提供的 Visitor、Session、租户和粗粒度设备/地域构建 Context。
// ① VisitorId、SessionId 与 PublicTenantId 都由服务端上下文产生。AnalyticsCaptureContext context = WithPublicTenant( AnonymousVisitorContextMiddleware.BuildCaptureContext(httpContext, visitor), configuration);
// ② Handler 限制 1..50 条,并拒绝客户端伪造 experiment-assigned。Result result = await mediator.Send( new CaptureTelemetryBatchCommand(context, body.Events), httpContext.RequestAborted);
// ③ Success 只表示进入当前进程队列,不表示已持久化。return result.ToHttp(httpContext);7. 数据保护不是完整隐私体系
Lead 的 Name、Contact、InquiryContent 使用 ASP.NET Core Data Protection 加密;分析不存完整 IP/UA,URL 去 query/fragment,自定义维度有 allowlist 和启发式 PII 拒绝。
这些是数据最小化措施,不等于合法基础、同意、撤回、保留或数据主体验证。Visitor Journey 在 Redis 保存 400 天;当前没有 Consent aggregate 或 retention job。
8. 可靠性语义
分析接纳使用进程内 Channel。队满返回 AnalyticsQueueFull;正常停机尝试排空;进程崩溃丢失队列。重试耗尽后进入当前实例 ConcurrentDictionary DLQ,重启同样丢失。
短链点击若遥测队列已满,Endpoint 会返回失败而不重定向,意味着分析可用性当前阻断核心导航。联系通知失败也可能覆盖已成功保存 Lead 的结果。
9. 当前治理缺口
五个 Feature:public-content、contact、analytics、experiments、short-links 均默认关闭,但源码没有消费 WebsiteFeatures.*。部署不能依赖它们关闭路由。
LeadRead/LeadManage 权限存在,Lead 聚合也有状态迁移和跟进方法,但没有对应 Application Handler 或 Host Endpoint;目录声明不是已交付 CRM。
10. 深入阅读
11. 源码核查
# ① 检查手写与生成端点。rg -n "Map(Get|Post|Put|Delete)|GenerateEndpoint" \ src/Hosts/BitzOrcas.Api/Endpoints/Website* src/Platform/Website -g '*.cs'
# ② 五个 Feature 当前预期只命中目录声明。rg -n "WebsiteFeatures\." src tests -g '*.cs' --glob '!**/bin/**' --glob '!**/obj/**'
# ③ Scheduled 当前预期没有到期扫描或自动发布消费者。rg -n "PublishStatus\.Scheduled|ScheduleContent|Scheduled" src/Platform/Website src/Hosts -g '*.cs'12. 阅读顺序
先从 Endpoint 确认公开/认证/权限/限流,再读 Handler 的可信上下文来源,随后检查聚合不变量、Store 的事务/租户谓词,最后验证缓存、事件、后台任务、失败恢复和测试证据。不要从 DTO 或 XML 注释推断能力已经闭环。
13. 变更评审问题
- 新公开路由是否经过 Visitor 中间件,是否会被自动计 pageview?
- 请求是平台全局还是租户资源,模型是否一致?
- Feature 是否真的在 Endpoint/授权管线执行?
- 匿名标识是否被误用为授权凭据?
- Success 表示接纳、提交还是外部副作用完成?
- 状态与集成事件是否有 outbox 证据?
- Redis/Channel/DLQ 重启后的行为是什么?
- Data Protection Key Ring 是否可灾备恢复?
- 公开输出是否有 HTML/URL/XML 上下文安全策略?
- 擦除是否覆盖队列、DLQ、Mart、缓存和备份?
- Job 是否有租约、重复执行和部分成功合同?
- 测试是否覆盖双 ORM、多实例和真实 Host 路由?