平台管理
内部看板
/admin/board 的看板是平台团队自己的 Kanban,用于跟踪已发布的工作、
当前努力和休眠的待办事项。仅限内部 — 对客户永远不可见,
从客户管理员永远无法访问(super_admin 中间件对其他角色返回 404)。
管理员侧边栏中故意没有导航条目 — 看板是内部团队工具,
不是我们想在自己的超级管理员侧边栏中宣传的东西。直接通过 URL
(/admin/board)或本文档页面导航。
为什么存在
待办事项在几个月内堆积 — 买家反馈线程、设计笔记、推迟的功能。 没有一个单一的持久家园,它们会泄漏到 Slack、草稿文件或记忆中。 看板是持久的家园。未来的工程会话将其作为起点阅读。
列
| 列 | 用途 |
|---|---|
| Backlog | 我们计划做但尚未开始的任何事情。大多数卡片在这里。 |
| In progress | 有人正在积极工作的卡片。尽量保持较小(1-3 项)。 |
| Review | 编码完成,等待交叉检查、部署或买家确认。 |
| Done | 已发布 + 验证。保留用于取证;不自动归档。 |
实时更新
看板每 8 秒通过 Inertia 的部分重新加载轮询自己
(router.reload({ only: ['tasks'] })),因此两个编辑同一看板的超级管理员
可以看到彼此的操作而无需手动重新加载。标题中的小 "Live · last synced Xs ago"
药丸确认轮询是活动的。
钩子(resources/js/hooks/use-board-live-sync.ts)
在构造上是内存泄漏安全的:
- Single-flight guard — 慢速连接不能堆积请求;当一个仍在飞行中时,下一个 tick 被跳过。
- Tab-visibility pause — 后台选项卡不轮询。聚焦时,钩子立即触发 + 重新武装间隔,因此返回的用户看到新鲜数据而无需等待完整周期。
- Dialog pause — 当 Create / Edit 对话框打开时,钩子停止轮询,因此用户正在填写的表单数据永远不会被传入的刷新替换。对话框打开时药丸切换到 "Paused"。
- Cleanup on unmount — 间隔 ID 和 visibilitychange 监听器位于同一个 effect 内;两者都在 effect 的清理中移除,因此它们不能超过页面的生命周期。
如果我们将来将 Echo 客户端连接到管理员 SPA(目前没有),
这个钩子可以替换为私有工作区通道上的 Reverb 订阅 —
页面的其余部分保持不变,因为钩子返回相同的
{ lastSyncedAt, isSyncing } 形状,无论传输方式如何。
每列的排序顺序
每列在页面渲染时有自己的排序规则,因此您滚动的列显示最有用的卡片在顶部:
| 列 | 排序 | 原因 |
|---|---|---|
| Backlog | created_at ASC(最旧优先) |
FIFO 队列感觉 — 等待时间最长的项目浮到顶部,这样我们不会失去跟踪。 |
| In progress | position ASC(手动) |
管理员在 Doing 中拖放;尊重他们设置的顺序。 |
| Review | position ASC(手动) |
与 Doing 相同 — 手动顺序获胜。 |
| Done | updated_at DESC(最新完成优先) |
最近发布的卡片一眼看去最有用 — 最近的提交、最近的胜利。 |
持久化
卡片作为单个 JSON 文件存在于
storage/app/private/kanban-tasks.json(Laravel 的本地磁盘)—
不在数据库中。这是故意的:它在开发期间保护看板免受
php artisan migrate:fresh 的影响,这是团队意外清除进行中的跟踪的最常见方式。
该文件在模式重置、工厂拆除和测试事务中幸存。
当您想要新标签/状态/属性时,不需要新表,不需要迁移 — 只需编辑 JSON 形状并提升控制器验证。卡片在服务器端按(状态顺序、位置)排序, 因此页面可以在每列中从上到下渲染而无需接触网络。
要备份或共享看板,复制 JSON 文件。要清除它,删除文件 (下次读取返回空看板,在下一次写入时惰性重新创建)。 要恢复以前的版本,将文件放回。
种子
Artisan 命令 board:seed 幂等地填充看板,
包含 Phase-1 后的待办事项和此会话 Done 列中发布的项目。
重新运行它不添加任何内容 — 按标题匹配 — 因此可以安全地烘焙到部署步骤中。
php artisan board:seed
票号
每张卡片在创建时都带有一个顺序的 #N(1, 2, 3, ...)。
数字从不重复 — 即使卡片被归档,下一张新卡片也会从 max(historical) + 1 开始,而不是空闲的位置。
这使得引用稳定:“查看 #14”在三个月后与今天的工作方式相同。
该数字在看板 UI 上每个卡片标题旁边渲染为小型等宽徽章。
CLI 命令接受数字作为卡片的短别名 — board:start 14, board:done "#14" 等。
精确标题查找仍然适用于向后兼容,但数字是推荐的参考样式。
编号更改之前的现有看板在第一次读取时自动回填:旧卡片按 created_at 顺序分配数字。
持久化文件在第一次访问时收敛到规范形状;不需要任何手动操作。
CLI 工作流
三个 artisan 命令涵盖从 CLI 的卡片生命周期 — 匹配项目 CLAUDE.md “硬规则” #7(捕获)和 #8(关闭)。 从 Claude Code 会话或任何终端使用这些命令;打开管理员 UI 是可选的。
捕获新卡片
在接触源代码之前运行。每个任务 — 推迟或即将立即完成 — 首先获得一个带有书面计划的卡片。
php artisan board:add "Add Paddle gateway" --body="$(cat <<'EOT'
## Why
EU/UK buyers prefer Paddle for VAT/MoR handling — currently they
have to set up Stripe Tax separately, which several CodeCanyon
buyers have flagged.
## Plan
1. New PaddleProductSync mirroring StripeProductSync.
2. Add paddle_product_id + paddle_plan_id columns to plans.
3. Wire CheckoutController to dispatch on gateway === 'paddle'.
4. Add admin gateway toggle in Settings → System.
## Files
- app/Services/Billing/PaddleProductSync.php (new)
- database/migrations/...
- app/Http/Controllers/Billing/CheckoutController.php
## Constraints
- Idempotent sync on product/plan IDs.
- Same gateway-registry pattern as Stripe/PayPal/Razorpay.
EOT
)" --label=billing --label=eu
标题幂等 — 重新运行相同的命令不执行任何操作。
状态默认为 backlog;标签是可重复的标志。
正文的 ## Plan 是合同:用户在编写任何代码之前阅读它,并在需要时重定向。
没有计划正文的卡片不是真正的卡片 — 它们是假装成票证的聊天笔记。
将卡片移动到 Doing
# 按数字(推荐):
php artisan board:start 14
# 或按精确标题:
php artisan board:start "Add Paddle gateway"
在代码更改之前运行。看板的“In progress”列应准确反映当前正在积极工作的内容。 幂等 — 已经在做的卡片报告无操作。
使用 UI 测试计划关闭卡片
php artisan board:done 14 \
--test="$(cat <<'EOT'
1. Sign in as super_admin
2. Open /admin/plans
3. Click "Sync to Paddle" on the Pro row
4. Confirm the modal shows "Synced — Paddle plan up to date"
5. Reload; confirm the gateway badge stays green
EOT
)" \
--release=v1.1.0
--test 是强制性的 — 命令拒绝在没有它的情况下关闭卡片。
计划附加到卡片正文中的 ## How to test from UI 标题下;
添加卡片时编写的原始上下文保持完整。
幂等:用新计划重新运行替换先前的部分,从不重复标题。
测试步骤必须是 UI 驱动的,而不是“在代码中断言 X”。 从“以 <role> 身份登录”开始,以便审阅者(或未来的您)可以在没有上下文的情况下遵循它们。
发布标签
--release=<tag> 将卡片发布的版本戳记到卡片行上
(在看板上标题旁边渲染为小绿色药丸)。
每当您关闭卡片时传递它,以便团队可以一眼看到 Done 并查看“v1.1.0 中发布了什么?”而无需运行 git log。
如果您在关闭时忘记传递 --release,批量戳记命令会标记每个尚未携带版本的 Done 卡片:
# 用 v1.1.0 戳记每个 Done 卡片(幂等 — 跳过已经有标签的卡片):
php artisan board:stamp-version v1.1.0 --status=done
# 按数字/标题戳记特定卡片:
php artisan board:stamp-version v1.1.0 --ref=23 --ref=34
# 强制覆盖现有标签(例如,如果卡片被错误戳记):
php artisan board:stamp-version v1.1.0 --status=done --force
版本是自由形式的字符串;我们使用类似 semver 的标签(v1.1.0, v1.2.0-rc1),但架构不强制执行格式。
药丸渲染字面值。
它不是什么
- 不是客户功能。客户不应该看到它。
- 在 v1 中不是拖放 — 列移动通过卡片详细信息对话框上的下拉菜单进行。拖放是我们将在有真正需求时添加的 UI 成本。
- 不是多看板。一个平台看板,仅此而已。如果有人以后需要每工作区 TODO 列表,那将进入单独的表。