Files
SBrainCO/docs/租户配置与用户管理指南.md
freedakgmail c15ca07b7c docs: 新增租户配置与用户管理指南
涵盖新租户接入全流程(数据库创建、frp隧道配置、租户注册、用户创建)、
角色权限说明、菜单权限对照表、完整API示例
2026-08-02 21:40:40 +08:00

22 KiB
Raw Permalink Blame History

租户配置与用户管理指南

概述

系统采用多租户架构,平台管理员可创建租户(连锁品牌),每个租户有独立的业务数据库。租户管理员(hq)可在租户内创建区域经理、店长等角色。

角色体系

角色 role 值 说明 数据范围
平台管理员 platform_admin SaaS 平台运维,管理所有租户 全平台
总部管理员 hq 租户内最高权限,管理本租户用户 本租户全部数据
商品部 dept 商品/供应链相关功能 本租户全部数据
区域经理 regional 管辖指定门店,看区域汇总 region 字段指定的门店
店长 store 管理指定门店 store_code 字段指定的门店

数据库结构

  • sbrain_admin(平台管理库,服务器端口 5432):存放 tenant_configs(租户配置)、tenant_users(租户用户)和 platform_admins(平台管理员)
  • bill_query(业务数据库,客户本机端口 5432):存放各租户的业务数据,通过 FRP 隧道映射到服务器的 16xxx 端口(现有租户用 15432,新租户从 16001 起)

架构示意

租户A用户 ─┐
租户B用户 ─┤→ Nginx(服务器) → 后端(服务器:9333) → 识别租户 → 连对应frp端口 → 客户本机PostgreSQL
租户C用户 ─┘

浏览器 → https://dm.all8ai.top
       → Nginx 反向代理 /api/ → 127.0.0.1:9333 (服务器后端)
       → 后端解析 JWT 获取 tenant_id
       → 查 tenant_configs 表获取该租户的 frp 端口
       → 连接 127.0.0.1:16xxx (frp 隧道)
       → frp 转发到客户本机 127.0.0.1:5432 (PostgreSQL)
       → 返回数据

组件部署位置

组件 位置 说明
前端(静态文件) 服务器 Nginx 托管,统一部署
后端(Node.js) 服务器 pm2 管理,一套代码服务所有租户
服务器主库(PostgreSQL) 服务器 5432 端口 仅存 tenant_configstenant_usersplatform_admins,不存业务数据
业务数据库(PostgreSQL) 客户本机 5432 端口 每个租户自己的数据库,数据完全在客户侧
frps(frp服务端) 服务器 7000 端口 接收各租户 frp 客户端连接
frpc(frp客户端) 客户本机 每个租户运行自己的 frpc,暴露本机 PostgreSQL

〇、新租户接入全流程

接入一个新租户需要完成以下 4 个阶段:

