首页/技术架构

轻量栈,重负载

模块化单体 + PostgreSQL + 独立 Worker,不依赖 Redis / Kafka / Kubernetes。 不堆叠中间件,把硬件预算留给规则计算与数据库索引。

Stack

技术栈总览

每一层的选型都以"减少可故障部件"为目标。能用数据库做的事,不引入新的中间件。

层选型说明
前端 React 19.3 + Vite 6.4 唯一运行时依赖 lucide-react;无 UI 组件库、无 TypeScript、无 Redux / Zustand,状态来自服务端响应。
后端 Python 3.12 模块化单体 185 个 .py / 22,089 行。生产为 FastAPI + Uvicorn;单机模式使用标准库 HTTP。
存储(单机) SQLite 业务库 data/cpq.sqlite3 与控制库 data/control.sqlite3 分离。
存储(生产) PostgreSQL 17 强制:未设置 CPQ_DATABASE_URL 时,CPQ_ENV=production 启动失败。
表规模 38 张表 / 23 个索引 性能索引见下方清单。
文件层 blob_store 本地共享卷或 S3 兼容;租户前缀 CPQ_S3_PREFIX/{tenant}/{key};内容寻址读缓存。
后台任务 job_queue + 独立 Worker 子进程 数据库原子认领 SKIP LOCKED;租约 180 秒,每 10 秒续租,最多重试 3 次。
网关 Nginx 1.30.5 least_conn、keepalive 32、gzip(响应体 ≥1024 字节)、client_max_body_size 30m。
中间件依赖 无 不依赖 Redis / Kafka / Kubernetes。
出处:仓库 package.json、vite.config.js、pyproject.toml、server/asgi.py、server/storage.py、server/storage_schema.py、server/blob_store.py、server/job_queue.py、server/worker.py、deploy/nginx.conf。

性能索引(节选)

38 张表 / 23 个索引中,与列表、检索、授权直接相关的索引名如下。

quotes_owner_identity、quotes_updated、quotes_owner、quotes_scope、quotes_number、products_model、products_sku、lines_sku、grants_user、background_ready

文件层规则

  • 租户前缀CPQ_S3_PREFIX/{tenant}/{key}
  • 读缓存内容寻址
  • prune 范围仅 jobs/ 与 previews/
  • 绝不删除原始图片 / 资料 / 报价 / 审计
  • 写入前置saas_quota.reserve_file

后台任务语义

  • 认领为数据库原子操作(SKIP LOCKED),同一任务不会被两个 Worker 取走。
  • 租约 180 秒,每 10 秒续租;失去租约的旧进程不能覆盖新结果。
  • 长任务在独立子进程中运行,默认超时 120 秒;超时先 terminate 再 kill,并明确失败。
  • 幂等:相同 用户 + requestId + 参数去重,约束为 UNIQUE(actor, request_key)。
  • 重试上限 3 次。

网关热解析

upstream 使用 resolve 配合 resolver 127.0.0.11 valid=10s。 容器重建导致 IP 变化时,网关无需重启即可解析到新地址。

  • 负载均衡least_conn
  • 上游长连接keepalive 32
  • gzip 阈值≥ 1024 字节
  • 请求体上限30m
185 模块
后端 .py / 22,089 行
308 文件
前端 / 82,659 行
142 篇
设计文档(含 70 篇 P0–P63)
38 / 23
数据库表 / 索引
Process & ports

进程模型与端口

开发、单机与生产使用同一份镜像,差异只体现在配置与进程编排上。

端口用途
8765单机 / 原工作空间 HTTP:销售端 /sales、管理端 /admin、自助门户 /portal
8766多租户 SaaS:/workspaces(我的企业)、/platform(平台运营);企业入口 <企业地址>.localhost:8766
5173Vite dev server(开发时使用,/api 代理到 8765)
8000容器内 API(不对外暴露)
443Nginx HTTPS 网关(生产唯一公开入口)

生产拓扑

浏览器 → HTTPS 网关 → 无状态 API × 2 容器(每容器 2 个 Uvicorn 进程) → PostgreSQL 17。 独立 Worker 与报表调度器各自连库,不经过 API;API、Worker、调度器三者共享同一套文件存储。

浏览器HTTPS
→
网关Nginx 1.30.5 · 443
→
API × 2每容器 2 Uvicorn · 8000
→
PostgreSQL 17max_connections 100

API 无状态

会话与租户上下文不驻留进程内存;容器可随时替换。实测中容器 IP 变化后网关无需改动,登录会话持续有效。

Worker 独立

文档生成、报表导出等长任务在独立子进程中顺序执行,不占用 API 请求名额;默认超时 120 秒。

调度器独立

报表调度器单独连库,与 Worker 使用各自的连接池,互不挤占。

Deployment

三种部署形态(同一镜像)

从一台笔记本到多租户平台,镜像不变,变的是配置、进程编排与数据库。

01

单机 / 开发模式

  • SQLite,业务库与控制库分离
  • 双击 启动CPQ.command 启动
  • 后台任务同步执行,无需 Worker
  • 入口 127.0.0.1:8765
02

企业独立部署(生产)

  • Docker Compose(deploy/compose.yml)
  • API×2 + Worker + Scheduler + PostgreSQL 17 + Nginx
  • PostgreSQL 17 强制,网关只开 443
  • 连接预算 66 < 上限 100
03

多租户 SaaS

  • deploy/saas-compose.yml
  • 每 API 实例最多 10 个租户池 × 2 连接,平台池 4
  • 角色分离,API 生产启动拒绝开通凭据
  • 首期规模基线约 100 家企业

