CGL SCRM  /  architecture

架构

请求链路、部署拓扑与运行时

一次访问,系统做了什么

市场部同事在浏览器里点开一个链接,到他看见页面,中间大概走了这么几步。

1
浏览器
同事打开 scrm.jxccwx.com
2
域名中转
Cloudflare 把请求转给腾讯云上的服务
3
认出是谁
看浏览器带的登录凭证 → 认出是哪位同事
4
查数据
按他要看的页面去数据库取数
5
拼页面
把数据填进页面模板,生成整张 HTML
6
返回
浏览器直接显示,前端不用再拉一次数据
「拼好再发」这一点值得说:页面是在服务器上拼完整了才发给浏览器的, 不是先发一个空壳、再由浏览器去要数据。好处是打开快、也少了很多「转圈等数据」的中间状态。 代价是每次都要服务器算一遍——但这个量级完全扛得住。

系统分几层

从上到下,每一层只干自己那件事。

页面 / 接口
决定「哪些网址存在、每个网址干什么」。 共 17 个模块、199 个网址。 后台页面、手机上报名用的页面、给 AI 用的接口,都在这一层。
app/routers/ · 17 个文件 / 199 条路由
业务逻辑
真正的「怎么做」都在这里。 比如「生成一张二维码」「算这周新增了多少线索」「导出 Excel」。 页面层只负责调用,不写业务规则。
app/services/ · 15 个模块 / 162 个功能点
数据表
定义「数据长什么样」。 哪张表有哪些字段、什么类型、能不能为空。改这里的字段名会影响整个系统。
app/models/ · 69 张表 / 1,129 个字段
公共零件
各处都要用的小工具。 读配置、写日志、连数据库、算版本号、字典表(比如「线索状态」有哪几种)。
app/core/ · app/db/ · 9 + 若干模块

请求进来,先过这几道关

每个请求都依次经过下面这些「关卡」,然后才轮到具体页面处理。顺序是代码里定的,不是猜的。

1
inject_user
先认出「现在是谁在访问」——把当前登录用户挂到这次请求上(认不出就当没登录,不拦人)
2
FlashMiddleware
把上一步操作留下的提示语(「保存成功」这类)带过来,显示完就丢掉
3
RequestContextMiddleware
给这次请求编一个编号,之后所有日志都带上它,出问题能一条线串起来
4
SecurityHeadersMiddleware
给响应盖上几个安全头,告诉浏览器别乱猜内容类型、别被嵌进别人的页面
一个容易误会的地方:认出「谁在访问」的那道关只负责认,不负责拦。 就算没登录,它也只是记一句「这次是游客」,然后照样放行。 真正决定「这个页面能不能看」的,是每个页面自己挂的权限检查—— 这部分在「安全模型」那一页讲。

出错了会怎样

哪种情况谁来收拾
builtins.Exception unhandled_exc_handler
fastapi.exceptions.HTTPException http_exc_handler
fastapi.exceptions.RequestValidationError validation_exc_handler
fastapi.exceptions.WebSocketRequestValidationError websocket_request_validation_exception_handler
starlette.exceptions.HTTPException http_exception_handler

注意第二、三行看着名字一样,其实是两个不同的类(一个来自 FastAPI、一个来自 Starlette)—— 所以两行都得留着,不能合成一条。

设计上有一条明确的取向:「没登录」跳登录页、「参数填错」回一句人话、「真出了意外」给一个带编号的错误页。 出问题时那个编号能让人拿它去日志里搜,直接定位到是哪一次访问。

两个「体检」地址

这两个网址不给人看,是给监控和排查用的。它们不需要登录。

网址回答什么问题
/healthz「服务还活着吗?现在跑的是哪一版代码?」
/readyz「数据库连上了吗?微信配好了吗?图片存哪?字体在不在?有没有脏数据?」

为什么版本号这么重要

这套系统跑在云上,云平台自己显示的「运行中 / 创建中」判断不了新版本有没有真的生效—— 明明已经更新完了,它可能还显示「创建中」。所以代码里自己算了一个版本号(页脚那个 rev 9bcd3b57),改完就换一个值。 上线后打开这个网址一看,就知道线上到底是新的还是旧的。

一个必须说清楚的局限:这个版本号只跟着程序代码变。 如果你只改了配置、改了文档、改了这个展示站,版本号是不会动的。 所以「版本号没变」≠「什么都没改」,这两件事不能混。

页面长什么样,是在服务器上定好的

全站 56 个页面模板。 「后台的线索列表」「手机上报名的页面」「活动落地页」各有各的模板, 数据由服务器填进去,浏览器收到的是成品。

模板分组文件数大致管什么
(根) 4 公共骨架与错误页:每个页面都套用的外壳、404、500
auth 1 登录页
h5 8 客户在手机上看到的页面:活动落地、报名、问卷、分享
manager 43 后台页面:看板、线索、活动、问卷、内容、账号、审计、报表

数据存在哪

线上正式数据
PostgreSQL 数据库。程序不直接连它,而是走一条内部专门的数据通道 (这样能在不打开全部网络出口的前提下访问数据库,也少一层暴露面)。
开发和测试数据
本机一个单文件数据库,方便随时清空重来。跑自检时会先把里面清干净再灌测试数据, 跑完再按档案还原。
这里有个反复踩的坑,值得单独标出来: 「在本机跑得好好的,一上线就出问题」在这套系统上出现过八次。 原因都是两套环境走了不同的代码路径—— 比如本机数据库比较宽松、线上一丝不苟;本机时间是「不带时区」的、线上是「带时区」的。 所以规矩是:凡是动了数据、类型、时间、密钥的改动,必须单独再跑一遍真对线上的检查。

图片、二维码、海报存在哪

这些东西不放数据库(数据库只存「文件在哪」),实际文件放在对象存储里。 现在线上用的存储方式是:cos (腾讯云 COS);本机开发时用的是本地磁盘,访问前缀 /static/uploads。

挂载顺序曾经出过一次真事故:两个静态目录的注册顺序写反了, 长的那条被短的抢走,结果是「文件明明在硬盘上,页面上却是裂图」。 这类问题不会报错,只会默默显示不出来——所以顺序这件事在代码里专门留了注释。

系统里几个固定网址

网址前缀干什么用
/manager/*后台。市场部同事日常用的地方。
/video/*对外获客页面。客户在手机上点开报名、填问卷的地方。
/api/v1/*给程序用的接口(原来没有,这次新增)。
/mcp给 AI 助手用的入口(原来没有,这次新增)。
/Botch_qrcode/*二维码图直接生成——老名字,原样保留,老物料上的链接才不会失效。
/wechat微信服务号把事件推给我们的地方。
/healthz · /readyz体检地址,见上面。

前两层里带 /manager、/video、 /Botch_qrcode 的网址,都是照搬老系统的—— 包括「Botch」这个拼写错误,也一起照搬了。理由很简单: 老物料、老二维码、老脚本里写的都是这些地址,改了它们就等于把过去的东西全弄坏。