作品集
CROSS-DEPARTMENT-KANBAN PUBLIC / 2026

部分信息已脱敏。持有访问密钥可查看完整版本。

新能源与建设器材租赁企业

跨部门依赖的社内看板系统

把跑在 Google Sheets 加 Apps Script 上的部门间依赖流程,做成 Cloudflare 边缘上的实时看板;依赖不单独建表,与任务共用同一套模型。17 天单人交付上线。

概要

客户原来的部门间依赖流程跑在 Google Sheets 加 Apps Script 上:谁请谁做、做到哪一步,靠一张共享表格和手工提醒维持。Vulcan 把这套流程做成了看板系统。

跨部门依赖没有单独建表。一条依赖就是一个普通任务,只是它的负责部门不是创建者所在的部门。依赖管理和任务管理因此共用同一张表、同一套查询和同一个看板。

提交依赖的向导先在创建者自己部门建草稿,送信时才改写负责部门并从接收方采番。放弃的草稿只留在自己的看板上,不会污染对方队列,也不需要为此增加一张表。

代码规模 3.4 万行
测试用例 470+
使用规模 约 100 名员工 / 10 余个部门
开发周期 17 天

架构设计

前端是 SvelteKit 的 SPA 模式,整个应用关掉服务端渲染。看板是重交互界面,没有 SEO 需求,SSR 换不到收益,反而让每次交互都要照顾两套渲染路径。

数据只走本应用自己的 BFF 端点,共 42 个。端点跑在 Worker 上直接绑定数据库,浏览器永远拿不到数据库句柄。数据访问收敛在共享的 Drizzle 查询层,应用里不写 SQL。

运行时全部在 Cloudflare 边缘:Worker 承载应用,D1 存数据,R2 放附件,KV 存个人设置,Durable Object 负责状态同步。登录是 Google OAuth,会话与另外两个内部应用共用一套。

系统架构:浏览器端 SPA、Worker 上的 BFF 端点、以及 D1 / R2 / KV / Durable Object 四类边缘资源的关系
系统架构:浏览器端 SPA、Worker 上的 BFF 端点、以及 D1 / R2 / KV / Durable Object 四类边缘资源的关系

技术难点

多人同步:只广播失效信号

一个人改了任务,其他人的看板要跟上,但这里没有引入 CRDT。Durable Object 只当广播站,推送的内容是失效的查询键前缀,客户端收到后让对应缓存过期并重新拉取。数据的唯一真相始终是数据库。

广播分两层收窄。键前缀里带部门和任务标识,前缀匹配天然只命中正在看那块内容的人;个人视图的事件再按员工标识定向投递。连接上的员工标签由服务端网关覆写,客户端伪造不了。

代价换来的是乐观并发和排序逻辑一个字都不用改,Durable Object 里也不存任何业务数据。连接用休眠 API,心跳由运行时自动应答,不唤醒实例。断线时退回轮询兜底。

LIVE 可以直接操作
双栏对照:左侧修改任务,右侧显示实际收到的信号载荷与随后触发的重新拉取

把 Durable Object 塞进被框架覆写的入口

Cloudflare 适配器每次构建都会重写 Worker 入口文件,手写的 Durable Object 导出放不住。构建脚本因此在编译后把产物移开,原位生成一个包装入口:普通请求转发给框架,连接升级请求拦下,同时导出 Durable Object 类。

包装文件里只有接线,逻辑全部留在有类型检查和测试覆盖的源文件里。脚本自带重入判断,重复执行不会把已移开的产物再覆盖一次。

乐观并发与分数索引

可编辑的行带版本号,更新语句把版本写进条件,影响行数为零就返回冲突。拖拽排序用分数索引,服务端根据落点两侧邻居生成新的排序值,客户端不允许自己写。

两者配合的结果是并发拖拽不会重排整列。抢到同一位置时插入会撞上唯一索引,走有限次重试;采番这类不可回退的操作放在受版本保护的写入之前,输掉竞争最多留一个号码空档。

父任务状态的递归导出

父任务的状态从子任务算出来,不单独存一份。子任务一变就从它的父节点向上逐层重算,某一层算出的状态没变就停止,因为再往上的推导只依赖这个没变的值。另外带一个已访问集合防环。

日语场景下不用全文索引

搜索走部分匹配加应用层打分,没有用 SQLite 的全文索引。默认分词器切不开日语,三字组索引又会漏掉予約、会議这类两字词。检索范围限定在本部门,扫描代价可以接受。

看板主界面:按状态分列、支持拖拽排序,右键菜单可直接修改单个字段
看板主界面:按状态分列、支持拖拽排序,右键菜单可直接修改单个字段
列表视图
列表视图
时间线视图
时间线视图

AI 原生的开发方式

这个项目从第一次提交到部署上线是 17 天,400 次提交里有 223 次带着 AI 的共同作者署名。开发方式是把 AI agent 当设计搭档:我定问题和取舍,它出实现,我复核并决定是否留下。

让这种速度可持续的关键是把判断留下来。仓库里四份面向 agent 的文档共两千余行,写的不是代码结构,是每个决定为什么这么定,以及哪些路已经试过并被否掉了。

为什么删掉日历表、为什么依赖不单独建表、为什么搜索不用全文索引,都连同代价写在文档和数据库迁移文件的开头。下一次改到这里的人读到的是判断依据,不用重新猜。

文档同时钉住了几处容易漂移的地方:枚举只在一个文件定义、界面改名不动代码里的查询键、口径不同的日期字段不许合并。每一条都配了对应的测试来守。