refactor: CMS从Sage子模块重构为独立ahserver Web应用

架构变更:
- CMS作为独立进程运行(端口9090),不再嵌入Sage
- 使用ahserver框架,复用rbac模块做认证授权
- 所有模块共享sage数据库(配置在conf/config.json)

新增文件:
- app/cms.py: 独立Web应用主入口(webapp(init))
- app/global_func.py: 全局函数(get_module_dbname/UiWindow等)
- conf/config.json: 应用配置模板(数据库/路径/处理器/Redis)
- start.sh/stop.sh: 进程管理脚本
- pyproject.toml: 顶层Python包配置

路径重构(去掉/entcms前缀):
- 官网首页: /entcms/index.ui → /index.ui
- 管理后台: /entcms/admin.ui → /admin.ui
- API: /entcms/api/xxx.dspy → /api/xxx.dspy
- CRUD: /entcms/cms_content_list → /cms_content_list
- dingdingflow保持/dingdingflow前缀(映射子目录)

config.json路径映射:
- entcms/wwwroot → / (根路径)
- dingdingflow/wwwroot → /dingdingflow
- bricks/dist → /bricks

构建脚本(build.sh):
- 创建独立venv(py3/)
- 安装核心依赖(apppublic/sqlor/ahserver/bricks/rbac等)
- json2ddl生成CMS业务表DDL
- xls2ui生成CRUD UI
- 生成systemd服务文件

load_path.py更新:
- entcms: 所有路径去掉/entcms前缀
- dingdingflow: 保持/dingdingflow前缀
- 查找set_role_perm.py支持CMS和Sage两种环境

init_superuser.py更新:
- 支持CMS独立环境(自动检测py3/conf)
- 创建superuser角色并分配全部权限
This commit is contained in:
yumoqing 2026-05-27 17:20:36 +08:00
parent 52f4632dfc
commit 569c8e6715
18 changed files with 624 additions and 217 deletions

32
.gitignore vendored
View File

@ -1,3 +1,31 @@
*.pyc # Python
__pycache__/ __pycache__/
mysql.ddl.sql *.py[cod]
*.egg-info/
dist/
build/
# Virtual environment
py3/
pkgs/
# Runtime
logs/
files/
*.pid
cms.service
# DDL generated
*/mysql.ddl.sql
# IDE
.idea/
.vscode/
*.swp
# OS
.DS_Store
Thumbs.db
# Config with credentials
# conf/config.json # Keep template in git

42
app/cms.py Normal file
View File

@ -0,0 +1,42 @@
"""
开元云科技CMS 独立Web应用主入口
启动: py3/bin/python app/cms.py -p 9090 -w $(pwd)
"""
import os, sys
# Ensure app/ is in path for local imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from appPublic.log import MyLogger, info
from appPublic.folderUtils import ProgramPath
from appPublic.jsonConfig import getConfig
from appPublic.registerfunction import RegisterFunction
from bricks_for_python.init import load_pybricks
from ahserver.webapp import webapp
from ahserver.serverenv import ServerEnv
from sqlor.dbpools import DBPools
# CMS业务模块
from entcms.init import load_entcms
from dingdingflow.init import load_dingdingflow
# RBAC认证(复用sage的rbac模块)
from rbac.init import load_rbac
from appbase.init import load_appbase
# 全局函数
from global_func import set_globalvariable
__version__ = '1.0.0'
def init():
rf = RegisterFunction()
set_globalvariable()
load_pybricks()
load_appbase()
load_rbac()
load_entcms()
load_dingdingflow()
if __name__ == '__main__':
webapp(init)

56
app/global_func.py Normal file
View File

@ -0,0 +1,56 @@
"""
CMS全局函数 注册到ServerEnv供.dspy和.ui调用
"""
from ahserver.serverenv import ServerEnv
def get_module_dbname(mname):
"""所有模块共享sage数据库"""
return 'sage'
def UiWindow(title, icon, content, cheight=10, cwidth=15):
return {
"widgettype": "PopupWindow",
"options": {
"author": "cms",
"cwidth": cwidth,
"cheight": cheight,
"title": title,
"content": content,
"icon": icon or entire_url('/bricks/imgs/app.png'),
"movable": True,
"auto_open": True
}
}
def UiError(title="出错", message="出错啦", timeout=5):
return {
"widgettype": "Error",
"options": {
"author": "cms",
"timeout": timeout,
"cwidth": 15,
"cheight": 10,
"title": title,
"message": message
}
}
def UiMessage(title="消息", message="后台消息", timeout=5):
return {
"widgettype": "Message",
"options": {
"author": "cms",
"timeout": timeout,
"cwidth": 15,
"cheight": 10,
"title": title,
"message": message
}
}
def set_globalvariable():
g = ServerEnv()
g.get_module_dbname = get_module_dbname
g.UiError = UiError
g.UiMessage = UiMessage
g.UiWindow = UiWindow

