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