ARTICLE DETAIL

资讯详情

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

Wekan 持久化架构修复清单深度解读:从数据审计到用户级撤销/重做

Wekan 持久化架构修复清单深度解读:从数据审计到用户级撤销/重做 Wekan 持久化架构修复清单深度解读从数据审计到用户级撤销/重做【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan本篇文章以 Wekan 安全审计文档 FIXES_CHECKLIST.md 为核心骨架结合其配套的 PERSISTENCE_AUDIT.md 与 ARCHITECTURE_IMPROVEMENTS.md 以及仓库源码系统梳理 2025-12-23 数据持久化审计中发现的 6 大问题及修复方案看板级 UI 状态与用户级偏好的彻底分离、localStorage 校验与清理、用户级位置历史UserPositionHistory集合与撤销/重做能力、swimlaneId 数据救援迁移。读完本文你将掌握 Wekan 数据持久化层的架构边界、源码级实现原理以及一套可直接落地的数据完整性与安全加固清单。一、审计背景为什么需要这次持久化修复Wekan 的看板数据由多个 MongoDB 集合承载包括泳道swimlanes、列表lists、卡片cards、检查清单checklists与检查清单项checklistItems。它们共同维护标题、排序sort、颜色、归档等看板级数据与此同时折叠状态、列表宽度、泳道高度等 UI 偏好则具有明显的用户级属性。审计发现的问题集中在两者边界模糊导致的三大类缺陷折叠状态存储位置冲突泳道与列表的 schema 中存在看板级collapsed字段但客户端读取逻辑isCollapsed()优先使用用户 profile 中的值导致一个人折叠、所有人被折叠或状态不持久的行为混乱localStorage 无校验未登录用户的 UI 偏好存放在 localStorage没有任何范围校验与清理机制可能被写入非法值甚至撑爆浏览器配额位置历史缺失仅有记录原始位置的 models/positionHistory.js无法支撑撤销/重做与操作审计。完整审计结论见 PERSISTENCE_AUDIT.md修复实施指南见 ARCHITECTURE_IMPROVEMENTS.md。二、Issue #1消除看板级折叠状态确立UI 偏好按用户存储原则2.1 问题本质models/swimlanes.js 与 models/lists.js 原先在 schema 中定义了看板级collapsed字段默认false而isCollapsed()辅助方法却优先读取用户级存储二者不一致直接造成折叠状态既不是真正的看板级、也不是可靠的用户级。2.2 修复方案从泳道与列表 schema 中移除看板级collapsed字段并在 schema 中留下明确注释。当前源码中 models/swimlanes.js 与 models/lists.js 均注释// NOTE: collapsed state is per-user only, stored in user profile.collapsedLists // and localStorage for non-logged-in users移除泳道的collapse()变更方法models/swimlanes.js 注释说明改为调用user.setCollapsedSwimlane(boardId, swimlaneId, collapsed)移除 REST API 中对列表collapsed字段的处理保留isCollapsed()的三级读取策略登录用户优先读 profileprofile.collapsedLists/profile.collapsedSwimlanes未登录用户回退到 CookieUsers.getPublicCollapsedList()/Users.getPublicCollapsedSwimlane()最终兜底字段值。2.3 源码佐证用户级存储的实际落点位于 models/users.jsprofile.collapsedLists结构collapsedLists[boardId][listId] booleanprofile.collapsedSwimlanes结构collapsedSwimlanes[boardId][swimlaneId] boolean读取辅助方法getCollapsedListFromStorage()/getCollapsedSwimlaneFromStorage()models/users.js会先检查是否为布尔值防止脏数据污染 UI。架构结论折叠状态是用户级 UI 偏好属于profile子文档而标题、排序、颜色、归档、泳道高度、列表宽度等是看板级数据直接落在各自集合文档中。三、Issue #2localStorage 全量校验与每日自动清理3.1 校验器实现新增 client/lib/localStorageValidator.js对 5 个 localStorage key 做结构与范围校验localStorage key含义校验规则wekan-swimlane-heights泳道高度数字-1自动或50–2000像素wekan-list-widths列表宽度数字100–1000像素wekan-list-constraints列表最大宽度约束同列表宽度规则wekan-collapsed-lists列表折叠状态严格布尔值wekan-collapsed-swimlanes泳道折叠状态严格布尔值核心校验函数源码中isValidNumber要求typeof value number、非NaN、有限值且在[min, max]区间isValidBoolean要求typeof value boolean非法类型、越界数值、损坏的 JSON 会被静默清除符合 Invalid data: silently removed 策略超过配额的旧数据按最旧优先裁掉。3.2 配额与清理策略源码常量定义了硬性配额MAX_BOARDS_PER_KEY 50每个 key 最多保留 50 个看板MAX_ITEMS_PER_BOARD 100每个看板最多保留 100 条项目记录全部 5 个 key 合计上限即50 × 100 × 5 25000条文档中按 50 boards × 100 items 折算为 5000 条目上限以列表/泳道各占一类计MAX_AGE_MS 90 天超过 90 天的历史数据也会被清理shouldRunCleanup()通过wekan-last-cleanup时间戳判断每天最多执行一次模块加载时自动在Meteor.startup中挂载清理逻辑if (Meteor.isClient)分支。3.3 用户级读写封装新增 models/lib/userStorageHelpers.js提供带边界检查的读写函数getValidatedNumber(key, boardId, itemId, defaultValue, min, max)读取时校验类型、有限性与[min, max]范围越界返回默认值setValidatedNumber(...)写入前校验非法值拒绝写入并输出console.warngetValidatedBoolean(...)/setValidatedBoolean(...)布尔值类型强校验。这套封装同时被登录用户 profile 存储与未登录用户 localStorage 存储复用从读写两端杜绝脏数据。四、Issue #3用户级位置历史 UserPositionHistory 与撤销/重做4.1 新集合设计新增 models/userPositionHistory.js与仅记录初始位置的 models/positionHistory.js 不同它专门记录每次位置变更支撑撤销/重做。核心字段字段类型说明userIdString变更者强制隔离维度boardIdString变更所在看板entityTypeStringswimlane/list/card/checklist/checklistItementityIdString被移动实体 IDactionTypeStringmove/create/delete/restore/archivepreviousState/newStateObjectblackbox变更前后完整状态快照previousSort/newSortNumber变更前后排序值previousSwimlaneId/newSwimlaneId等String跨泳道/列表/看板移动记录isCheckpoint/checkpointNameBoolean / String用户标记的检查点保存点batchIdString批量操作分组 IDundone/undoneAtBoolean / Date撤销栈/重做栈状态#64784.2 自动追踪集成卡片移动card.move()models/cards.js在服务端完成更新后会捕获previousState含boardId/swimlaneId/listId/sort随后调用UserPositionHistory.trackChange(...)记录变更并通过recordCardChange双写旧存储作为过渡期兼容const UserPositionHistory require(/models/userPositionHistory).default; UserPositionHistory.trackChange({ userId: Meteor.userId(), boardId: this.boardId, entityType: card, entityId: this._id, actionType: move, previousState, newState: { boardId, swimlaneId, listId, sort }, });值得注意的实现细节该处使用惰性 require而非顶层 import加载userPositionHistory因为 models/userPositionHistory.js 反向依赖本文件顶层 import 会形成循环依赖可能得到undefined绑定。trackChange定义于 server/models/userPositionHistory.js在写入新变更前会先清除该用户看板下所有undone: true的记录保证新的真实变更会清空重做栈避免重做到过期状态。4.3 撤销/重做与检查点辅助方法models/userPositionHistory.jsgetDescription()生成人类可读描述move card to different list 等canUndo()通过ReactiveCache检查实体是否仍然存在undo()/redo()按entityType分支恢复previous*/new*字段对delete/restore动作配合列表软删除deletedAt/deletedBy/deleteBatchId及其卡片级联批处理。Meteor 方法server/models/userPositionHistory.js方法功能userPositionHistory.createCheckpoint(boardId, checkpointName)创建命名检查点保存点userPositionHistory.undo(historyId)撤销指定历史记录userPositionHistory.undoLast(boardId)撤销该看板最近一次未撤销变更CtrlZ 路径#6478userPositionHistory.redoLast(boardId)重做最近一次被撤销的变更CtrlY 路径userPositionHistory.getRecent(boardId, limit)获取最近变更limit上限 100userPositionHistory.getCheckpoints(boardId)获取全部检查点userPositionHistory.restoreToCheckpoint(checkpointId)回滚到检查点逐个撤销其后所有可撤销变更4.4 索引与自动清理server/models/userPositionHistory.js 在启动时通过ensureIndex建立 5 个索引{userId, boardId, createdAt:-1}、{userId, entityType, entityId}、{userId, isCheckpoint}、{batchId}、{createdAt}。清理逻辑UserPositionHistory.cleanup()每 24 小时运行一次可通过Meteor.settings.public.enableHistoryCleanup: false关闭规则每个用户、每个看板保留最近 1000 条非检查点记录更早的删除检查点永不删除查询条件带isCheckpoint: { $ne: true }。4.5 安全设计所有方法先调用requireBoardVisible(userId, boardId)server/models/userPositionHistory.js校验board.isVisibleBy()防止越权读写任意看板的历史undo/getRecent/getCheckpoints查询均带userId: this.userId过滤用户只能操作自己的历史undo()在跨看板恢复位置时boardId ! card.boardId会在运行时二次校验目标看板成员资格拦截伪造或过期历史条目引发的跨板越权触发 canary 告警history.cross-board对应安全测试见 server/lib/tests/clonebleed.security.tests.js。五、Issue #4swimlaneId 校验与孤儿数据救援迁移5.1 迁移目标新增迁移 server/migrations/ensureValidSwimlaneIds.js导出runEnsureValidSwimlaneIdsMigration()含MIGRATION_NAME ensure-valid-swimlane-ids、MIGRATION_VERSION 1通过migrations集合记录完成状态{ name: ensure-valid-swimlane-ids, version: 1, completedAt: Date, results: { cardsFixed: Number, listsFixed: Number, cardsRescued: Number } }迁移幂等已存在且version 1时直接返回{ alreadyCompleted: true, ...results }。5.2 三类修复操作修复缺失 swimlaneId 的卡片查找swimlaneId不存在 /null/的卡片分配到该看板排序最前的非模板、未归档泳道若看板没有任何泳道则自动创建名为Defaultsort: 0的泳道修复缺失 swimlaneId 的列表置为空字符串保持列表可跨泳道共享的向后兼容模型救援孤儿卡片对swimlaneId指向已删除泳道的卡片自动创建红色、置于末尾sort: 9999999的Rescued Data (Missing Swimlane)泳道并迁移过去同时写入 Activities 记录createSwimlane/moveCard保证透明可审计。5.3 永久校验钩子迁移除一次性修复外还会安装常驻校验钩子Meteor.startup时执行addSwimlaneIdValidationHooks()Cards.before.insert客户端提交的swimlaneId若指向已删除/已归档的泳道自动回退到看板第一个可见泳道对应 #1959/#1971 的坑位完全没有swimlaneId时自动分配默认泳道Cards.before.update阻止$unset.swimlaneId若$set.swimlaneId null则替换为默认泳道 ID。现状说明源码中原本服务端启动自动跑全量迁移的代码已注释禁用见 server/migrations/ensureValidSwimlaneIds.js迁移函数改由管理面板 / Cron / Run All Migrations 驱动校验钩子始终随启动安装持续保障数据完整性。六、Issue #5 #6启动期错误与引用错误修复FIXES_CHECKLIST 还记录了两次运行期崩溃的修复Migrations.findOne is not a functionMongo 集合定义必须置于文件顶部server/migrations/ensureValidSwimlaneIds.js确保在任何使用前已完成实例化UserPositionHistory 引用错误移除 ES6 export 全局假设、采用默认导出并对ChecklistItems等可能未加载的引用添加typeof防御性检查models/userPositionHistory.js 的if (typeof ChecklistItems ! undefined)。这两类问题本质上都是 Meteor 模块加载时序与循环依赖问题修复策略可归纳为定义前置 惰性引用 防御检查。七、数据校验与配额规则汇总7.1 校验范围速查表数据类型存储位置校验规则列表宽度localStorage profile100–1000像素列表约束localStorage100–1000像素泳道高度localStorage profile-1自动或50–2000像素折叠列表 / 折叠泳道localStorage profile严格布尔值卡片swimlaneIdMongoDB必填合法 ObjectIdinsert/update 钩子保证列表swimlaneIdMongoDB可选合法 ObjectId 或7.2 清理规则localStorage损坏/非法类型/越界数据直接移除超 50 看板删最旧每看板超 100 条删最旧每天至多清理一次UserPositionHistory每用户每看板保留最近 1000 条检查点保留每天清理一次查询上限getRecent强制Math.min(limit, 100)。7.3 性能与容量基线localStorage 单 key 上限 50 看板 × 100 条位置历史每用户每看板 1000 条检查点除外5 个复合索引保障检索性能历史查询结果受限支持分页。八、部署就绪度评估与后续路线依据 FIXES_CHECKLIST.md 与 COMPLETION_SUMMARY.md已完成看板级折叠状态清除、localStorage 全量校验与清理、UserPositionHistory 集合与撤销/重做含检查点、批处理、用户隔离、自动清理、Meteor 方法、swimlaneId 校验钩子与救援迁移、Migrations 与 UserPositionHistory 引用错误修复进行中撤销/重做 UI 组件工具栏按钮、历史侧边栏、键盘快捷键CtrlZ / CtrlShiftZ / CtrlShiftS规划中字段级历史对description、title、comments等做历史追踪并纳入全部看板搜索、跨用户协作撤销、历史导出CSV/JSON、可视化时间线回放测试缺口单元测试localStorageValidator.js、userStorageHelpers.js、userPositionHistory.js、ensureValidSwimlaneIds.js与集成测试仍未创建属于部署前 TODO迁移对现有安装的影响ensureValidSwimlaneIds迁移与 localStorage 清理均自动化无需人工操作已有看板级collapsed值会被忽略用户的折叠偏好继续从 profile/Cookie/localStorage 读取保持向后兼容。九、开发者速查为新增用户级偏好接入校验体系按 ARCHITECTURE_IMPROVEMENTS.md 第 8 节新增一个用户级偏好需要四步扩展 profile schemamodels/users.jsprofile.myNewPreference: { type: Object, optional: true, blackbox: true, }编写校验函数在userStorageHelpers或本地实现validateMyNewPreference(data)校验结构并返回清洗后的数据读写双通道登录用户走 profile未登录用户走 localStoragegetMyNewPreferenceFromStorage(boardId, itemId) { if (this._id) { return this.getMyNewPreference(boardId, itemId); } return getValidatedData(wekan-my-preference, validators.myPreference); }注册清理在 client/lib/localStorageValidator.js 的validateAndCleanLocalStorage()中加入对应的validateAndCleanKey(key, validator)调用纳入每日清理与配额管理。这套schema 定义 双层存储 读写校验 启动清理的闭环就是本次审计沉淀下的标准范式。十、问题排查速查表现象根因修复Migrations.findOne is not a function集合在文件底部才定义将new Mongo.Collection(migrations)移到文件顶部UserPositionHistory not foundES6 export 假设 / 循环依赖使用默认导出 惰性 requireChecklistItems undefined引用时机过早添加typeof防御检查localStorage 配额超限数据无上限增长50 看板 / 100 条每看板 / 每日清理折叠状态不持久看板级与用户级存储冲突移除看板级collapsed统一走 profile/Cookie拖拽卡片后 CtrlZ 无效typeof UserPositionHistory恒为 false改为惰性require#6478跨看板撤销越权风险历史条目可被 DDP 伪造undo()运行时二次校验目标看板成员资格结语本次 2025-12-23 的持久化审计与修复确立了 Wekan 数据层的一条根本原则看板数据标题、排序、颜色、归档、高度、宽度按看板共享持久化UI 偏好折叠状态等按用户隔离存储。在此基础上补齐了 localStorage 全量校验、用户级位置历史与撤销/重做、swimlaneId 完整性保障三块关键能力。对部署方而言迁移全程自动化、向后兼容对开发者而言UserPositionHistory的索引、清理、权限钩子与localStorageValidator的配额策略构成了可直接复用的安全范式剩余工作集中在测试覆盖与撤销/重做 UI 的落地。Last Updated: 2025-12-23 ·Status: ✅ COMPLETE AND READY【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表