261
build.sh
View File

@ -1,87 +1,198 @@
#!/bin/bash #!/usr/bin/env bash
# CMS项目构建脚本 # 开元云科技CMS — 独立Web应用构建脚本
# 构建 entcms + dingdingflow 模块并集成到Sage系统 # 用法: cd ~/repos/cms && ./build.sh
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cdir=$(pwd)
echo "=== CMS项目构建 ===" uname=$(id -un)
echo "Script dir: $SCRIPT_DIR" gname=$(id -gn)
# 查找Sage根目录 echo "============================================"
SAGE_ROOT="" echo " 开元云科技CMS — 独立Web应用构建"
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do echo "============================================"
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
SAGE_ROOT="$(cd "$candidate" && pwd)" # ===========================================
break # Step 1: Python虚拟环境
# ===========================================
echo ""
echo "--- Step 1: 创建Python虚拟环境 ---"
if [ ! -d "py3" ]; then
python3 -m venv py3
fi
source py3/bin/activate
# ===========================================
# Step 2: 核心依赖
# ===========================================
echo ""
echo "--- Step 2: 安装核心依赖 ---"
mkdir -p pkgs
# 核心框架包
for m in apppublic sqlor ahserver bricks-for-python xls2ddl
do
echo " install $m..."
cd $cdir/pkgs
if [ ! -d "$m" ]; then
git clone https://git.opencomputing.cn/yumoqing/$m
fi fi
cd $m
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
done done
if [ -z "$SAGE_ROOT" ]; then # bricks前端
echo "ERROR: 找不到Sage根目录" echo " install bricks..."
exit 1 cd $cdir/pkgs
if [ ! -d "bricks" ]; then
git clone https://git.opencomputing.cn/yumoqing/bricks
fi
cd bricks/bricks
./build.sh 2>/dev/null || echo " WARN: bricks build skipped"
# bricks符号链接
mkdir -p $cdir/bricks
if [ -d "$cdir/pkgs/bricks/dist" ]; then
rm -f $cdir/bricks
ln -sf $cdir/pkgs/bricks/dist $cdir/bricks
fi fi
echo "Sage root: $SAGE_ROOT" # ===========================================
PY="$SAGE_ROOT/py3/bin/python" # Step 3: RBAC + AppBase模块(认证依赖)
PIP="$SAGE_ROOT/py3/bin/pip" # ===========================================
echo ""
# 安装模块 echo "--- Step 3: 安装RBAC/AppBase模块 ---"
for mod in entcms dingdingflow; do for m in appbase rbac checklang
echo "" do
echo "=== 安装 $mod ===" echo " install $m..."
cd "$SCRIPT_DIR/$mod" cd $cdir/pkgs
$PIP install -e . if [ ! -d "$m" ]; then
git clone https://git.opencomputing.cn/yumoqing/$m
# 生成DDL
if [ -d "models" ] && [ "$(ls models/*.json 2>/dev/null)" ]; then
echo "生成DDL..."
cd models
$PY -c "from sqlor.ddl_template_mysql import DDLTemplate; print(DDLTemplate().generate('.'))" > ../mysql.ddl.sql 2>/dev/null || json2ddl mysql . > ../mysql.ddl.sql 2>/dev/null || echo "DDL generation skipped (json2ddl not available)"
cd ..
fi fi
cd $m
# 生成CRUD UI $cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
if [ -d "json" ] && [ "$(ls json/*.json 2>/dev/null)" ]; then
echo "生成CRUD UI..."
cd json
xls2ui -m ../models -o ../wwwroot $mod *.json 2>/dev/null || echo "CRUD UI generation skipped (xls2ui not available)"
cd ..
fi
# 链接wwwroot到Sage
MODULE_WWWROOT="$SAGE_ROOT/wwwroot/$mod"
mkdir -p "$MODULE_WWWROOT/api"
# 链接UI/CSS/JS文件
for f in "$SCRIPT_DIR/$mod/wwwroot"/*.ui "$SCRIPT_DIR/$mod/wwwroot"/*.css "$SCRIPT_DIR/$mod/wwwroot"/*.js; do
[ -f "$f" ] || continue
fname=$(basename "$f")
ln -sf "$f" "$MODULE_WWWROOT/$fname"
done
# 链接api目录下的.dspy文件
for f in "$SCRIPT_DIR/$mod/wwwroot/api"/*.dspy; do
[ -f "$f" ] || continue
fname=$(basename "$f")
ln -sf "$f" "$MODULE_WWWROOT/api/$fname"
done
# 链接生成的CRUD目录
for d in "$SCRIPT_DIR/$mod/wwwroot"/*/; do
[ -d "$d" ] || continue
dname=$(basename "$d")
case "$dname" in api|styles|scripts) continue ;; esac
ln -sf "$d" "$MODULE_WWWROOT/$dname"
done
echo "$mod 安装完成"
done done
# ===========================================
# Step 4: CMS业务模块
# ===========================================
echo "" echo ""
echo "=== 构建完成 ===" echo "--- Step 4: 安装CMS业务模块 ---"
echo "请执行以下步骤完成集成:"
echo "1. 编辑 $SAGE_ROOT/app/sage.py 添加模块导入" # entcms模块
echo "2. 编辑 $SAGE_ROOT/build.sh 添加模块到安装循环" echo " install entcms..."
echo "3. 执行 RBAC 权限配置" cd $cdir/entcms
echo "4. 重启Sage服务" $cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: entcms install failed"
# dingdingflow模块
echo " install dingdingflow..."
cd $cdir/dingdingflow
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: dingdingflow install failed"
# ===========================================
# Step 5: 数据库DDL(CMS业务表)
# ===========================================
echo ""
echo "--- Step 5: 生成数据库DDL ---"
# entcms表DDL
if [ -d "$cdir/entcms/models" ]; then
cd $cdir/entcms/models
echo " 生成 entcms DDL..."
$cdir/py3/bin/json2ddl mysql . > $cdir/entcms/mysql.ddl.sql 2>/dev/null || echo " WARN: json2ddl failed for entcms"
echo " DDL已生成: entcms/mysql.ddl.sql"
fi
# dingdingflow表DDL
if [ -d "$cdir/dingdingflow/models" ]; then
cd $cdir/dingdingflow/models
echo " 生成 dingdingflow DDL..."
$cdir/py3/bin/json2ddl mysql . > $cdir/dingdingflow/mysql.ddl.sql 2>/dev/null || echo " WARN: json2ddl failed for dingdingflow"
echo " DDL已生成: dingdingflow/mysql.ddl.sql"
fi
# ===========================================
# Step 6: CRUD UI生成
# ===========================================
echo ""
echo "--- Step 6: 生成CRUD UI ---"
# entcms CRUD
if [ -d "$cdir/entcms/json" ]; then
cd $cdir/entcms/json
echo " 生成 entcms CRUD UI..."
for f in *.json; do
[ -f "$f" ] || continue
echo " $f"
$cdir/py3/bin/xls2ui -m ../models -o ../wwwroot entcms $f 2>/dev/null || echo " WARN: xls2ui failed for $f"
done
fi
# dingdingflow CRUD
if [ -d "$cdir/dingdingflow/json" ]; then
cd $cdir/dingdingflow/json
echo " 生成 dingdingflow CRUD UI..."
for f in *.json; do
[ -f "$f" ] || continue
echo " $f"
$cdir/py3/bin/xls2ui -m ../models -o ../wwwroot dingdingflow $f 2>/dev/null || echo " WARN: xls2ui failed for $f"
done
fi
# ===========================================
# Step 7: 日志和文件目录
# ===========================================
echo ""
echo "--- Step 7: 创建运行时目录 ---"
mkdir -p $cdir/logs
mkdir -p $cdir/files
# ===========================================
# Step 8: systemd服务文件
# ===========================================
echo ""
echo "--- Step 8: 生成systemd服务文件 ---"
cat > $cdir/cms.service <<EOF
[Unit]
Description=KaiYuan Cloud CMS Web Application
After=network.target
[Service]
User=$uname
Group=$gname
Type=forking
WorkingDirectory=$cdir
ExecStart=$cdir/start.sh
ExecStop=$cdir/stop.sh
StandardOutput=append:$cdir/logs/cms.log
StandardError=append:$cdir/logs/cms.log
SyslogIdentifier=cms
[Install]
WantedBy=multi-user.target
EOF
echo " cms.service 已生成"
# ===========================================
# Done
# ===========================================
cd $cdir
echo ""
echo "============================================"
echo " 构建完成!"
echo "============================================"
echo ""
echo "后续步骤:"
echo " 1. 编辑 conf/config.json 填入数据库密码"
echo " 2. 执行DDL创建CMS业务表:"
echo " mysql -h HOST -u USER -pPASS sage < entcms/mysql.ddl.sql"
echo " mysql -h HOST -u USER -pPASS sage < dingdingflow/mysql.ddl.sql"
echo " 3. 加载RBAC权限:"
echo " py3/bin/python entcms/scripts/load_path.py"
echo " py3/bin/python dingdingflow/scripts/load_path.py"
echo " 4. 初始化超级用户:"
echo " py3/bin/python scripts/init_superuser.py"
echo " 5. 启动应用:"
echo " ./start.sh"
echo ""
echo "访问地址: http://localhost:9090/"
echo "管理后台: http://localhost:9090/admin.ui"

