feat: tenant 多租户域名管理模块
- tenant_domain 表: domain → resellerid 映射, UNIQUE(domain) - init.py: get_domain_by_host() 根据域名查找分销商 - index.ui: Tabular CRUD 管理界面 - load_path.py: RBAC 权限注册
This commit is contained in:
parent
d20b97fad1
commit
d8eea22b89
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
*.swp
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# CRUD auto-generated directories
|
||||||
|
wwwroot/tenant_domain/
|
||||||
41
README.md
41
README.md
@ -1,2 +1,41 @@
|
|||||||
# tenant
|
# Tenant 模块
|
||||||
|
|
||||||
|
多租户域名管理。每个分销商可申请独立域名,客户在域名下注册即自动绑定为该分销商的客户。
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### tenant_domain(租户域名)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | VARCHAR(32) | 主键 |
|
||||||
|
| domain | VARCHAR(200) | 域名 UNIQUE |
|
||||||
|
| resellerid | VARCHAR(32) | 分销商机构ID |
|
||||||
|
| orgid | VARCHAR(32) | 所属平台机构ID |
|
||||||
|
| status | VARCHAR(10) | active/inactive |
|
||||||
|
| created_at | TIMESTAMP | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP | 更新时间 |
|
||||||
|
|
||||||
|
## 集成点
|
||||||
|
|
||||||
|
1. **注册自动绑定**: 用户注册时根据 `request.host` 查询 `tenant_domain` → 找到 `resellerid` → 写入 `discount_customer_bind`
|
||||||
|
2. **RBAC**: `load_path.py` 注册所有路径
|
||||||
|
3. **Sage 集成**: 需要在 `app/sage.py` import + load, `build.sh` install loop, `global_menu.ui` menu entry
|
||||||
|
|
||||||
|
## DB 迁移
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tenant_domain (
|
||||||
|
id VARCHAR(32) NOT NULL,
|
||||||
|
domain VARCHAR(200) NOT NULL,
|
||||||
|
resellerid VARCHAR(32) NOT NULL,
|
||||||
|
orgid VARCHAR(32) NOT NULL,
|
||||||
|
status VARCHAR(10) NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX idx_tenant_domain_domain (domain),
|
||||||
|
INDEX idx_tenant_domain_reseller (resellerid),
|
||||||
|
INDEX idx_tenant_domain_orgid (orgid)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|||||||
26
json/tenant_domain.json
Normal file
26
json/tenant_domain.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"tblname": "tenant_domain",
|
||||||
|
"title": "租户域名管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": "created_at desc",
|
||||||
|
"logined_userorgid": "orgid",
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id"],
|
||||||
|
"alters": {
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "active", "text": "启用"},
|
||||||
|
{"value": "inactive", "text": "停用"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editexclouded": ["id", "created_at", "updated_at"],
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/tenant_domain_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/tenant_domain_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/tenant_domain_delete.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
models/tenant_domain.json
Normal file
92
models/tenant_domain.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "tenant_domain",
|
||||||
|
"title": "租户域名",
|
||||||
|
"primary": ["id"],
|
||||||
|
"catelog": "entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"title": "主键ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "domain",
|
||||||
|
"title": "域名",
|
||||||
|
"type": "str",
|
||||||
|
"length": 200,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resellerid",
|
||||||
|
"title": "分销商机构ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orgid",
|
||||||
|
"title": "所属平台机构ID",
|
||||||
|
"type": "str",
|
||||||
|
"length": 32,
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"title": "状态",
|
||||||
|
"type": "str",
|
||||||
|
"length": 10,
|
||||||
|
"nullable": "no",
|
||||||
|
"default": "active"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"title": "创建时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"title": "更新时间",
|
||||||
|
"type": "timestamp",
|
||||||
|
"nullable": "no"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "idx_tenant_domain_domain",
|
||||||
|
"idxtype": "unique",
|
||||||
|
"idxfields": ["domain"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_tenant_domain_reseller",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["resellerid"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_tenant_domain_orgid",
|
||||||
|
"idxtype": "index",
|
||||||
|
"idxfields": ["orgid"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"field": "resellerid",
|
||||||
|
"table": "organization",
|
||||||
|
"valuefield": "id",
|
||||||
|
"textfield": "orgname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "status",
|
||||||
|
"table": "appcodes_kv",
|
||||||
|
"valuefield": "k",
|
||||||
|
"textfield": "v",
|
||||||
|
"cond": "parentid='domain_status'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "tenant"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "多租户域名管理模块 — 分销商独立域名,客户域名注册自动归属"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"sqlor",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["tenant*"]
|
||||||
37
scripts/load_path.py
Normal file
37
scripts/load_path.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""RBAC 权限注册 — tenant 模块"""
|
||||||
|
import os, sys, subprocess
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
MOD = "tenant"
|
||||||
|
|
||||||
|
for candidate in [
|
||||||
|
os.path.join(SCRIPT_DIR, "..", ".."),
|
||||||
|
os.path.expanduser("~/repos/sage"),
|
||||||
|
os.path.expanduser("~/sage"),
|
||||||
|
]:
|
||||||
|
if os.path.isdir(os.path.join(candidate, "wwwroot")):
|
||||||
|
SAGE_ROOT = candidate
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("ERROR: Sage root not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
PATHS_LOGINED = [
|
||||||
|
f"/{MOD}",
|
||||||
|
f"/{MOD}/index.ui",
|
||||||
|
f"/{MOD}/tenant_domain",
|
||||||
|
f"/{MOD}/tenant_domain/index.ui",
|
||||||
|
f"/{MOD}/tenant_domain/get_tenant_domain.dspy",
|
||||||
|
f"/{MOD}/tenant_domain/add_tenant_domain.dspy",
|
||||||
|
f"/{MOD}/tenant_domain/update_tenant_domain.dspy",
|
||||||
|
f"/{MOD}/tenant_domain/delete_tenant_domain.dspy",
|
||||||
|
]
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
subprocess.run(cmd, shell=True, cwd=SAGE_ROOT)
|
||||||
|
|
||||||
|
for path in PATHS_LOGINED:
|
||||||
|
run(f'./py3/bin/python set_role_perm.py "logined" "{path}"')
|
||||||
|
|
||||||
|
print(f"tenant: {len(PATHS_LOGINED)} RBAC paths registered")
|
||||||
3
tenant/__init__.py
Normal file
3
tenant/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .init import load_tenant
|
||||||
|
|
||||||
|
__all__ = ['load_tenant']
|
||||||
25
tenant/init.py
Normal file
25
tenant/init.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Tenant module — 多租户域名管理
|
||||||
|
每个分销商可申请独立域名,客户在其域名注册即绑定为该分销商的客户。
|
||||||
|
"""
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from sqlor.dbpools import get_sor_context
|
||||||
|
|
||||||
|
MODULE_NAME = "tenant"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_domain_by_host(request, host):
|
||||||
|
"""根据请求域名查找对应的分销商"""
|
||||||
|
env = request._run_ns
|
||||||
|
domain = host.split(':')[0]
|
||||||
|
async with get_sor_context(env, 'tenant') as sor:
|
||||||
|
sql = """SELECT resellerid, orgid FROM tenant_domain
|
||||||
|
WHERE domain = ${domain}$ AND status = 'active'
|
||||||
|
LIMIT 1"""
|
||||||
|
recs = await sor.sqlExe(sql, {'domain': domain})
|
||||||
|
return recs[0] if recs else None
|
||||||
|
|
||||||
|
|
||||||
|
def load_tenant():
|
||||||
|
env = ServerEnv()
|
||||||
|
env.get_domain_by_host = get_domain_by_host
|
||||||
117
wwwroot/index.ui
Normal file
117
wwwroot/index.ui
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "16px",
|
||||||
|
"gap": "12px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "租户域名管理",
|
||||||
|
"fontSize": "22px",
|
||||||
|
"fontWeight": "700",
|
||||||
|
"color": "#F1F5F9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "管理各分销商的独立域名,客户通过域名注册后自动归属到对应分销商",
|
||||||
|
"fontSize": "13px",
|
||||||
|
"color": "#94A3B8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Tabular",
|
||||||
|
"id": "domain_table",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"css": "card",
|
||||||
|
"title": "域名列表",
|
||||||
|
"data_url": "{{entire_url('/tenant/tenant_domain')}}",
|
||||||
|
"data_method": "GET",
|
||||||
|
"page_rows": 20,
|
||||||
|
"toolbar": {
|
||||||
|
"tools": [
|
||||||
|
{"name": "add", "label": "新增域名"},
|
||||||
|
{"name": "edit", "label": "编辑", "selected_row": true},
|
||||||
|
{"name": "delete", "label": "删除", "selected_row": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"row_options": {
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["id"],
|
||||||
|
"alters": {
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "active", "text": "启用"},
|
||||||
|
{"value": "inactive", "text": "停用"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resellerid": {
|
||||||
|
"uitype": "code",
|
||||||
|
"valueField": "resellerid",
|
||||||
|
"textField": "resellerid_text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cwidths": {
|
||||||
|
"domain": 20,
|
||||||
|
"resellerid": 15,
|
||||||
|
"status": 6,
|
||||||
|
"created_at": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{"name": "domain", "type": "str", "length": 200, "cwidth": 20, "uitype": "str", "label": "域名"},
|
||||||
|
{"name": "resellerid", "type": "str", "length": 32, "cwidth": 15, "uitype": "code", "label": "分销商"},
|
||||||
|
{"name": "status", "type": "str", "length": 10, "cwidth": 6, "uitype": "code", "label": "状态"},
|
||||||
|
{"name": "created_at", "type": "timestamp", "cwidth": 12, "uitype": "timestamp", "label": "创建时间"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "add",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "PopupWindow",
|
||||||
|
"popup_options": {
|
||||||
|
"title": "新增域名",
|
||||||
|
"dismiss_events": ["cancel", "submited"]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/tenant/tenant_domain')}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "edit",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "PopupWindow",
|
||||||
|
"popup_options": {
|
||||||
|
"title": "编辑域名",
|
||||||
|
"dismiss_events": ["cancel", "submited"]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/tenant/tenant_domain')}}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "delete",
|
||||||
|
"actiontype": "method",
|
||||||
|
"target": "self",
|
||||||
|
"method": "delete_rows",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('/tenant/tenant_domain')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user