更多模块

产品分析、推广返佣、审计日志、站内反馈、上线前的等待列表和公开演示模式:各自做什么,以及怎么开启或关闭。

除了登录、支付和积分,ShipKit 还带了六个较小的模块。每个都是 src/features/<name>/ 下的一个垂直切片,都可以用 /delete-feature <name> 删除(见删除功能)。有的克隆下来就是开启的,有的要等你打开开关。

模块默认状态开关
产品分析关闭src/config/app-config.ts 里的 analytics.posthog.key
推广返佣开启删除这个功能即可关闭
审计日志开启删除这个功能即可关闭
反馈开启删除这个功能即可关闭
等待列表关闭src/config/app-config.ts 里设 waitlist: true,然后部署
演示模式关闭DEMO_MODE=true,只能用在单独的演示部署上

下文提到的后台页面都由权限控制(affiliate.read、audit.read 等),见权限和管理后台。

产品分析(PostHog)

分析分两半,在你填入 PostHog 项目 key 之前,两边都不会有任何动静:

// src/config/app-config.ts
analytics: {
  posthog: {
    key: 'phc_…',                       // empty = no script, no events
    host: 'https://us.i.posthog.com',   // or the EU cluster
    assets: 'https://us-assets.i.posthog.com',
    ui: 'https://us.posthog.com',
  },
},

这个 key 是公开的只写令牌,所以放在配置里,而不是密钥文件里。

  • 浏览器端:src/features/analytics/components/posthog.tsx 懒加载 SDK,记录页面浏览,包括客户端路由跳转。它通过你自己域名下的 /api/ph 和 PostHog 通信,广告拦截器看到的是第一方流量;这个代理有自己的按 IP 限流(wrangler.jsonc 里的 ANALYTICS_RATE_LIMIT)。持久化用 localStorage,不用 cookie。自动采集和会话录制都是关闭的。已登录访客以用户 id 识别,退出登录时会重置 id。
  • 服务端:任何代码都可以调用 @/core/analytics/events 里的 track()。ShipKit 已经会发送signed_up、signed_in、account_deleted、checkout_started、subscription_started、purchase_completed、waitlist_joined 和 waitlist_approved。事件名用 snake_case 过去式,带几个扁平属性;绝不放密钥或自由文本。

track() 是尽力而为的:PostHog 出错只会记日志,不会让它所描述的操作失败。删掉这个功能后 track() 依然存在,只是什么都不做,所以调用处不用改。

推广返佣

任何用户都可以在应用侧边栏的推广返佣(/affiliate)加入,拿到一个永久的推广码和链接 /r/<code>。

  1. 点击链接会设置一个 30 天有效的 AFFILIATE_REF cookie,然后跳转到首页。它是服务端路由,因为营销页是预渲染的,没法设置 cookie。
  2. 带着这个 cookie 注册的新账户会归属到这位推广者名下,只归属一次,之后不再改变。未知的推广码和自己推荐自己都会被忽略,而不会让注册失败。
  3. 这个用户的每一笔 order.paid(包括续费)都会按金额的 20% 生成一条佣金。它先处于 pending,有 30 天的退款保留期;期间发生全额退款就作废。之后由每晚的定时任务标为 approved。
  4. 已批准的余额达到 50 美元后,推广者可以申请提现,并填写收款方式。
  5. 管理员在收入 → 推广返佣(/admin/affiliate)处理申请,用自己惯用的方式打款,然后把申请标为已支付或已拒绝。ShipKit 本身不转账。拒绝会释放这些佣金,推广者可以再次申请。

这四个数(佣金比例、保留期、最低提现额、归因窗口)都是设置 → 推广返佣里的运营设置,改了不用重新部署。比例调整只影响之后的订单,不影响已有的。单独谈好的比例,在该推广者那一行设置 commissionBps和 negotiatedRate。有未处理提现申请的账户,在申请处理完之前无法删除。

审计日志

@/core/audit/events 里的 audit() 记录谁对什么做了什么。登录等认证事件、角色和封禁变更、API key 管理、积分调整、套餐变更以及每个支付事件都已经覆盖;支付事件来自 webhook,所以没有操作人。在你的代码改动账户或安全状态的地方加一次调用:

import { audit, requestMeta } from '@/core/audit/events'

await audit({
  action: 'project.deleted', // <area>.<verb>
  actorId: context.session.user.id,
  actorEmail: context.session.user.email,
  targetType: 'project',
  targetId: project.id,
  meta: { name: project.name }, // never secrets or whole request bodies
  ...requestMeta(getRequest().headers),
})

记录存在 D1 里,显示在开发者 → 审计日志(/admin/audit)、每个用户的抽屉里,也可以通过 /api/v1/admin/audit 读取。180 天后自动清理(audit.retention_days,最少 7 天)。账户删除后,它的记录会保留,因为这正是审计日志的意义,但其中的邮箱地址会被清除。和 track() 一样,audit() 也是尽力而为,删掉这个功能后它就不做任何事。

反馈

侧边栏底部用户菜单里的帮助与反馈会打开一个简短的表单:类型(bug、想法、问题、其他)、正文,以及最多三张、每张最大 5 MB 的截图,可以选择、粘贴或拖入。截图存在 R2 里,只有上传者本人和管理员能看到。这两个上限都是设置项(feedback.image_limit、feedback.image_max_mb)。

管理员在用户 → 反馈(/admin/feedback)处理,状态有 new、seen 和 closed。回复会把新反馈标为已查看,并通过站内通知和邮件告诉用户。用户在账户对话框的我的反馈里查看自己的反馈和回复。

等待列表

要做封闭上线,在 src/config/app-config.ts 里设 waitlist: true 然后部署。它是构建时开关,因为预渲染的营销页需要对应的行动按钮:开启时,落地页的按钮指向 /waitlist 而不是 /dashboard。

开关打开后,用户登录时会被放进队列并跳到 /waitlist,页面显示他的排位,还可以留一句说明,讲讲想用它做什么。能打开管理后台的人不用排队。管理员在用户 → 等待列表(/admin/waitlist)批准用户;批准会发一封邮件和一条站内通知,用户下次访问时就能进入。批准也可以撤回。

把开关改回 false 再部署就正式开放。队列和它的历史记录仍留在表里。

公开演示模式

同一份代码的第二个部署可以用作在线演示。只在那个部署上设 DEMO_MODE=true,别处都不要设。

  • 登录页变成一个按钮。每次点击都会创建一个一次性账户(demo-…@demo.invalid),角色为 demo:可以进入管理后台,并拥有全部 *.read 权限。权限从注册表推导,所以新功能的读权限会自动出现。每次写操作都会被同一个 assertPermission 检查拒绝,和真实角色碰到的完全一样。
  • 其他所有登录方式都会被拒绝,所以那里不会积累真实身份。这个按钮有限流。
  • 应用顶部的横幅会说明后台只读,并给出 Stripe 测试卡号。
  • 每晚的 demo.purge-visitors 任务会删除超过一天的演示账户,连同上传的文件。

搭建方法:复制一份 wrangler.jsonc,给它独立的 Worker 名称、D1 数据库和 R2 存储桶,用测试模式的支付密钥,再用 bun run db:seed --remote 填充数据。种子数据的日期相对于脚本运行的那天,所以要时不时重跑一次。见部署。

演示模式是为了给潜在客户展示你的产品。绝不要在真实部署上设置 DEMO_MODE:它会把后台访问权交给任何点了按钮的人。