80
conf/config.json Normal file
View File

@ -0,0 +1,80 @@
{
"password_key": "CHANGE_ME_16_CHARS",
"logger": {
"name": "cms",
"levelname": "info",
"logfile": "$[workdir]$/logs/cms.log"
},
"filesroot": "$[workdir]$/files",
"databases": {
"sage": {
"driver": "aiomysql",
"async_mode": true,
"coding": "utf8",
"dbname": "sage",
"kwargs": {
"user": "test",
"db": "sage",
"password": "ENCRYPTED_PASSWORD_HERE",
"host": "localhost"
}
}
},
"website": {
"paths": [
[
"$[workdir]$/entcms/wwwroot",
""
],
[
"$[workdir]$/dingdingflow/wwwroot",
"/dingdingflow"
],
[
"$[workdir]$/bricks",
"/bricks"
]
],
"host": "0.0.0.0",
"port": 9090,
"coding": "utf-8",
"session_redis": {
"url": "redis://127.0.0.1:6379/0"
},
"indexes": [
"index.ui",
"index.html",
"index.tmpl"
],
"processors": [
[
".xlsxds",
"xlsxds"
],
[
".sqlds",
"sqlds"
],
[
".tmpl",
"tmpl"
],
[
".dspy",
"dspy"
],
[
".ui",
"bui"
],
[
".md",
"md"
]
]
},
"langMapping": {
"zh-Hans-CN": "zh-cn",
"en-US": "en"
}
}

