ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Fizzy 开发与部署指南:从 AGENTS.md 读懂多租户、UUID 与数据库全文搜索架构

Fizzy 开发与部署指南:从 AGENTS.md 读懂多租户、UUID 与数据库全文搜索架构 Fizzy 开发与部署指南从 AGENTS.md 读懂多租户、UUID 与数据库全文搜索架构【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy本文以 Fizzy一个看板式项目管理与问题追踪应用卡片在面板的各列之间移动并支持评论、提及与指派仓库根目录的 AGENTS.md 为骨架深入拆解其作为 Agent/开发者工作手册背后的技术要点URL 化多租户、UUIDv7 主键、MySQL/SQLite 双引擎全文搜索、流式导入导出、Kamal 自托管部署与代码风格约定。读完本文你将理解 Fizzy 内部这些关键架构决策的“为什么”并能在仓库中按图索骥找到对应源码、配置与测试为自己的二次开发或自托管部署打下基础。一、AGENTS.md 的定位默认值而非铁律AGENTS.md 开篇先交代了这份文档的性质它给出的指令是“带理由的默认值defaults with reasons而不是法律”。当眼前代码与文档冲突时应当选择更好的路径并标记冲突数据丢失、安全与 CI 门禁这类不变量invariants要被暴露出来而不是被覆盖。同时作者在提交前要“先攻击自己的 diff”Attack your own diff。也就是说这份文档更像一张架构速查地图它不重复教科书内容而是专门标记出那些“如果不知道就会踩坑”的项目特有约定。这也是全文后续所有章节的共同底色——每一节都在提醒读者哪里容易做出与代码事实相悖的假设。二、部署与 SaaS 模式开关2.1 自托管部署走 KamalAGENTS.md 明确了两点事实默认分支是main自托管部署使用 Kamal配置文件是 config/deploy.yml完整流程见 docs/kamal-deployment.md。打开 config/deploy.yml 可以看到开箱即用的起步配置# Name of this app service: fizzy image: fizzy # Where to deploy fizzy servers: web: - fizzy.example.com # Set your server name here ssh: user: root proxy: ssl: true host: fizzy.example.com env: secret: - SECRET_KEY_BASE - VAPID_PUBLIC_KEY - VAPID_PRIVATE_KEY - SMTP_USERNAME - SMTP_PASSWORD clear: BASE_URL: https://fizzy.example.com MAILER_FROM_ADDRESS: supportexample.com SMTP_ADDRESS: mail.example.com MULTI_TENANT: false SOLID_QUEUE_IN_PUMA: true结合 docs/kamal-deployment.md 可以还原完整部署链路fork 仓库 →kamal init生成.kamal目录与.kamal/secrets→ 修改 config/deploy.yml 与 secrets →bin/kamal setup完成首次部署会自动安装 Docker、构建镜像并启动后续增量发布只需bin/kamal deploy。secrets 文件中需要SECRET_KEY_BASE可用bin/rails secret生成、VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEYWeb Push 通知密钥对可在bin/rails c中用WebPush.generate_key生成以及 SMTP 凭据。若不想启用 SSL可将proxy.ssl置为falseMULTI_TENANT置为true则允许同一实例上多个账户注册默认只允许单账户。2.2 SaaS 模式切换tmp/saas.txtAGENTS.md 指出本地 Agent 工作模式下tmp/saas.txt是检出层级checkout-level的 SaaS 开关由bin/setup读取。文件存在时必须先读saas/AGENTS.md再继续文件不存在时则不要应用其中的指令。仓库实现进一步印证了这一机制bin/setup 与 bin/dev 都会检测tmp/saas.txt是否存在lib/fizzy.rb 中Fizzy.saas?的判定逻辑是ENV[SAAS]为真且不等于false或tmp/saas.txt文件存在lib/tasks/saas.rake 定义了SAAS_FILE_PATH tmp/saas.txt配合saas/AGENTS.md可知bin/rails saas:enable创建该文件、bin/rails saas:disable删除它。值得注意的细节是启用 SaaS 会切换 GemfileGemfile.saas并默认数据库适配器切换为 MySQL因此它会改变所有bin/rails、bin/kamal命令的行为——这也是为什么 AGENTS.md 要求“文件存在时先读 saas/AGENTS.md”。三、多租户是 URL 化的AccountSlug::Extractor 中间件AGENTS.md 用一整节强调多租户的形态每个账户获得一个十进制external_account_id作为 URL 前缀形如/{account_id}/boards/...。这里有一个容易被误导的点路由助手route helpers和那些假设根路径为/的 request specs 会给你错误信号因为应用实际上是被“挂载”在该前缀下的。核心实现位于 config/initializers/tenanting/account_slug.rbmodule AccountSlug PATTERN /(\d)/ PATH_INFO_MATCH /\A(\/#{AccountSlug::PATTERN})/ class Extractor def initialize(app) app app end def call(env) request ActionDispatch::Request.new(env) # $1, $2, $ script_name, slug, path_info if request.script_name request.script_name ~ PATH_INFO_MATCH env[fizzy.external_account_id] AccountSlug.decode($2) elsif request.path_info ~ PATH_INFO_MATCH # Yanks the prefix off PATH_INFO and move it to SCRIPT_NAME request.engine_script_name request.script_name $1 request.path_info $.empty? ? / : $ env[fizzy.external_account_id] AccountSlug.decode($2) end if env[fizzy.external_account_id] account Account.find_by(external_account_id: env[fizzy.external_account_id]) Current.with_account(account) do app.call env end else Current.without_account do app.call env end end end end end Rails.application.config.middleware.insert_after Rack::TempfileReaper, AccountSlug::Extractor其原理可以拆成三步拦截前缀从PATH_INFO中把形如/123的账户前缀“拽出来”写入SCRIPT_NAME使 Rails 表现得像被挂载在该路径下设置租户上下文解码出的external_account_id存入env[fizzy.external_account_id]随后以Current.with_account(account)包裹整个请求处理全局Current.account由此建立保持链路完整脚本名分支专门处理 Action Cable 升级重连这类场景避免前缀信息在长连接场景中丢失。配套的中间件测试在 test/middleware/account_slug_extractor_test.rb。AGENTS.md 还点出三个关键推论数据隔离的全局例外Domain 记录业务数据按账户隔离而Identity、Session、认证相关记录是全局的Identity 与 User 是多对多关系一个基于邮箱的全局Identity可以持有多个账户下的User所以“一个邮箱地址 ≠ 一个账户成员身份”面板访问控制按用户的Access记录per-user进行授权而非按账户整体授权。另外后台任务Background jobs需要自行序列化并恢复Current.account——这是多租户上下文在异步执行边界上最容易丢的地方AGENTS.md 特意提醒。四、UUIDv7 主键25 字符的 base36 编码AGENTS.md 指出所有表都使用 UUIDv7 主键并以 base36 编码为 25 个字符。fixture 中的 UUID 被特意生成得比任何运行时记录更“老”以保证测试中.first/.last的顺序是确定的——所以不要试图通过对比插入顺序与 id 顺序去“修正”排序。仓库层面有两处实现与之一一对应适配器层config/initializers/uuid_primary_keys.rbMySQL 下把binary(16)识别为 UUID 类型、SQLite 下把blob(16)识别为 UUID 类型并让 schema dump 将这两者映射回:uuid同时为 UUID 主键注册默认值生成器。类型编码层lib/rails_ext/active_record_uuid_type.rb实现 UUID 的 hex 表示与 base36 字符串表示之间的互转hex_to_base36/base36_to_hex并打包成 MySQL 的二进制存储。对应的单元测试位于 test/lib/rails_ext/active_record_uuid_type_test.rb。打开 db/schema.rb 可以看到所有表都以id: :uuid定义如accesses、account_cancellations、account_external_id_sequences等印证了“全表 UUID 主键”这一全局约定。UUIDv7 本身按时间排序生成配合 base36 压缩为 25 字符兼顾了数据库索引友好性与 URL 长度。五、搜索MySQL 上 16 路分片SQLite 上单 FTS5 索引AGENTS.md 明确全文搜索跑在数据库里而不是 Elasticsearch。模型全部位于 app/models/search/包含record、highlighter、query、result、stemmer等组件。而且两种数据库的形态截然不同不要假设在 SQLite 下也看到分片结构MySQLSearch::Record::Trilogy按账户 ID 的 CRC32 取模分 16 路分片SHARD_COUNT 16见 app/models/search/record/trilogy.rbSHARD_COUNT 16 scope :matching, -(query, account_id) do full_query account#{account_id} (#{Search::Stemmer.stem(query)}) where(MATCH(#{table_name}.account_key, #{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE), full_query) end SHARD_CLASSES SHARD_COUNT.times.map do |shard_id| Class.new(self) do self.table_name search_records_#{shard_id} # ... end end.freeze def shard_id_for_account(account_id) Zlib.crc32(account_id.to_s) % SHARD_COUNT end def for(account_id) SHARD_CLASSES[shard_id_for_account(account_id)] end每个账户通过shard_id_for_account被稳定映射到search_records_0至search_records_15中的某一张表写入时以account_key account#{account_id}打标查询时用 MySQL 的MATCH ... AGAINST(... IN BOOLEAN MODE)同时匹配账户键、正文与标题并结合 Search::Stemmer 做词干化预处理。SQLiteSearch::Record::SQLite单 FTS5 虚拟表索引见 app/models/search/record/sqlite.rb 与 app/models/search/record/sqlite/fts.rb。FTS5 表search_records_fts通过rowid关联主表保存时用INSERT OR REPLACE做 upsert查询走INNER JOIN ... MATCH ?高亮/摘要则直接借助 FTS5 内置的highlight与snippet函数再由escape_fts_highlight做 HTML 转义与标记还原。结合 db/migrate/20251112093037_create_search_indices.rb、20251113190256_create_search_record_shards.rb 与 20251120110206_add_search_records.rb 等迁移可以看到这套分片与索引体系是如何一步步落库的。对开发者而言这段提示的核心价值在于调试搜索时先确认当前跑在哪个数据库适配器上再决定用分片思维还是单索引思维。六、导入导出本地与 S3 双存储数百 GB 也要流式处理AGENTS.md 对数据迁移给出了硬性要求实例间数据传输app/models/account/data_transfer/、app/models/zip_file必须同时兼容本地磁盘与 S3 存储且归档文件可能超过数百 GB——必须流式stream处理绝不能把整个文件缓冲进内存。从目录结构看数据导出的组织方式相当模块化app/models/account/data_transfer/ 下按记录类型拆分为account_record_set.rb、user_record_set.rb、entropy_record_set.rb、manifest.rb、record_set.rb并针对 Action Text 富文本action_text/rich_text_record_set.rb与 Active Storage 附件active_storage/attachment_record_set.rb、blob_record_set.rb、file_record_set.rb做了专门处理app/models/zip_file/writer.rb 是流式写出的核心它持有ZipKit::Streamer通过stream_to(io)暴露块接口把每个文件条目逐段写入输出 IO而不是先攒成完整归档再落地def stream_to(io) # ... streamer.public_send(write_method, path) { |sink| yield sink } # ... streamer.close end def streamer streamer || ZipKit::Streamer.new(output_io) end这也解释了 AGENTS.md 为什么把“stream, never buffer a whole file”当成不可违反的工程约束在数百 GB 的归档场景下任何整文件缓冲都会直接打爆内存。相关导出模型见 app/models/export.rb、导入模型见 app/models/account/import.rb迁移 20251223000002_create_account_imports.rb 与failure_reason字段的加入佐证了导入状态的演进。七、代码风格先读 STYLE.mdAGENTS.md 的最后一节是协作约定编辑或审查代码之前先读 STYLE.md。从 STYLE.md 可以看到该项目明确偏好的风格取向条件返回倾向展开的条件分支而非 guard clause方法开头的提前返回除外方法排序类方法 → 公有方法initialize置顶→ 私有方法且按调用顺序垂直排列!命名只有存在无!的对应方法时才使用!不把!当作“破坏性操作”标记CRUD 控制器端点建模为 REST 资源操作动作无法对应标准 CRUD 动词时就引入新资源而不是添加自定义动作例如用resource :closure代替post :close/post :reopen控制器与模型交互坚持 vanilla Rails 的“薄控制器 富领域模型”不引入服务对象层异步操作工作类job保持浅层逻辑委托给领域模型常用_later后缀表示入队、_now后缀表示同步执行。这些约定与 AGENTS.md“默认值而非铁律”的基调一脉相承先理解约定再在代码事实面前灵活取舍。八、小结一张可以照着走的架构速查表回看 AGENTS.md 全文它实际上是用最小的篇幅锁定了 Fizzy 里最“反直觉”的五个事实部署默认分支main自托管走 Kamalconfig/deploy.yml本地 Agent 开发先看tmp/saas.txt是否存在再决定是否读saas/AGENTS.md租户多租户是 URL 化的AccountSlug::Extractor把前缀从PATH_INFO搬到SCRIPT_NAME业务数据按账户隔离、认证数据全局共享后台任务要自己恢复Current.account主键全表 UUIDv7 base36 的 25 字符 idfixture UUID 特意“更老”以保证测试顺序稳定搜索数据库内全文搜索MySQL 按账户 CRC32 分 16 片、SQLite 用单一 FTS5 表二者形态不同不可互相套用迁移导入导出要同时兼容本地与 S3且归档可能达数百 GB必须流式处理。对于将要为 Fizzy 贡献代码、修复 bug 或搭建自托管实例的开发者与 Agent 而言这五条正是最容易“想当然”而犯错的地方。对照本文给出的源码路径逐条验证就能把 AGENTS.md 里的每一条约定都落到具体实现上从而安全地在这个看板应用中动手改造。【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表