Ghost 上下文地图(Context Map):Portal、Gift Subscriptions 与 Gift Links 的领域边界与术语契约
CONTEXT-MAP.md 是 Ghost 仓库中用领域驱动设计(DDD)的“上下文映射”方法组织业务边界的导航文档:它声明了三个业务上下文(Portal、Gift Subscriptions、Gift Links)各自的语言文档位置与职责范围,并用两条关系描述了上下文之间的协作方式。阅读本文后,你将掌握 Ghost 会员/赠礼体系的领域划分、每个上下文的关键术语(含必须避免的歧义用语),以及这些术语在 gifts 服务源码 中的落点,便于在阅读代码或设计新功能时保持与官方术语一致。
1. 上下文地图的作用与结构
CONTEXT-MAP.md 由两部分构成:Contexts(上下文清单)与 Relationships(上下文关系)。每个上下文条目给出两样东西:
- Path:该上下文详细语言文档(CONTEXT.md)的相对路径;
- 一句话职责描述:该上下文负责的业务范围。
仓库中这三个上下文分别是:
| 上下文 | 语言文档位置 | 职责范围 |
|---|---|---|
| Portal | apps/portal/CONTEXT.md | 嵌入在 Ghost 站点中的访客端会员组件(modal),负责会员注册、登录、付费订阅结账、优惠(offers)、账户管理,以及访客端的礼品订阅旅程 |
| Gift Subscriptions | ghost/core/core/server/services/gifts/CONTEXT.md | 预付、固定期限的会员访问权,覆盖礼品购买、礼品兑换、礼品过期、礼品消耗,以及向付费订阅的延续(continuation) |
| Gift Links | ghost/core/core/server/services/gift-links/CONTEXT.md | 在不创建会员的前提下,对单篇受保护文章/页面进行可分享的访问授权 |
从源码结构看,上下文与目录一一对应:Portal 是独立前端应用(apps/portal/),Gift Subscriptions 是核心服务端服务模块(ghost/core/core/server/services/gifts/),Gift Links 是另一独立服务端模块(ghost/core/core/server/services/gift-links/)。这种“一个上下文、一个目录、一份 CONTEXT.md”的组织方式,使术语文档始终贴近实现。
上下文文档普遍采用 Language(语言)章节 + 逐条术语定义的形式,并且每条术语都附有 Avoid(应避免使用的同义词)清单。这一机制是上下文地图的核心价值:它把“同一个词在不同上下文里含义不同”的问题显式化,防止团队(以及 AI 助手)在跨模块协作时用错术语。
2. Portal 上下文:访客端入口术语
apps/portal/CONTEXT.md 定义了 9 个术语。Portal 的难点在于:站点上有多种控件都能“打开 Portal”,但它们打开的页面与生效条件各不相同。以下按原文档完整列出,并结合关系小节说明。
2.1 控件与链接类术语
| 术语 | 定义(据原文档) |
|---|---|
| Portal button(Portal 按钮) | 站点上的环境式(ambient)控件,允许访客在未跟随任何特定 Portal 链接的情况下打开 Portal。它不决定某个特定 Portal 链接是否打开 Portal。 |
| Offer link(优惠链接) | 用于查看并接受会员优惠的 Portal 链接。优惠链接总是打开优惠页——即使 Portal 按钮被隐藏——且仅对有资格接受优惠的访客生效。Portal 的优惠触发器就是优惠链接。对无资格的访客——已有付费会员(不含免费/complimentary 会员)、或过期/已归档/挽留(retention)优惠——链接被静默忽略,Portal 不打开。 |
| Checkout button(结账按钮) | 直接为某个会员方案发起结账的站点控件,把访客直接送入结账流程,而不是打开优惠页。 |
| Checkout attempt(结账尝试) | 为某会员方案发起结账的一次尝试。结账尝试受到跨结账流量的请求限制保护,并对针对同一邮箱地址的重复尝试施加限制。若因已存在活跃付费订阅而被拒绝:已登出访客会被转入“登录邮箱流程”,而已登录会员则被告知已拥有活跃订阅——不会发送登录邮件。 |
从源码结构看,Gift Subscriptions 服务的 startCheckout 接口(见 gifts README)负责时长目录(duration catalogue)、Portal 方案门槛、层级/计费周期校验与权威价格——这正是上下文文档中“结账尝试”这一语言概念在后端的落点。
2.2 礼品订阅相关术语
Portal 文档中一半术语与礼品订阅旅程相关,它们刻意与“全局开关”解耦:
| 术语 | 定义(据原文档) | 避免使用的词 |
|---|---|---|
| Gift checkout(礼品结账) | Portal 中买家选择时长与会员层级、发起礼品订阅一次性购买的旅程。 | Gift purchase、gift page |
| Portal gift promotion(Portal 礼品推广) | 发布者可控制的、进入礼品结账的 Ghost 自有入口的呈现方式。注册页推广与账户页推广相互独立控制;两者都不决定通过直接 Portal 链接访问礼品结账本身是否可用。 | “Gift subscriptions enabled”“gifting enabled”“global gift promotion” |
| Signup gift entry point(注册礼品入口) | 注册旅程中的 Portal 推广,面向想为他人购买会员的访客。其可见性独立于账户礼品入口。 | Gift signup、account gift entry point |
| Account gift entry point(账户礼品入口) | 账户旅程中面向付费会员与 complimentary 会员的推广,供其为他人购买礼品。不展示给免费会员或正在接受赠礼访问的会员。 | Gift continuation、gift-recipient card |
| Gift redemption page(礼品兑换页) | 由兑换链接打开的 Portal 界面,访客或已登录会员可在此查看并领取(claim)礼品订阅。 | Gift Link page |
这里的关键设计意图是:三个开关维度——注册入口可见性、账户入口可见性、直接链接结账可用性——彼此正交。Avoid 清单明确禁止把“入口推广可见”与“礼品订阅功能是否启用”混为一谈。
3. Gift Subscriptions 上下文:赠礼订阅的完整语言
gifts/CONTEXT.md 是三个上下文中语言最庞大的一份,按“角色 / 购买与领取 / 时间与生命周期”三组共定义 21 个术语。以下完整继承原文档骨架。
3.1 角色(Roles)
| 术语 | 定义(据原文档) | 避免使用的词 |
|---|---|---|
| Buyer(买家) | 为礼品订阅付款的人。买家无需是会员,若自身有资格也可成为兑换者。 | Purchaser、recipient、redeemer |
| Redeemer(兑换者) | 领取礼品订阅并获得其访问权的会员。兑换者由兑换时而非购买时决定。 | Recipient、buyer |
| Recipient(受赠人) | 礼品订阅所针对的人。其邮箱只用于投递路由,不预留兑换权;无需是会员,在领取之前不会成为兑换者。 | Receiver、redeemer |
这三者的分离解释了“兑换者由兑换时决定”的语义:受赠人邮箱只是投递通道,任何人持链接均可领取。
3.2 购买与领取(Purchase and claim)
| 术语 | 定义(据原文档) | 避免使用的词 |
|---|---|---|
| Gift subscription(礼品订阅) | 预付、不可续期的会员权益,为某一会员层级提供固定期限的访问权。从领取时开始,而非从购买时开始。 | Gift membership、recurring subscription |
| Gift purchase(礼品购买) | 创建一份可分享礼品订阅的一次性购买。购买完成不意味着礼品邮件已发出。 | Gift checkout |
| Giftable offering(可赠售组合) | 当前可同时购买的至少一个付费会员层级与赠礼时长的组合。其缺失意味着礼品结账无法提供可购买的礼品。 | “Gifting enabled”、“paid memberships enabled” |
| Delivery method(投递方式) | 买家选择移交礼品订阅的方式:由站点邮件发给受赠人,或买家私下达成交换链接。 | Delivery mode |
| Redemption link(兑换链接) | 单次使用、限时有效的链接,可借此查看并领取已购买的礼品订阅。持链接者可看到买家姓名、指定受赠人姓名、受赠人邮箱与个人留言;指定受赠人不预留兑换权。 | Gift Link |
| Gift redemption(礼品兑换) | 领取一份有资格的礼品订阅的行为。领取者成为兑换者,赠礼访问自该时刻开始。 | Gift activation |
| Gift delivery(礼品投递) | 通过邮件向受赠人传达兑换链接。投递本身不领取礼品,也不开启赠礼访问。 | Gift redemption、gift activation |
| Scheduled delivery(定时投递) | 受赠人邮件计划在未来某日投递的礼品投递。排定投递时间不会推迟兑换的可领取性。 | Scheduled gift、scheduled redemption |
| Delivery date(投递日期) | 为定时投递选择的站点本地日历日期。在站点时区的 09:00 使受赠人邮件到期;选择当天则立即投递。它不影响兑换可领取性。 | Delivery time、deliver-at date |
| Personal message(个人留言) | 买家的留言,构成礼品的一部分,可在礼品被展示的各处(含邮件与兑换体验)呈现。 | Delivery message、email message |
| Gift sent(礼品已发送) | 受赠人邮件已被站点配置的发信通道(mail transport)接受。即使没有后续服务商回执,也视为投递完成。 | “Gift delivered”、provider delivery |
| Email delivery status(邮件投递状态) | 把受赠人邮件交给发信通道的当前进度:pending、sending、sent、failed 或 cancelled。与后续服务商汇报的结果相互独立。 | Delivery outcome |
| Delivery outcome(投递结果) | 礼品邮件最近一次服务商汇报的结果,如 delivered、temporarily failed、permanently failed。若发信通道不提供投递遥测,结果保持未知。 | “Gift sent”、delivery state |
“邮件投递状态”与“投递结果”的切分对应两种可靠性事实:前者是 Ghost 自己控制的“至少交付给通道一次”,后者是服务商(如 Mailgun)异步回传的事件。gifts README 印证了这一点:GiftDeliveryService.recordOutcome(...) 仅保留最新的 Mailgun 投递结果,而发信通道的接受(acceptance)才是权威的“已发送”事实;新记录到的服务商永久失败会以尽力(best-effort)方式向买家发送包含礼品链接的事务性通知,供其手动分享。
3.3 时间与生命周期(Time and lifecycle)
这是整个上下文最精密的部分,四个时间概念严格分离:
| 术语 | 定义(据原文档) |
|---|---|
| Redemption availability(兑换可领取性) | 礼品订阅可被领取的时刻。自购买时即开始,所有礼品皆如此——包括受赠人邮件被排期到未来的礼品。 |
| Claim window(领取窗口) | 从礼品购买到礼品过期、未兑换礼品可被领取的期间。当投递被排期到未来日期时,领取窗口可能超过 365 天。 |
| Expiry anchor(过期锚点) | 计算礼品过期所用的站点本地日历日期。定时投递取所选投递日期,其余情况取礼品购买日期。 |
| Gift duration(礼品时长) | 赠礼访问的总长度,自兑换起算。 |
| Gift cadence(礼品计费周期) | 用于给礼品订阅定价的月付或年付基础。它不使礼品变为周期性(recurring)订阅。 |
| Gift expiration(礼品过期) | 未兑换礼品领取窗口的终点:过期锚点之后 365 个站点日历日;后续邮件处理不会移动它。 |
| Gift consumption(礼品消耗) | 已兑换礼品提供访问这一角色的终点:时长走完,或付费订阅接管时。 |
| Consumption reminder(消耗提醒) | 发送给兑换者的邮件,每份礼品至多一次、在礼品消耗前最多 7 天发出,以便在赠礼访问结束前安排礼品延续。 |
| Gift continuation(礼品延续) | 从赠礼访问启动付费订阅,并在开始计费前把剩余赠礼时长结转(carry forward)。 |
这组术语与源码常量一一对应,constants.ts 给出了精确数值:
export const GIFT_EXPIRY_DAYS = 365; // 过期锚点后的领取窗口长度
export const GIFT_MAX_SCHEDULE_DAYS = 365; // 投递排期的最远提前量
export const GIFT_SEND_HOUR = 9; // 定时投递在站点本地 09:00 发送
export const GIFT_REMINDER_LEAD_DAYS = 7; // 消耗提醒最早提前量
export const GIFT_REMINDER_FLOOR_DAYS = 3; // 消耗提醒最晚提前量
export const GIFT_DELIVERY_STALE_AFTER_MS = 60 * 60 * 1000; // 投递“进行中”超过 1 小时视为陈旧
export const GIFT_DELIVERY_EMAIL_TAG = 'gift-delivery';
值得注意的两处实现细节:
- GIFT_MAX_SCHEDULE_DAYS 的注释明确说明 Portal 端在 apps/portal 的 gift-page.tsx 中持有一份独立副本(
GIFT_MAX_SCHEDULE_DAYS),因为 Portal 是独立分发的——两端修改必须同步。这正体现了上下文地图“上下文独立、契约需人工同步”的特征。 - GIFT_SEND_HOUR 的注释说明 Portal 端从不推导发送时刻,精确发送时刻通过成功 URL 中的
gift_scheduled_at参数传递——与术语表中“Delivery date 在站点时区 09:00 使邮件到期”的定义严格一致。
4. Gift Links 上下文:与兑换链接的关键区分
gift-links/CONTEXT.md 只有单一术语,但它的存在恰恰是上下文地图最有价值的部分之一:
Gift Link:一个可撤销(revocable)的链接,持有者可借此访问单篇受保护的帖子或页面。它不创建或兑换礼品订阅。 Avoid: Redemption link、gift-subscription link
Gift Links 是一个独立的小型服务端模块,源码见 gift-links 目录(含 service.ts、actions.ts、models.ts 等)。它与 Gift Subscriptions 的关系是名称极易混淆但机制完全不同:一个 Gift Link 授予对单篇内容的一次性可撤销访问、不产生会员身份;而一个 Redemption link 指向整段固定期限的会员访问权。上下文文档通过在两个上下文中互相写入 Avoid 条款(Gift Subscriptions 的“Redemption link”避免使用“Gift Link”,Gift Links 的“Gift Link”避免使用“Redemption link”)来消除这一歧义。
5. 上下文关系(Relationships)
CONTEXT-MAP.md 在 Relationships 一节给出两条关系:
- Portal ↔ Gift Subscriptions:Portal 呈现礼品订阅的购买与兑换旅程。
- Gift Subscriptions ↔ Gift Links:礼品订阅的兑换链接声明(claim)固定期限的会员访问权;Gift Link 则授予单篇受保护帖子或页面的访问权。
第一条关系在代码中的体现是清晰的分层:Portal 作为前端旅程发起请求,Gift Subscriptions 服务的 capability-oriented 接口 承接能力调用——README 明确说明“调用方拿不到 Gift 实体、Bookshelf 模型、事务或 Stripe 对象”,只获得稳定 DTO 与读模型。关键接口包括:
startCheckout(input):拥有时长目录、Portal 方案门槛、层级/周期校验、权威价格、token、成功参数、元数据、客户与一次性支付;getRedeemable(input)/redeem(input):返回公共兑换 DTO(对应 Portal 的 Gift redemption page);preparePaidContinuation(input):校验活跃礼品并返回稳定 tier/cadence/trial 决策,供正常订阅结账路径使用(对应 Gift continuation 术语);handlePaidSubscriptionActivation(memberId):付费订阅接管时消耗礼品时长;processReminders()/processConsumed()/processExpired():拥有礼品生命周期处理;reassignRedeemer(...):数据导入能力。
第二条关系则如第 4 节所述,本质是术语防混淆契约而非调用关系——两个模块互不调用,只是产品上都需要被明确区分。
6. 生命周期在源码中的落点:调度、投递与恢复
术语表中的生命周期概念(scheduled delivery、consumption reminder、delivery at-least-once)在 gifts/index.ts 的 init() 中有完整装配,可作为理解上下文语言的实现证据:
- 投递调度:
GiftDeliveryService配合SignedFlushScheduler(端点gifts/flush_deliveries,调度名gift_delivery)按 GiftDelivery 的scheduledAt排期;立即投递在购买后经由进程内事件SendGiftDeliveryEvent触发,定时投递在scheduledAt到达时进入同一条处理路径——印证术语表“Scheduled delivery 不推迟兑换可领取性”的定义。 - 消耗提醒调度:另一个
SignedFlushScheduler(端点gifts/flush_reminders)以gift.reminderDueAt()为排期依据,由StartGiftReminderFlushEvent触发processReminders();周期性任务通过 jobs(含 send-gift-reminders-job.js 与 clean-gifts-job.ts)注册。 - 付费订阅接管:
DomainEvents订阅SubscriptionActivatedEvent,触发giftService.handlePaidSubscriptionActivation(...)——即 Gift consumption 中“付费订阅接管”这一分支的实现。 - 启动恢复:
recoverPendingDeliveries()在所有服务初始化完成后运行,重新排期未来投递并恢复上次关闭时中断的发送;README 说明投递领取是原子的、过期的进行中领取在崩溃后会被重试,因此“发信通道接受”是**至少一次(at least once)**语义——对应术语表中 Email delivery status 的pending/sending状态存在原因。 - 邮件通道选择:
init()中GiftEmailService同时装配事务性邮件器(GhostMailer,用于买家确认与失败通知)与批量 Mailgun 客户端(MailgunClient,用于受赠人投递),使投递结果可观测;无 Mailgun 时回退到 provider 无关的事务邮件通道。
7. 如何使用这份上下文地图
- 跨模块沟通时先查术语:涉及 Portal 入口、优惠链接或礼品功能时,先到 CONTEXT-MAP.md 定位所属上下文,再到对应 CONTEXT.md 使用规范用语,并避开 Avoid 清单中的词(例如不要说“gifting enabled”来表达入口推广)。
- 区分两个“link”:Gift Link(gift-links)≠ Redemption link(gifts);前者是内容访问授权,后者是会员权益兑换。
- 时间问题按四个概念拆解:谈赠礼时间线时先分清 redemption availability(购买即可领)、claim window(至多锚点+365 天)、gift duration(自兑换起算)、gift expiration(过期锚点+365 站点日历日),数值以 constants.ts 为准。
- 以接口边界定位实现:Gift Subscriptions 的能力入口在 gifts/README.md 列出的接口集合,装配与事件订阅在 gifts/index.ts;Portal 侧旅程代码位于 apps/portal/src。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00