View File

@ -1,24 +1,28 @@
""" """
dingdingflow RBAC权限配置 企业类型: owner dingdingflow RBAC权限配置 企业类型: owner
角色: superuser(继承全部), webmaster(提交审批), reviewer(审批管理), CMS独立部署dingdingflow路径保持/dingdingflow前缀
supervisor(审批配置)
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py 用法: cd ~/repos/cms && py3/bin/python dingdingflow/scripts/load_path.py
""" """
import os, sys, subprocess import os, sys, subprocess
def find_sage_root(): def find_app_root():
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]: script_dir = os.path.dirname(os.path.abspath(__file__))
if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3")): return os.path.dirname(os.path.dirname(script_dir))
return c
return None
sage_root = find_sage_root() app_root = find_app_root()
sage_root = None
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
if os.path.isdir(os.path.join(c, "py3", "bin")):
sage_root = c
break
if not sage_root: if not sage_root:
print("ERROR: Cannot find Sage root"); sys.exit(1) sage_root = app_root
py = os.path.join(sage_root, "py3", "bin", "python") py = os.path.join(app_root, "py3", "bin", "python")
sp = os.path.join(sage_root, "set_role_perm.py") sp = os.path.join(sage_root, "set_role_perm.py") if os.path.exists(os.path.join(sage_root, "set_role_perm.py")) else None
if not sp:
print("ERROR: 找不到set_role_perm.py"); sys.exit(1)
def run(role, paths): def run(role, paths):
for p in paths: for p in paths:
@ -30,7 +34,6 @@ any_paths = [
"/dingdingflow/menu.ui", "/dingdingflow/menu.ui",
] ]
# webmaster: 提交审批
webmaster_paths = [ webmaster_paths = [
"/dingdingflow", "/dingdingflow",
"/dingdingflow/index.ui", "/dingdingflow/index.ui",
@ -39,7 +42,6 @@ webmaster_paths = [
"/dingdingflow/api/dd_approvals_list.dspy", "/dingdingflow/api/dd_approvals_list.dspy",
] ]
# reviewer: 审批管理(查看全部 + 更新审批状态)
reviewer_paths = [ reviewer_paths = [
"/dingdingflow", "/dingdingflow",
"/dingdingflow/index.ui", "/dingdingflow/index.ui",
@ -48,7 +50,6 @@ reviewer_paths = [
"/dingdingflow/api/dd_approvals_update.dspy", "/dingdingflow/api/dd_approvals_update.dspy",
] ]
# supervisor: 审批配置管理 + 全部审批记录
supervisor_paths = [ supervisor_paths = [
"/dingdingflow", "/dingdingflow",
"/dingdingflow/index.ui", "/dingdingflow/index.ui",
@ -66,12 +67,8 @@ supervisor_paths = [
] ]
print("=== dingdingflow RBAC权限配置 ===") print("=== dingdingflow RBAC权限配置 ===")
print(f"\n--- any (匿名/钉钉回调) ---")
run("any", any_paths) run("any", any_paths)
print(f"\n--- owner.webmaster ---")
run("owner.webmaster", webmaster_paths) run("owner.webmaster", webmaster_paths)
print(f"\n--- owner.reviewer ---")
run("owner.reviewer", reviewer_paths) run("owner.reviewer", reviewer_paths)
print(f"\n--- owner.supervisor ---")
run("owner.supervisor", supervisor_paths) run("owner.supervisor", supervisor_paths)
print("\n完成") print("\n完成")

View File

@ -29,9 +29,9 @@
} }
}, },
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/cms_categories_create.dspy')}}", "new_data_url": "{{entire_url('api/cms_categories_create.dspy')}}",
"update_data_url": "{{entire_url('../api/cms_categories_update.dspy')}}", "update_data_url": "{{entire_url('api/cms_categories_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/cms_categories_delete.dspy')}}" "delete_data_url": "{{entire_url('api/cms_categories_delete.dspy')}}"
} }
} }
} }