企业独立部署:服务与关键参数

服务关键参数
PostgreSQL 17 postgres:17-alpine;max_connections=100、shared_buffers=256MB
API × 2 uvicorn --workers 2 --timeout-keep-alive 30 --proxy-headers;CPQ_DB_POOL_SIZE=8、CPQ_CONTROL_POOL_SIZE=4、CPQ_REQUEST_CONCURRENCY=32
Worker 连接池 4;独立子进程执行长任务
Scheduler 连接池 2;报表调度
Nginx 网关 只开放 443;least_conn、keepalive 32、gzip
连接预算 48(API)+ 18(Worker / 调度器)= 66 < PostgreSQL 上限 100

多租户 SaaS:池与角色

  • 每 API 实例租户池最多 10 × 2 连接
  • 平台池4
  • 单进程请求并发上限CPQ_REQUEST_CONCURRENCY=16
  • 首期规模基线约 100 家企业

PostgreSQL 17 上已验证 100 家企业由持久开通任务创建。

角色分离(四种职责互不复用凭据):

  • 集群管理员
  • 迁移角色
  • 平台 DML 角色
  • 开通角色

API 在生产环境启动时拒绝开通凭据,开通能力与线上请求路径隔离。

套餐额度项免费版专业版
内部成员320
门户账号20200
目录产品2005,000
月新报价1002,000
月文档生成1002,000
文件容量1 GiB20 GiB
并发后台任务12
排队上限20100
每分钟 API 请求6003,000
额度由 saas_quota 校验,文件写入先经 reserve_file 预留容量。
部署边界

Compose 是单主机参考部署,不宣称主机级高可用。 RPO 24 小时 / RTO 4 小时仍是部署演练目标,不是已达成的 SLA。 未实现自助开通与 SaaS 计费;未测试跨地域数据库与中国办公网络环境。

Container & security baseline

容器与安全基线

以下为镜像构建、运行时约束、数据库权限与传输层的既定基线,逐项可核查。

镜像与构建

  • 两阶段构建:node:22-bookworm-slim 构建前端 → python:3.12-slim-bookworm 运行。
  • 安装 fonts-wqy-microhei,用于中文 PDF 字体。
  • 依赖安装:pip install --require-hashes -r requirements.lock(85 KB 锁定)。
  • 构建期执行 python -m compileall 预编译,避免任务子进程重复编译。
  • 运行身份 USER cpq(uid 10001,非 root)。

Compose 运行时约束

  • no-new-privilegestrue
  • cap_drop[ALL]
  • inittrue
  • mem_limit2g
  • stop_grace_period30s

数据库权限初始化

  • 应用角色 cpq:NOSUPERUSER NOCREATEDB NOCREATEROLE。
  • REVOKE ALL ON DATABASE ... FROM PUBLIC。
  • REVOKE CREATE ON SCHEMA public。
  • 管理员密码与应用密码必须是不同的随机值。
  • 不把管理员密码注入 API / Worker。

首次管理员

  • 通过 POST /api/auth/setup 创建,请求需带 X-CPQ-Setup-Token 头。
  • 成功后从运行配置移除 Token 并重建 API 容器。
  • 不提供默认生产账号 / 密码。
  • SSO 未实现,当前仅本机账号体系。
项基线
网关日志 只记录 $remote_addr "$method $uri" $status $request_time;不记录 Cookie、密码、请求体
上游重试 proxy_next_upstream error timeout;写请求失败不重放
传输 TLS 1.2 / 1.3
Cookie host-only + Secure + HttpOnly + __Host- 前缀
跨站与来源 CSRF 校验与来源检查
主机头 TrustedHostMiddleware 限定可信 Host
监控端点 /metrics 需 Bearer Token,未授权返回 404(不暴露端点存在)
请求体上限 30 MiB
过载保护 超载返回 503 + Retry-After,明确"服务繁忙"
Tenancy

数据隔离与多租户

租户不由请求参数决定。隔离在请求上下文与连接借出两个环节同时生效。

租户识别

  • 租户由可信 Host 映射(CPQ_TENANT_HOSTS)确定。
  • 请求参数不能选择租户。
  • 每次借出连接都在事务范围内设置 search_path。
  • 连接归还后不残留租户状态。

隔离维度

  • 会话、账号、报价、后台任务、缓存、文件分别隔离。
  • SaaS 每家企业拥有独立的 main / control schema。
  • 每家企业配备专属的 DML 运行角色。
PostgreSQL 17 实测

以运行角色尝试跨 schema 读取、写入、创建 schema、创建角色,四项均被拒绝。

隔离机制的边界

租户隔离由应用请求上下文实施,未使用 PostgreSQL RLS,也不向租户提供数据库账号。 合同要求数据库级隔离的客户,须使用独立数据库 + 独立应用实例。 当前未实现自助开通与 SaaS 计费。

Export limits

报告导出容量边界

超出上限时系统给出明确提示,不静默截断结果。

  • 普通报表 · 单次导出≤ 500,000 行
  • 复杂内置报表 · 单次导出≤ 200,000 行
  • 含动态商务字段的报表≤ 5,000 份报价
  • 超限行为明确提示,不静默截断

相关实测:50,000 行 CSV 入队后 6.658 秒完成;50,000 行 XLSX(含入队 / 等待 / 下载)27.191 秒。 测量条件与完整数据见 性能与质量。

关于本页数字。 规模、端口、池配置与额度均取自仓库源码与部署文件;实测数据须连同测量条件一并理解,不构成生产服务等级承诺。 未实现的外部连接、SSO、自助开通与计费等项,见 能力边界。