# SaaS + 本地数据库架构方案 > 日期:2026-08-02 > 目标:应用 SaaS 化部署在服务器,数据库留在客户本机,保证用户对数据安全性的要求 ## 一、设计原则 - **可维护性**:一套应用代码,统一部署在服务器,统一升级 - **数据安全性**:业务数据始终在客户本机,客户完全掌控,服务器不存业务数据 - **多租户**:支持多客户接入,每客户独立数据库,互不可见 ## 二、目标架构 ``` 租户A用户 ─┐ 租户B用户 ─┤→ Nginx(服务器) → 后端(服务器:3333) → 识别租户 → 连对应frp端口 → 客户本机PostgreSQL 租户C用户 ─┘ ``` ### 数据流向 ``` 浏览器 → https://dm.all8ai.top → Nginx 反向代理 /api/ → 127.0.0.1:3333 (服务器后端) → 后端解析 tenant_id → 查 tenant_configs 表获取该租户的 frp 端口 → 连接 127.0.0.1:16xxx (frp 隧道) → frp 转发到客户本机 127.0.0.1:5432 (PostgreSQL) → 返回数据 ``` ## 三、组件部署位置 | 组件 | 位置 | 说明 | |------|------|------| | 前端(静态文件) | 服务器 | Nginx 托管,统一部署 | | 后端(Node.js) | 服务器 | 统一维护,一套代码服务所有租户 | | 服务器主库(PostgreSQL) | 服务器 | 仅存租户配置、用户账号、frp端口映射,不存业务数据 | | 业务数据库(PostgreSQL) | 客户本机 | 每个租户自己的数据库,数据完全在客户侧 | | frps(frp服务端) | 服务器 | 接收各租户 frp 客户端连接 | | frpc(frp客户端) | 客户本机 | 每个租户运行自己的 frpc,暴露本机 PostgreSQL | ## 四、多租户路由机制 ### 4.1 服务器主库表结构 ```sql -- 服务器主库:仅存租户配置,不存业务数据 CREATE TABLE tenant_configs ( tenant_id VARCHAR(50) PRIMARY KEY, tenant_name VARCHAR(100) NOT NULL, db_host VARCHAR(100) NOT NULL DEFAULT '127.0.0.1', db_port INTEGER NOT NULL, -- frp 分配的端口,如 16001 db_name VARCHAR(100) NOT NULL DEFAULT 'bill_query', db_user VARCHAR(50) DEFAULT 'postgres', db_password VARCHAR(100) DEFAULT '', frp_port INTEGER NOT NULL, -- frp 远程端口 status VARCHAR(20) DEFAULT 'inactive', -- active/inactive created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE tenant_users ( id SERIAL PRIMARY KEY, tenant_id VARCHAR(50) REFERENCES tenant_configs(tenant_id), username VARCHAR(50) NOT NULL, password VARCHAR(200) NOT NULL, role VARCHAR(20) NOT NULL, -- hq/regional/store/dept name VARCHAR(50), UNIQUE(tenant_id, username) ); ``` ### 4.2 后端中间件:租户识别 + 动态DB连接 ```typescript // tenant.middleware.ts // 从 JWT token 中解析 tenant_id // 根据 tenant_id 查 tenant_configs 获取 DB 连接信息 // 动态创建/复用 pg.Pool 连接到对应租户的数据库 interface TenantContext { tenantId: string dbPool: pg.Pool } // 请求进来 → 解析 token → 获取 tenant_id → 查配置 → 连对应DB // 不同租户的 DB 连接完全隔离,端口不同 ``` ### 4.3 frp 多租户配置 服务器 frps 配置(`/etc/frp/frps.toml`): ```toml bindAddr = "0.0.0.0" bindPort = 7000 auth.token = "frp_sbrain_2026" ``` 租户A本机 frpc 配置: ```toml serverAddr = "dm.all8ai.top" serverPort = 7000 auth.token = "frp_sbrain_2026" [[proxies]] name = "tenant-a-db" type = "tcp" localIP = "127.0.0.1" localPort = 5432 remotePort = 16001 ``` 租户B本机 frpc 配置: ```toml serverAddr = "dm.all8ai.top" serverPort = 7000 auth.token = "frp_sbrain_2026" [[proxies]] name = "tenant-b-db" type = "tcp" localIP = "127.0.0.1" localPort = 5432 remotePort = 16002 ``` 每个租户分配独立的远程端口(16001、16002、16003...)。 ## 五、数据安全保证 1. **数据在客户侧**:业务数据始终在客户本机 PostgreSQL,不出局域网 2. **传输加密**:frp 隧道支持 TLS 加密,数据传输安全 3. **物理隔离**:不同租户的数据库连接端口不同,进程隔离,一个租户的查询不可能泄漏到另一个 4. **客户可控**:客户可随时关闭 frpc 断开连接,服务器立即无法访问该租户数据 5. **服务器零业务数据**:服务器只存应用代码 + 租户路由配置,不存任何业务数据 6. **审计可追溯**:所有 DB 操作通过后端 API,可记录审计日志 ## 六、客户接入流程 ``` 1. 管理员在服务器后台创建租户 → 分配 tenant_id(如 tenant_a) → 分配 frp 远程端口(如 16001) → 生成 frpc 配置文件 2. 发送 frpc 配置给客户 3. 客户本机准备 → 安装 PostgreSQL → 创建数据库 bill_query → 导入初始数据(pg_restore) → 安装 frpc 并启动 4. 服务器后端检测到 frp 连接 → tenant_configs.status 更新为 active → 该租户用户可登录使用 5. 客户用户访问 → 浏览器打开 https://dm.all8ai.top → 登录(绑定 tenant_id) → API 请求自动路由到该租户的本机数据库 ``` ## 七、与当前架构对比 | 维度 | 当前架构 | 目标架构 | |------|---------|---------| | 后端 | 本机 localhost:3333 | 服务器 | | 前端 | 服务器(静态文件) | 服务器(不变) | | 业务DB | 本机 PostgreSQL | 客户本机 PostgreSQL(每租户独立) | | frp 隧道 | 1条(API 3333→13333) | N条(DB 5432→16xxx),每租户1条 | | 多租户 | 不支持 | 支持,按租户路由DB | | 本机依赖 | 必须开着 | 不需要(服务器独立运行) | | 数据安全 | 无隔离 | 客户数据在客户侧,服务器零业务数据 | | 后端部署 | 改完即生效(tsx watch) | deploy-server.sh(rsync + 重启) | | 可维护性 | 仅单租户 | 一套代码服务所有租户,统一升级 | ## 八、实施计划 ### 一期:单租户迁移(当前系统 → 服务器后端 + 本机DB) | 步骤 | 内容 | 说明 | |------|------|------| | 1 | 服务器安装 Node.js 24 + npm | 后端运行环境 | | 2 | 服务器安装 PostgreSQL | 仅作主库,存租户配置 | | 3 | frp 改为暴露 PostgreSQL | 新增 dm-db 代理(5432→15432) | | 4 | 后端部署到服务器 | rsync 代码 + npm install + pm2 管理 | | 5 | server/.env 改 DB_PORT=15432 | 连 frp 暴露的数据库端口 | | 6 | Nginx 配置调整 | /api/ → 127.0.0.1:3333(不再走13333) | | 7 | 验证 | 线上前端 → 服务器后端 → frp隧道 → 本机DB | ### 二期:多租户支持 | 步骤 | 内容 | 说明 | |------|------|------| | 1 | 创建 tenant_configs / tenant_users 表 | 服务器主库 | | 2 | 后端加租户识别中间件 | JWT 解析 tenant_id | | 3 | 后端加动态 DB 连接池 | 按租户连不同端口 | | 4 | 前端登录绑定租户 | 登录页增加租户标识 | | 5 | frps 多租户端口管理 | 每租户独立端口 | | 6 | 租户管理后台 | 创建租户、查看连接状态、分配端口 | ### 三期:商业化运营 | 步骤 | 内容 | 说明 | |------|------|------| | 1 | 客户自助注册 | 自动分配 tenant_id + frp 端口 | | 2 | frpc 一键安装脚本 | 客户本机自动配置 | | 3 | 按租户计费 | 用量统计、账单 | | 4 | 审计日志 | 记录所有 DB 操作 | | 5 | 数据备份方案 | 客户本机自动备份 | ## 九、部署脚本 ### 9.1 后端部署脚本(deploy-server.sh) ```bash #!/bin/bash # 部署后端到服务器 SERVER="ubuntu@dm.all8ai.top" REMOTE_DIR="/opt/sbrain-server" echo "=== 部署后端到服务器 ===" # 1. 同步代码(排除 node_modules) rsync -avz --exclude='node_modules' --exclude='.env' \ server/ $SERVER:$REMOTE_DIR/ # 2. 安装依赖 ssh $SERVER "cd $REMOTE_DIR && npm install --production" # 3. 重启服务(pm2) ssh $SERVER "cd $REMOTE_DIR && pm2 restart sbrain-server || pm2 start 'npm run start' --name sbrain-server" echo "=== 部署完成 ===" ``` ### 9.2 前端部署脚本(deploy.sh,保持不变) ```bash # 前端构建 + scp 上传,与当前流程一致 bash deploy.sh ``` ### 9.3 客户端 frpc 安装脚本(给新租户) ```bash #!/bin/bash # 客户本机一键安装 frpc TENANT_ID=$1 FRP_PORT=$2 FRP_TOKEN="frp_sbrain_2026" cat > /etc/frp/frpc.toml << EOF serverAddr = "dm.all8ai.top" serverPort = 7000 auth.token = "$FRP_TOKEN" [[proxies]] name = "${TENANT_ID}-db" type = "tcp" localIP = "127.0.0.1" localPort = 5432 remotePort = $FRP_PORT EOF # 启动 frpc frpc -c /etc/frp/frpc.toml & ``` ## 十、风险与应对 | 风险 | 影响 | 应对 | |------|------|------| | 客户本机休眠/关机 | 该租户 API 不可用 | 前端显示"数据库离线"提示;建议客户保持开机 | | frp 隧道断开 | 该租户 API 不可用 | frpc 配置 `loginFailExit = false` 自动重连 | | 服务器后端重启 | 所有租户短暂不可用 | pm2 自动重启;健康检查 | | 客户本机 PostgreSQL 崩溃 | 该租户数据不可用 | 客户侧责任,提供备份建议文档 | | 网络延迟 | API 响应慢 | frp TCP 隧道延迟通常 <50ms,可接受 | ## 十一、服务器资源需求 | 资源 | 最低配置 | 推荐配置 | |------|---------|---------| | CPU | 2核 | 4核 | | 内存 | 2GB | 4GB | | 磁盘 | 10GB | 20GB | | 带宽 | 5Mbps | 10Mbps | | Node.js | v24 | v24 | | PostgreSQL | 15(主库) | 15(主库) | 当前服务器 `152.136.182.184` 需确认配置是否满足。