View File

@ -89,9 +89,9 @@
} }
}, },
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/cms_content_create.dspy')}}", "new_data_url": "{{entire_url('api/cms_content_create.dspy')}}",
"update_data_url": "{{entire_url('../api/cms_content_update.dspy')}}", "update_data_url": "{{entire_url('api/cms_content_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/cms_content_delete.dspy')}}" "delete_data_url": "{{entire_url('api/cms_content_delete.dspy')}}"
} }
} }
} }

View File

@ -85,9 +85,9 @@
} }
}, },
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/cms_leads_create.dspy')}}", "new_data_url": "{{entire_url('api/cms_leads_create.dspy')}}",
"update_data_url": "{{entire_url('../api/cms_leads_update.dspy')}}", "update_data_url": "{{entire_url('api/cms_leads_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/cms_leads_delete.dspy')}}" "delete_data_url": "{{entire_url('api/cms_leads_delete.dspy')}}"
} }
} }
} }

View File

@ -84,9 +84,9 @@
} }
}, },
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/cms_sections_create.dspy')}}", "new_data_url": "{{entire_url('api/cms_sections_create.dspy')}}",
"update_data_url": "{{entire_url('../api/cms_sections_update.dspy')}}", "update_data_url": "{{entire_url('api/cms_sections_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/cms_sections_delete.dspy')}}" "delete_data_url": "{{entire_url('api/cms_sections_delete.dspy')}}"
} }
} }
} }

View File

@ -55,9 +55,9 @@
} }
}, },
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/cms_site_config_create.dspy')}}", "new_data_url": "{{entire_url('api/cms_site_config_create.dspy')}}",
"update_data_url": "{{entire_url('../api/cms_site_config_update.dspy')}}", "update_data_url": "{{entire_url('api/cms_site_config_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/cms_site_config_delete.dspy')}}" "delete_data_url": "{{entire_url('api/cms_site_config_delete.dspy')}}"
} }
} }
} }

View File