阶段1:客户本机准备 PostgreSQL 数据库
阶段2:配置 frp 隧道(客户本机 frpc + 服务器 frps 端口分配)
阶段3:服务器后台创建租户配置并激活
阶段4:创建用户账号(hq → regional → store

阶段1:客户本机准备数据库

1.1 安装 PostgreSQL

# macOS (Homebrew)
brew install postgresql@15
brew services start postgresql@15

# Ubuntu/Debian
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql

1.2 创建数据库和用户

# 进入 PostgreSQL
psql postgres

# 创建数据库用户(如不需要密码,使用 trust 认证)
CREATE USER freedak SUPERUSER;

# 创建业务数据库
CREATE DATABASE bill_query OWNER freedak;

# 退出
\q

1.3 导入业务数据

# 方式1:从备份文件恢复
pg_restore -U freedak -d bill_query -1 /path/to/backup.dump

# 方式2:从 SQL 文件导入
psql -U freedak -d bill_query -f /path/to/init_data.sql

# 验证数据
psql -U freedak -d bill_query -c "SELECT count(*) FROM analytics.dim_store;"

1.4 配置 PostgreSQL 允许本地连接

确保 postgresql.conf 中:

listen_addresses = 'localhost'
port = 5432

确保 pg_hba.conf 中允许本地 trust 连接(如使用密码则改为 md5/scram-sha-256):

# TYPE  DATABASE     USER      ADDRESS          METHOD
local   all          all                        trust
host    all          all       127.0.0.1/32     trust

修改后重启 PostgreSQL

# macOS
brew services restart postgresql@15

# Ubuntu
sudo systemctl restart postgresql

阶段2:配置 frp 隧道

每个租户需要一条独立的 frp 隧道,将客户本机的 PostgreSQL 5432 端口映射到服务器的一个远程端口。

2.1 服务器端 frps 配置

服务器上已运行 frps,配置文件位于 /etc/frp/frps.toml

bindAddr = "0.0.0.0"
bindPort = 7000
auth.token = "frp_gitea_2026"

无需修改,frps 会自动接受来自 frpc 的连接。只需为每个租户分配一个不冲突的远程端口。

2.2 分配远程端口

端口分配规则:

租户 tenant_id 远程端口 用途
西部马华 tenant_a 15432 已占用
新租户B tenant_b 16001 新分配
新租户C tenant_c 16002 新分配
... ... ... 递增

注意:端口 15432 已被现有租户使用。新租户从 16001 开始分配。

2.3 客户本机安装 frpc

# macOS
brew install frp

# 或手动下载
# https://github.com/fatedier/frp/releases
# 下载对应平台的 frp,解压后取 frpc 可执行文件

2.4 客户本机配置 frpc

创建配置文件 /etc/frp/frpc.toml(或 ~/.frp/frpc.toml):

serverAddr = "dm.all8ai.top"
serverPort = 7000
auth.token = "frp_gitea_2026"
loginFailExit = false

[[proxies]]
name = "tenant_b-db"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5432
remotePort = 16001
  • name:必须唯一,建议用 租户ID-db 格式
  • remotePort:使用分配的远程端口(如 16001
  • loginFailExit = false:断线自动重连

2.5 启动 frpc

# 前台运行(测试用)
frpc -c /etc/frp/frpc.toml

# 后台运行(生产用)
nohup frpc -c /etc/frp/frpc.toml > /var/log/frpc.log 2>&1 &

# macOS 可用 brew services
brew services start frp

2.6 验证隧道连通

在服务器上测试远程端口是否可达:

# 在服务器上执行
ssh ubuntu@152.136.182.184 "ss -tlnp | grep 16001"
# 应看到 LISTEN 状态

# 测试数据库连接
ssh ubuntu@152.136.182.184 "psql -h 127.0.0.1 -p 16001 -U freedak -d bill_query -c 'SELECT 1;'"
# 应返回 1

阶段3:服务器后台创建租户配置

隧道连通后,在系统中注册租户。

3.1 平台管理员登录

  • 入口https://dm.all8ai.top/login
  • 用户名平台管理员
  • 密码:初始密码见部署配置
  • 登录后:自动跳转到「租户管理」页面

3.2 创建租户

在「租户管理」页面点击「新建租户」,填写以下信息:

字段 说明 示例
tenant_id 租户唯一标识(英文,不可改) tenant_b
tenant_name 租户显示名称 B品牌连锁
db_host 业务数据库地址(固定) 127.0.0.1
db_port frp 远程端口 16001
db_name 业务数据库名(固定) bill_query
db_user 数据库用户 freedak
db_password 数据库密码(空则不传) (留空)
frp_port FRP 隧道端口(同 db_port 16001

关键db_portfrp_port 必须填 frp 分配的远程端口(如 16001),不是客户本机的 5432。

3.3 激活租户

创建后默认状态为 inactive,需手动改为 active

API 方式(阶段3完整示例)

# 平台管理员登录获取 token
ADMIN_TOKEN=$(curl -s -X POST http://localhost:9333/api/admin/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"平台管理员","password":"xxx"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")

# 创建租户
curl -s -X POST http://localhost:9333/api/admin/tenants \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "tenant_id": "tenant_b",
    "tenant_name": "B品牌连锁",
    "db_host": "127.0.0.1",
    "db_port": 16001,
    "db_name": "bill_query",
    "db_user": "freedak",
    "db_password": "",
    "frp_port": 16001
  }'

# 激活租户
curl -s -X PUT http://localhost:9333/api/admin/tenants/tenant_b \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"status": "active"}'

# 验证数据库连接
curl -s http://localhost:9333/api/admin/tenants/tenant_b/status \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# 应返回 {"success":true,"data":{"connected":true}}

阶段4:创建用户账号

租户激活后,创建用户账号。详见下方「一、平台管理员操作」和「二、租户管理员操作」。


一、平台管理员操作

1.1 登录

  • 入口https://dm.all8ai.top/login
  • 用户名平台管理员
  • 密码:初始密码见部署配置
  • 登录后:自动跳转到「租户管理」页面

1.2 创建新租户

前置条件:客户本机已完成数据库准备和 frp 隧道配置(见「〇、新租户接入全流程」阶段1和阶段2),服务器上对应端口已可连通。

在「租户管理」页面点击「新建租户」,填写以下信息:

字段 说明 示例
tenant_id 租户唯一标识(英文,不可改) tenant_b
tenant_name 租户显示名称 B品牌连锁
db_host 业务数据库地址(固定) 127.0.0.1
db_port frp 远程端口(见阶段2分配) 16001
db_name 业务数据库名(固定) bill_query
db_user 数据库用户 freedak
db_password 数据库密码(空则不传) (留空)
frp_port FRP 隧道端口(同 db_port 16001

关键db_portfrp_port 必须填 frp 分配的远程端口(如 16001),不是客户本机的 5432。

创建后默认状态为 inactive(未激活),需手动激活为 active

API 方式

# 平台管理员登录获取 token
ADMIN_TOKEN=$(curl -s -X POST http://localhost:9333/api/admin/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"平台管理员","password":"xxx"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")

# 创建租户
curl -s -X POST http://localhost:9333/api/admin/tenants \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "tenant_id": "tenant_b",
    "tenant_name": "B品牌连锁",
    "db_host": "127.0.0.1",
    "db_port": 16001,
    "db_name": "bill_query",
    "db_user": "freedak",
    "db_password": "",
    "frp_port": 16001
  }'

# 激活租户
curl -s -X PUT http://localhost:9333/api/admin/tenants/tenant_b \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"status": "active"}'

# 验证数据库连接
curl -s http://localhost:9333/api/admin/tenants/tenant_b/status \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# 应返回 {"success":true,"data":{"connected":true}}

1.3 为租户创建总部管理员(hq

平台管理员为租户创建第一个 hq 用户,后续用户由 hq 自行管理:

界面操作

在「租户管理」页面 → 点击租户 → 「用户」标签 → 「添加用户」

API 方式

curl -s -X POST http://localhost:9333/api/admin/tenants/tenant_b/users \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "brand_b_admin",
    "password": "123",
    "role": "hq",
    "name": "B品牌管理员"
  }'

注意:平台管理员创建用户时不包含 store_code/region/dept 字段。如需设置,由租户管理员(hq)登录后通过用户管理页面编辑。


二、租户管理员(hq)操作

2.1 登录

  • 入口https://dm.all8ai.top/login
  • 用户名:由平台管理员创建的 hq 账号
  • 登录后:自动跳转到「总部驾驶舱」

2.2 用户管理

进入「系统管理」→「用户管理」页面,可执行以下操作:

  • 添加用户:创建区域经理、店长、商品部账号
  • 编辑关联:修改用户关联的门店/区域
  • 修改密码:重置用户密码
  • 删除用户:移除用户账号

2.3 创建区域经理(regional

  1. 点击「添加用户」
  2. 填写信息:
    • 用户名:如 haidian_manager
    • 密码:如 123
    • 角色:选择「区域经理」
    • 姓名:如 海淀区区域经理
  3. 管辖门店:在门店列表中勾选该区域经理管辖的门店(多选)
    • region 字段存储的是逗号分隔的门店编码,如 0016,0018,0020,...
    • 可通过搜索框按门店名称或区域筛选
  4. 点击「添加」

API 方式

# hq 登录获取 token
HQ_TOKEN=$(curl -s -X POST http://localhost:9333/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"brand_b_admin","password":"123"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")

# 创建区域经理(region 字段填逗号分隔的门店编码)
curl -s -X POST http://localhost:9333/api/admin/tenant/users \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "haidian_manager",
    "password": "123",
    "role": "regional",
    "name": "海淀区区域经理",
    "region": "0016,0018,0020,0022,0024,0025,0028,0029,0032,0049,0054,0059,..."
  }'

2.4 创建店长(store

  1. 点击「添加用户」
  2. 填写信息:
    • 用户名:如 wanquanhe_manager
    • 密码:如 123
    • 角色:选择「店长」
    • 姓名:如 万泉河店长
  3. 所属门店:在门店列表中勾选该店长管理的门店(可多选)
    • store_code 字段存储逗号分隔的门店编码,如 0016
  4. 点击「添加」

API 方式

curl -s -X POST http://localhost:9333/api/admin/tenant/users \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "wanquanhe_manager",
    "password": "123",
    "role": "store",
    "name": "万泉河店长",
    "store_code": "0016"
  }'

2.5 创建商品部(dept

  1. 点击「添加用户」
  2. 填写信息:
    • 用户名:如 product_dept
    • 密码:如 123
    • 角色:选择「商品部」
    • 姓名:如 商品部
  3. 部门:填写部门名称,如 商品部
  4. 点击「添加」

API 方式

curl -s -X POST http://localhost:9333/api/admin/tenant/users \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "product_dept",
    "password": "123",
    "role": "dept",
    "name": "商品部",
    "dept": "商品部"
  }'

2.6 编辑用户关联门店

创建用户后如需修改关联门店:

  1. 在用户列表中点击「编辑」图标
  2. 重新勾选/取消勾选门店
  3. 点击「保存」

API 方式

# 修改用户关联的门店编码
curl -s -X PUT http://localhost:9333/api/admin/tenant/users/5 \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "store_code": "0016,0018",
    "region": "",
    "dept": ""
  }'

2.7 修改用户密码

  1. 在用户列表中点击「钥匙」图标
  2. 输入新密码
  3. 点击「确认修改」

API 方式

curl -s -X PUT http://localhost:9333/api/admin/tenant/users/5/password \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"password": "newpass"}'

三、数据范围机制

3.1 字段说明

角色 字段 存储内容 示例
hq / dept (无) 不做数据过滤 -
regional region 逗号分隔的门店编码 0016,0018,0020,...
store store_code 逗号分隔的门店编码 0016

重要region 字段存储的是门店编码(store_code,不是区域名称。系统通过 store_code = ANY(...) 进行 SQL 过滤。

3.2 过滤逻辑

  • hq / dept:无过滤,可查看本租户全部数据
  • regional:所有 API 查询自动注入 store_code = ANY(门店编码列表)
  • store:所有 API 查询自动注入 store_code = ANY(门店编码列表)

3.3 菜单权限

菜单 hq dept regional store
老板驾驶舱 - -
总部驾驶舱 - -
态势感知
银行授信 - - -
区域经理 -
区域对比 -
店长工作台 -
任务管理
月度验收 -
营收分析 - -
平台优惠 - -
会员复购 - -
会员LTV - -
菜品成本 - -
成本库存 - -
库存周转 - -
中央厨房 - -
配送对账 - -
BOM穿透 - -
生产要货 - -
门店费用 - -
商品SKU - -
菜单工程 - -
时间分析 - -
智能排班
员工绩效 - -
风险内控 - -
门店选址 - -
用户管理 - - -
数据质量 - -
指标字典
本体标准 - -

前端路由守卫(RoleRoute)确保用户无法通过直接输入 URL 访问无权限的页面。


四、完整操作示例

场景:为新连锁品牌「B品牌」配置全套账号

步骤 0:客户本机准备数据库 + frp 隧道

# === 在客户本机执行 ===

# 1. 创建数据库
psql postgres -c "CREATE USER freedak SUPERUSER;"
psql postgres -c "CREATE DATABASE bill_query OWNER freedak;"

# 2. 导入业务数据
pg_restore -U freedak -d bill_query -1 /path/to/backup.dump

# 3. 配置 frpc
cat > /etc/frp/frpc.toml << 'EOF'
serverAddr = "dm.all8ai.top"
serverPort = 7000
auth.token = "frp_gitea_2026"
loginFailExit = false

[[proxies]]
name = "tenant_b-db"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5432
remotePort = 16001
EOF

# 4. 启动 frpc
nohup frpc -c /etc/frp/frpc.toml > /var/log/frpc.log 2>&1 &

# === 在服务器上验证 ===
ssh ubuntu@152.136.182.184 "ss -tlnp | grep 16001"
# 应看到 LISTEN 状态

步骤 1:平台管理员创建租户

ADMIN_TOKEN=...  # 平台管理员 token

# 创建租户(db_port 和 frp_port 使用 frp 远程端口 16001
curl -X POST http://localhost:9333/api/admin/tenants \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "tenant_id": "tenant_b",
    "tenant_name": "B品牌连锁",
    "db_host": "127.0.0.1",
    "db_port": 16001,
    "db_name": "bill_query",
    "db_user": "freedak",
    "db_password": "",
    "frp_port": 16001
  }'

# 激活租户
curl -X PUT http://localhost:9333/api/admin/tenants/tenant_b \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"status": "active"}'

# 验证数据库连接
curl -s http://localhost:9333/api/admin/tenants/tenant_b/status \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# 应返回 {"success":true,"data":{"connected":true}}

# 创建总部管理员
curl -X POST http://localhost:9333/api/admin/tenants/tenant_b/users \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "brand_b_admin",
    "password": "123",
    "role": "hq",
    "name": "B品牌管理员"
  }'

步骤 2:总部管理员创建区域经理和店长

HQ_TOKEN=...  # brand_b_admin 的 token

# 查询可用门店列表
curl http://localhost:9333/api/admin/tenant/stores \
  -H "Authorization: Bearer $HQ_TOKEN"

# 创建朝阳区区域经理(管辖门店 0001~0010)
curl -X POST http://localhost:9333/api/admin/tenant/users \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "chaoyang_manager",
    "password": "123",
    "role": "regional",
    "name": "朝阳区区域经理",
    "region": "0001,0002,0003,0004,0005,0006,0007,0008,0009,0010"
  }'

# 创建店长(管理 0001 号门店)
curl -X POST http://localhost:9333/api/admin/tenant/users \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "store_0001",
    "password": "123",
    "role": "store",
    "name": "001号店长",
    "store_code": "0001"
  }'

# 创建商品部
curl -X POST http://localhost:9333/api/admin/tenant/users \
  -H "Authorization: Bearer $HQ_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "product_dept",
    "password": "123",
    "role": "dept",
    "name": "商品部",
    "dept": "商品部"
  }'

