给内部知识库配个售后运维平台:双系统架构实践

📅 2026/7/20 12:31:18
给内部知识库配个售后运维平台:双系统架构实践
系列文章:上上一篇: 《做了个内部知识库,部门人员都在用》上一篇: 《内部知识库跑一段时间后:我用 SQLite 替换了 JSON 存储,路由层 0 改动》仓库:xxx(私有) · 阶段:V3.0 · 技术栈:Python 3.10 Flask SQLite 单进程双系统一、V1 埋的种子,V3.0 才浇水第 1 篇说后来加 OPS(售后运维)的时候,就是直接多一个 Tab,架构没动——轻飘飘一句,真到 V3.0 切完 SQLite 准备动手,3 个真问题摆在面前:1、怎么加一个 Tab:KBMS 路由树已经写死 /knowledge/xxx,OPS 是不是要 /ops/xxx?那 PORTAL 怎么变。2、18 个 OPS 路由怎么解:[OPS-DISABLED] 占位路由已经躺了 8 个月,激活不是改 URL 路径——是整套子系统拉起来。3、切换怎么无感:用户登录 KBMS 中途想看工单,怎么切过去?重新登录?session 切换?Portal 落地?最终方案:单进程双系统-一个 Flask app 里同时跑 KBMS 和 OPS,共用 user/session/DB,Portal 是登录后的系统桌面,用户卡片级选系统、路由级校验权限。不是微服务、不是前后端分离、不是子域名—是 1 个 Python 进程、1 个 SQLite 文件、1 套用户表、2 个独立子系统。二、关键决策:PORTAL 当落地点,不当侧边栏V2 的时候首页直接是 KBMS,V3.0 加 OPS 之前必须先做 PORTAL——这是产品上的做对的事:app.route(/)def portal():if not current_user.is_authenticated:return redirect(/login)return render_template(portal.html, systems...)app.route(/switch/system)login_requireddef switch_system(system):总开关:切 active_system 写到 sessionif system not in (kbms, ops):abort(404)if not has_system_access(current_user.id, system):flash(没有权限进入该系统, warning)return redirect(/)session[active_system] systemreturn redirect(f/{system}/)3 个关键设计:PORTAL 独立成页,不进侧边栏:侧边栏是子系统内部导航(知识库的分类树、OPS 的工单看板),PORTAL 是系统桌面——视觉上像桌面,逻辑上落地即选项。1、session 记 active_system:用户进 KBMS 中途想看 OPS,点 OPS 卡片 → 跳 /switch/ops → 写 session → 跳 /ops/。回来点 KBMS 卡片 → 反向流程。2、路由级权限双层校验:Portal 卡片级(has_system_access 决定卡片显示) 路由级(ops_required 装饰器兜底)——卡片隐藏不算安全,路由必须真校验。V1 预留的用户 union 模式在这里派上用场:1 个 user 可以同时属于 KBMS 和 OPS,但只读角色(readonly)只能进 KBMS。user_systems 表多对多存关系,改 user 不改 session,改 session 不改 user。三、关键决策:18 个 [OPS-DISABLED] 路由,1 个开关全解V1 写 OPS 的时候只搭了路由骨架,具体逻辑全是占位——8 个月里 18 个路由一直挂着 [OPS-DISABLED] 标签:# V1 时期,V2.X 一直这样app.route(/ops/tickets)login_requireddef ops_tickets():if not has_system_access(current_user.id, ops):abort(403)return [OPS-DISABLED] 工单看板待开发V3.0 激活的核心动作不是写新代码,是把这些占位路由填上——但填法很关键:不能一个个改(18 个路由,改完发现权限漏校验又得回头),得整体性激活。做法:1 次 PR,18 个路由 18 个模板 1 个 /switch/system 总开关 portal 卡片解锁 侧边栏 3 组默认 classopen LAYOUT_BOTTOM onHome 判定修正。所有动作 1 个 commit 完成,1 个文件搞定。为什么这么干:不分散提交:18 个路由分散提 18 个 PR,中间状态(部分激活)用户登录会看到一半活的 OPS 一半死的——坑爹不拆子系统为独立 Blueprint:第 1 篇5 个我做错的事里第一条就是没用 Blueprint——这次 V3.0 不拆,先在单 app 里把 OPS 跑通,V4.0 再拆 Blueprint/switch/system 当总开关:任何系统级操作(改用户权限、关子系统维护)都过这个入口,未来做灰度发布也靠这个路由实际效果:激活前: 18 个路由全返 [OPS-DISABLED] 占位激活后: 18 个路由全活,PORTAL 落地可点 /switch/ops 切到 /ops/这是 V1 预留架构的胜利——预留不是白写,是 8 个月后的激活不用返工。四、关键决策:/switch/system 总开关 session 双写最朴素的设计:用户进 Portal 点 OPS → 直接跳 /ops/。问题:用户从 OPS 切到 KBMS 再切回来,active_system 状态怎么记?-- db.py 加 1 张表CREATE TABLE user_sessions (user_id TEXT PRIMARY KEY,active_system TEXT NOT NULL DEFAULT kbms,last_switch_at TEXT,FOREIGN KEY (user_id) REFERENCES users(id)); session 字典双写:app.route(/switch/system)login_requireddef switch_system(system):if system not in (kbms, ops):abort(404)if not has_system_access(current_user.id, system):flash(没有权限进入该系统, warning)return redirect(/)# 1) 写 sessionsession[active_system] system# 2) 写 db (下次登录恢复)save_data(user_sessions, [{id: current_user.id,active_system: system,last_switch_at: datetime.now().isoformat(),}], modeupsert)return redirect(f/{system}/)为什么双写:session 是当前会话的快速通道:每次路由渲染查 session[active_system],快db 是下次登录的恢复源:session 过期/换浏览器,db 记的 active_system 还在UPSERT 默认模式不删 db 现有行:save_data(..., modeupsert) 不会误删其他字段——比如 user_sessions 同时记 last_switch_at,只覆盖 active_system 不丢历史LAYOUT_BOTTOM 里的 onHome 判定也调整:# 改前def is_home(path):return path in (/, /portal)# 改后def is_home(path):return path in (/, /portal, /switch/kbms, /switch/ops)—切换路由算系统级操作,不该触发内容区的返回首页按钮。五、激活后压测:双系统跑得动吗?V3.0 切完 SQLite 激活 OPS,担心的是双系统资源争抢——1 个 SQLite 文件、2 个子系统同时读写,WAL 还撑得住吗?10 线程 × 50 写 500 次并发写 (跨 KBMS OPS):KBMS 写 (文档编辑): 250 次, 0 丢失OPS 写 (工单创建): 250 次, 0 丢失总耗时: 8.3s写 P95: 478ms写 P99: 921ms写 MAX: 1832ms5 读线程 × 3s:KBMS 读 (列表/详情): 3870 次OPS 读 (看板/工单): 2410 次读 P95: 42ms (KBMS) / 78ms (OPS)读 P99: 156ms结论:单进程双系统不爆。WAL busy_timeout5000 把 KBMS 写和 OPS 写串行化,读 P95 100ms——内部工具完全够用。stress_test.py 跑法:# 双系统混合压测import threading, randomKBMS_KEYS [knowledges, comments, kb_views]OPS_KEYS [tickets, ticket_logs, customers]def writer():for _ in range(50):key random.choice(KBMS_KEYS OPS_KEYS)new {id: ft_{uuid.uuid4().hex[:8]}, ts: time.time()}save_data(key, [new], modeupsert)跑出来 0 丢失——验证单进程双系统在同一 SQLite 上是稳的。六、5 个我做对的事① V1 预留架构,8 个月后激活零返工1 个 commit,18 个路由 18 个模板 1 个总开关 卡片解锁 侧边栏 LAYOUT_BOTTOM——预留架构是写过没浪费的代码。② PORTAL 独立成页,不进侧边栏侧边栏是子系统内部导航,PORTAL 是系统桌面——加任何子系统都不需要改 KBMS / OPS 的代码。③ /switch/system 总开关统一入口所有系统级操作走这个路由,未来加灰度发布、加 A/B 测试、加系统级通知,都靠这个入口。④ session db 双写 active_systemsession 走当前会话(快),db 记下次登录状态(持久)——双写不冲突,因为 UPSERT 默认模式不删其他字段。⑤ 路由级权限双层校验(卡片级 路由级)Portal 卡片控制显示(ops_required 装饰器兜底)——卡片隐藏不算安全,路由必须真校验。七、3 个我做错的事① 侧边栏 3 组没默认展开激活 OPS 后,实施反馈第一次进 OPS 不知道点哪里——3 组菜单的 classopen 漏写,所有 OPS 用户第一次都得手动展开。教训:新子系统激活,默认展开是产品细节不是技术细节。② PORTAL 落地有反人类动效第一次做 PORTAL 用了 0.5s 渐显,体感是卡顿——改成 0.1s 闪入,大家都说快。③ 没做系统级权限矩阵激活前只想了谁能进 OPS,没想谁能进 OPS 的报表——后来发现 5 个角色里有 1 个角色(只读)能进 OPS 看板但不能看报表。教训:权限矩阵要先画再写代码,不能边写边补。八、数据V3.0 激活 OPS 后:知识库文档:1.8 万 条(V3.0 切完是 1.5 万,涨 3000);售后工单:1.2 万 条(从 0 涨到 1.2 万——实施在用);客户档案:340 条(从 0 涨到 340);产品/服务类型:60 条;操作日志:35 万 条(从 21 万涨到 35 万);CPU 平均 11%(激活前 8%,涨 3%——双系统预期内);内存 680MB(激活前 600MB,涨 80MB);零数据丢失,零故障;九、下一步报表系统独立(从 OPS 里拆)移动端适配(实施出差在外接工单)真·多租户(对伙伴公司开放)双系统预留 → 激活你团队是怎么做的?是 V1 就预留架构,还是需要时重构?评论区聊聊是PORTAL 怎么设计才不像钉钉、还是sessiondb 双写 active_system 怎么不冲突、还是单进程双系统并发怎么压测——挑你最痛的。版权声明:本文为 CSDN 博主「杂记栈」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。原文链接:给内部知识库配个售后运维平台:双系统架构实践-CSDN博客