@ -1,25 +1,34 @@
""" """
entcms RBAC权限配置 企业类型: owner entcms RBAC权限配置 企业类型: owner
角色: superuser(继承全部), webmaster(内容管理), reviewer(审核), CMS独立部署路径不带/entcms前缀
supervisor(主管), customer-support(客服)
匿名: any (公开页面)
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py 用法: cd ~/repos/cms && py3/bin/python entcms/scripts/load_path.py
""" """
import os, sys, subprocess import os, sys, subprocess
def find_sage_root(): def find_app_root():
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]: """查找CMS应用根目录"""
if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3")): script_dir = os.path.dirname(os.path.abspath(__file__))
return c # scripts/ -> entcms/ -> cms root
return None return os.path.dirname(os.path.dirname(script_dir))
sage_root = find_sage_root() app_root = find_app_root()
# 查找Sage的set_role_perm.pyRBAC工具
sage_root = None
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
if os.path.isdir(os.path.join(c, "py3", "bin")):
sage_root = c
break
if not sage_root: if not sage_root:
print("ERROR: Cannot find Sage root"); sys.exit(1) # 使用CMS自己的py3
sage_root = app_root
py = os.path.join(sage_root, "py3", "bin", "python") py = os.path.join(app_root, "py3", "bin", "python")
sp = os.path.join(sage_root, "set_role_perm.py") sp = os.path.join(sage_root, "set_role_perm.py") if os.path.exists(os.path.join(sage_root, "set_role_perm.py")) else None
if not sp:
print("ERROR: 找不到set_role_perm.py请确保Sage或CMS已构建")
sys.exit(1)
def run(role, paths): def run(role, paths):
for p in paths: for p in paths:
@ -28,105 +37,97 @@ def run(role, paths):
# ─── anonymous (any) — 公开页面 + 公开API ─── # ─── anonymous (any) — 公开页面 + 公开API ───
any_paths = [ any_paths = [
"/entcms/index.ui", "/index.ui",
"/entcms/news.ui", "/news.ui",
"/entcms/news_detail.ui", "/news_detail.ui",
"/entcms/cases.ui", "/cases.ui",
"/entcms/products.ui", "/products.ui",
"/entcms/cms_styles.css", "/cms_styles.css",
"/entcms/cms_scripts.js", "/cms_scripts.js",
"/entcms/menu.ui", "/menu.ui",
"/entcms/api/submit_lead.dspy", "/api/submit_lead.dspy",
"/entcms/api/get_config.dspy", "/api/get_config.dspy",
"/entcms/api/get_published_content.dspy", "/api/get_published_content.dspy",
"/entcms/api/get_content_detail.dspy", "/api/get_content_detail.dspy",
"/entcms/api/get_sections.dspy", "/api/get_sections.dspy",
] ]
# ─── webmaster — 内容/分类/栏目/配置/线索 全部CRUD ─── # ─── webmaster — 内容/分类/栏目/配置/线索 全部CRUD ───
webmaster_paths = [ webmaster_paths = [
"/entcms", "/admin.ui",
"/entcms/admin.ui",
# 内容 # 内容
"/entcms/cms_content_list", "/entcms/cms_content_list/%", "/cms_content_list", "/cms_content_list/%",
"/entcms/api/cms_content_create.dspy", "/api/cms_content_create.dspy",
"/entcms/api/cms_content_update.dspy", "/api/cms_content_update.dspy",
"/entcms/api/cms_content_delete.dspy", "/api/cms_content_delete.dspy",
"/entcms/api/cms_content_list.dspy", "/api/cms_content_list.dspy",
# 分类 # 分类
"/entcms/cms_categories_list", "/entcms/cms_categories_list/%", "/cms_categories_list", "/cms_categories_list/%",
"/entcms/api/cms_categories_create.dspy", "/api/cms_categories_create.dspy",
"/entcms/api/cms_categories_update.dspy", "/api/cms_categories_update.dspy",
"/entcms/api/cms_categories_delete.dspy", "/api/cms_categories_delete.dspy",
"/entcms/api/cms_categories_list.dspy", "/api/cms_categories_list.dspy",
"/entcms/api/category_options.dspy", "/api/category_options.dspy",
# 栏目 # 栏目
"/entcms/cms_sections_list", "/entcms/cms_sections_list/%", "/cms_sections_list", "/cms_sections_list/%",
"/entcms/api/cms_sections_create.dspy", "/api/cms_sections_create.dspy",
"/entcms/api/cms_sections_update.dspy", "/api/cms_sections_update.dspy",
"/entcms/api/cms_sections_delete.dspy", "/api/cms_sections_delete.dspy",
"/entcms/api/cms_sections_list.dspy", "/api/cms_sections_list.dspy",
# 站点配置 # 站点配置
"/entcms/cms_site_config_list", "/entcms/cms_site_config_list/%", "/cms_site_config_list", "/cms_site_config_list/%",
"/entcms/api/cms_site_config_create.dspy", "/api/cms_site_config_create.dspy",
"/entcms/api/cms_site_config_update.dspy", "/api/cms_site_config_update.dspy",
"/entcms/api/cms_site_config_delete.dspy", "/api/cms_site_config_delete.dspy",
"/entcms/api/cms_site_config_list.dspy", "/api/cms_site_config_list.dspy",
# 线索管理 # 线索管理
"/entcms/cms_leads_list", "/entcms/cms_leads_list/%", "/cms_leads_list", "/cms_leads_list/%",
"/entcms/api/cms_leads_create.dspy", "/api/cms_leads_create.dspy",
"/entcms/api/cms_leads_update.dspy", "/api/cms_leads_update.dspy",
"/entcms/api/cms_leads_delete.dspy", "/api/cms_leads_delete.dspy",
"/entcms/api/cms_leads_list.dspy", "/api/cms_leads_list.dspy",
# 审批 # 审批
"/entcms/api/submit_content_approval.dspy", "/api/submit_content_approval.dspy",
] ]
# ─── reviewer — 查看内容 + 审批(只改status) ─── # ─── reviewer — 查看内容 + 审批(只改status) ───
reviewer_paths = [ reviewer_paths = [
"/entcms", "/admin.ui",
"/entcms/admin.ui", "/cms_content_list", "/cms_content_list/%",
"/entcms/cms_content_list", "/entcms/cms_content_list/%", "/api/cms_content_list.dspy",
"/entcms/api/cms_content_list.dspy", "/api/cms_content_update.dspy",
"/entcms/api/cms_content_update.dspy", # 仅更新status字段 "/api/category_options.dspy",
"/entcms/api/category_options.dspy",
] ]
# ─── supervisor — 查看全部 + 审批配置 + 线索管理 ─── # ─── supervisor — 查看全部 + 审批配置 + 线索管理 ───
supervisor_paths = [ supervisor_paths = [
"/entcms", "/admin.ui",
"/entcms/admin.ui", "/cms_content_list", "/cms_content_list/%",
# 只读 "/cms_categories_list", "/cms_categories_list/%",
"/entcms/cms_content_list", "/entcms/cms_content_list/%", "/cms_sections_list", "/cms_sections_list/%",
"/entcms/cms_categories_list", "/entcms/cms_categories_list/%", "/cms_site_config_list", "/cms_site_config_list/%",
"/entcms/cms_sections_list", "/entcms/cms_sections_list/%", "/api/cms_content_list.dspy",
"/entcms/cms_site_config_list", "/entcms/cms_site_config_list/%", "/api/cms_categories_list.dspy",
# 列表API(只读) "/api/cms_sections_list.dspy",
"/entcms/api/cms_content_list.dspy", "/api/cms_site_config_list.dspy",
"/entcms/api/cms_categories_list.dspy", "/api/category_options.dspy",
"/entcms/api/cms_sections_list.dspy", "/cms_leads_list", "/cms_leads_list/%",
"/entcms/api/cms_site_config_list.dspy", "/api/cms_leads_create.dspy",
"/entcms/api/category_options.dspy", "/api/cms_leads_update.dspy",
# 线索全权 "/api/cms_leads_delete.dspy",
"/entcms/cms_leads_list", "/entcms/cms_leads_list/%", "/api/cms_leads_list.dspy",
"/entcms/api/cms_leads_create.dspy", "/api/submit_content_approval.dspy",
"/entcms/api/cms_leads_update.dspy",
"/entcms/api/cms_leads_delete.dspy",
"/entcms/api/cms_leads_list.dspy",
# 审批
"/entcms/api/submit_content_approval.dspy",
] ]
# ─── customer-support — 线索查看和更新 ─── # ─── customer-support — 线索查看和更新 ───
support_paths = [ support_paths = [
"/entcms", "/admin.ui",
"/entcms/admin.ui", "/cms_leads_list", "/cms_leads_list/%",
"/entcms/cms_leads_list", "/entcms/cms_leads_list/%", "/api/cms_leads_list.dspy",
"/entcms/api/cms_leads_list.dspy", "/api/cms_leads_update.dspy",
"/entcms/api/cms_leads_update.dspy",
] ]
print("=== entcms RBAC权限配置 ===") print("=== CMS RBAC权限配置 ===")
print(f"\n--- any (匿名用户) ---") print(f"\n--- any (匿名用户) ---")
run("any", any_paths) run("any", any_paths)
print(f"\n--- owner.webmaster (内容管理员) ---") print(f"\n--- owner.webmaster (内容管理员) ---")