步骤 3:验证登录

用户名 密码 角色 预期首页 数据范围
brand_b_admin 123 hq 总部驾驶舱 全部门店
chaoyang_manager 123 regional 区域经理工作台 10 家朝阳区门店
store_0001 123 store 店长工作台 仅 0001 号门店
product_dept 123 dept 总部驾驶舱 全部门店

五、注意事项

  1. 租户状态:创建后默认 inactive,必须激活为 active 后用户才能登录
  2. 门店编码regionstore_code 字段存储的是 dim_store 表中的 store_code 值,不是门店名称
  3. 多选门店:店长和区域经理都可以关联多个门店,用逗号分隔
  4. 用户名唯一:同一租户内用户名不能重复
  5. 密码安全:密码使用 bcrypt 加密存储,无法逆向查看
  6. 路由守卫:前端通过 RoleRoute 组件阻止越权访问,后端通过 data-scope 中间件过滤数据
  7. 删除用户:删除用户不会影响已产生的业务数据
  8. frp 隧道:客户本机必须保持 frpc 运行,否则该租户所有 API 不可用。建议配置 loginFailExit = false 自动重连
  9. frp 端口:每个租户必须使用不同的远程端口,端口 15432 已被现有租户占用,新租户从 16001 开始递增
  10. 数据库密码:如客户本机 PostgreSQL 使用密码认证(非 trust),需在创建租户时填写 db_password
  11. 数据库离线:客户本机关机/休眠会导致该租户 API 不可用,前端会显示错误提示
  12. 数据导入:新租户的业务数据库必须导入完整的表结构和数据(analytics schema、dim_store 等),否则 API 会报错