View File

@ -47,7 +47,7 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.sage_main_content", "target": "app.sage_main_content",
"options": { "options": {
"url": "{{entire_url('/entcms/cms_content_list')}}" "url": "{{entire_url('/cms_content_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
@ -102,7 +102,7 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.sage_main_content", "target": "app.sage_main_content",
"options": { "options": {
"url": "{{entire_url('/entcms/cms_sections_list')}}" "url": "{{entire_url('/cms_sections_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
@ -157,7 +157,7 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.sage_main_content", "target": "app.sage_main_content",
"options": { "options": {
"url": "{{entire_url('/entcms/cms_categories_list')}}" "url": "{{entire_url('/cms_categories_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
@ -212,7 +212,7 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.sage_main_content", "target": "app.sage_main_content",
"options": { "options": {
"url": "{{entire_url('/entcms/cms_leads_list')}}" "url": "{{entire_url('/cms_leads_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
@ -267,7 +267,7 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.sage_main_content", "target": "app.sage_main_content",
"options": { "options": {
"url": "{{entire_url('/entcms/cms_site_config_list')}}" "url": "{{entire_url('/cms_site_config_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }

View File

@ -6,37 +6,37 @@
{ {
"name": "cms_content_list", "name": "cms_content_list",
"label": "内容管理", "label": "内容管理",
"url": "{{entire_url('/entcms/cms_content_list')}}", "url": "{{entire_url('/cms_content_list')}}",
"target": "app.sage_main_content" "target": "app.sage_main_content"
}, },
{ {
"name": "cms_sections_list", "name": "cms_sections_list",
"label": "栏目管理", "label": "栏目管理",
"url": "{{entire_url('/entcms/cms_sections_list')}}", "url": "{{entire_url('/cms_sections_list')}}",
"target": "app.sage_main_content" "target": "app.sage_main_content"
}, },
{ {
"name": "cms_categories_list", "name": "cms_categories_list",
"label": "内容分类", "label": "内容分类",
"url": "{{entire_url('/entcms/cms_categories_list')}}", "url": "{{entire_url('/cms_categories_list')}}",
"target": "app.sage_main_content" "target": "app.sage_main_content"
}, },
{ {
"name": "cms_leads_list", "name": "cms_leads_list",
"label": "商机线索", "label": "商机线索",
"url": "{{entire_url('/entcms/cms_leads_list')}}", "url": "{{entire_url('/cms_leads_list')}}",
"target": "app.sage_main_content" "target": "app.sage_main_content"
}, },
{ {
"name": "cms_site_config_list", "name": "cms_site_config_list",
"label": "站点配置", "label": "站点配置",
"url": "{{entire_url('/entcms/cms_site_config_list')}}", "url": "{{entire_url('/cms_site_config_list')}}",
"target": "app.sage_main_content" "target": "app.sage_main_content"
}, },
{ {
"name": "public_site", "name": "public_site",
"label": "官网预览", "label": "官网预览",
"url": "{{entire_url('/entcms/index.ui')}}", "url": "{{entire_url('/index.ui')}}",
"target": "app.sage_main_content" "target": "app.sage_main_content"
} }
] ]

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "kaiyuan-cms"
version = "1.0.0"
description = "开元云科技企业官网CMS系统 — 独立Web应用"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["entcms*", "dingdingflow*"]

View File

@ -1,15 +1,41 @@
""" """
初始化超级用户 初始化超级用户
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/scripts/init_superuser.py [username] [password] 用法:
CMS环境: py3/bin/python scripts/init_superuser.py [username] [password]
Sage环境: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/scripts/init_superuser.py [username] [password]
默认: admin / admin123 默认: admin / admin123
""" """
import os, sys, asyncio import os, sys, asyncio
# 自动检测运行环境
def find_workdir():
"""查找应用根目录"""
script_dir = os.path.dirname(os.path.abspath(__file__))
cms_root = os.path.dirname(script_dir)
# 检查是否是CMS独立环境
if os.path.isdir(os.path.join(cms_root, "py3", "bin")) and \
os.path.isfile(os.path.join(cms_root, "conf", "config.json")):
return cms_root
# 检查Sage环境
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
if os.path.isdir(os.path.join(c, "py3", "bin")):
return c
return None
workdir = find_workdir()
if not workdir:
print("ERROR: 找不到CMS或Sage应用目录")
sys.exit(1)
# 确保Python路径
sys.path.insert(0, os.path.join(workdir, "py3", "lib"))
os.chdir(workdir)
from sqlor.dbpools import DBPools from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig from appPublic.jsonConfig import getConfig
from appPublic.uniqueID import getID from appPublic.uniqueID import getID
# Sage环境导入password_encode
sys.path.insert(0, os.path.expanduser("~/repos/sage"))
from appPublic.password import password_encode from appPublic.password import password_encode
async def main(): async def main():
@ -23,7 +49,6 @@ async def main():
existing = await sor.R('users', {'username': username}) existing = await sor.R('users', {'username': username})
if existing: if existing:
print(f"用户 {username} 已存在 (id={existing[0]['id']})") print(f"用户 {username} 已存在 (id={existing[0]['id']})")
# 更新密码
await sor.U('users', { await sor.U('users', {
'id': existing[0]['id'], 'id': existing[0]['id'],
'passwd': password_encode(password) 'passwd': password_encode(password)
@ -59,8 +84,7 @@ async def main():
users = await sor.R('users', {'username': username}) users = await sor.R('users', {'username': username})
uid = users[0]['id'] uid = users[0]['id']
# 查找或创建用户-角色关联 (userrole表) # 分配角色
# 检查userrole表是否存在
try: try:
ur = await sor.R('userrole', {'userid': uid, 'roleid': role_id}) ur = await sor.R('userrole', {'userid': uid, 'roleid': role_id})
if not ur: if not ur:
@ -76,6 +100,24 @@ async def main():
print(f"注意: userrole表操作异常: {e}") print(f"注意: userrole表操作异常: {e}")
print("可能需要手动分配角色") print("可能需要手动分配角色")
# 给superuser分配全部权限
try:
all_perms = await sor.R('permission', {})
for perm in all_perms:
existing_rp = await sor.R('rolepermission', {
'roleid': role_id,
'permid': perm['id']
})
if not existing_rp:
await sor.C('rolepermission', {
'id': getID(),
'roleid': role_id,
'permid': perm['id']
})
print(f"已将全部 {len(all_perms)} 条权限分配给 owner.superuser")
except Exception as e:
print(f"注意: 权限分配异常: {e}")
print(f"\n登录信息:") print(f"\n登录信息:")
print(f" 用户名: {username}") print(f" 用户名: {username}")
print(f" 密码: {password}") print(f" 密码: {password}")

11
start.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/bash
# CMS独立应用启动脚本
cd "$(dirname "$0")"
WORKDIR="$(pwd)"
PIDFILE="$WORKDIR/cms.pid"
echo "启动 CMS Web Application (port 9090)..."
$WORKDIR/py3/bin/python $WORKDIR/app/cms.py -p 9090 -w $WORKDIR &
echo $! > $PIDFILE
echo "CMS started (PID: $(cat $PIDFILE))"
exit 0

22
stop.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/bash
# CMS独立应用停止脚本
cd "$(dirname "$0")"
PIDFILE="$(pwd)/cms.pid"
if [ -f "$PIDFILE" ]; then
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
echo "停止 CMS (PID: $PID)..."
kill "$PID"
sleep 2
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID"
fi
echo "CMS已停止"
else
echo "CMS进程已不存在"
fi
rm -f "$PIDFILE"
else
echo "未找到PID文件"
fi