feat: 角色权限体系 + 栏目管理

1. 角色体系(owner企业类型):
   - superuser: 超级用户(继承全部权限)
   - webmaster: 内容管理员(CRUD全部内容/分类/栏目/配置/线索)
   - reviewer: 内容审核(查看内容+审批状态更新)
   - supervisor: 主管(只读全部+线索管理+审批)
   - customer-support: 客服(线索查看和更新)
   - anonymous: 匿名用户(公开页面+提交线索)

2. 超级用户初始化脚本(scripts/init_superuser.py)
   - 默认: admin/admin123
   - 自动创建用户+分配owner.superuser角色

3. cms_sections栏目管理表:
   - section_key: 栏目标识(hero/products/cases/news/cta/footer/float)
   - display_config: 展示配置JSON(布局/列数/悬停效果)
   - style_config: 样式配置JSON(颜色/渐变/边框)
   - static_content: 静态内容(Hero标语/产品卡片/CTA文案)
   - is_visible: 显示/隐藏控制
   - sort_order: 栏目排序

4. cms_categories增加display_config字段(分类展示风格)

5. 初始化6个栏目数据(Hero/产品/案例/新闻/页脚/浮动入口)

6. 更新菜单和管理后台增加栏目管理入口
This commit is contained in:
yumoqing 2026-05-27 16:57:00 +08:00
parent 5cfb0e867b
commit 52f4632dfc
15 changed files with 776 additions and 76 deletions

View File

@ -1,42 +1,59 @@
"""
dingdingflow RBAC权限配置
dingdingflow RBAC权限配置 企业类型: owner
角色: superuser(继承全部), webmaster(提交审批), reviewer(审批管理),
supervisor(审批配置)
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py
"""
import os
import sys
import subprocess
import os, sys, subprocess
def find_sage_root():
for candidate in [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
]:
if os.path.isdir(os.path.join(candidate, "wwwroot")) and \
os.path.isdir(os.path.join(candidate, "py3")):
return candidate
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3")):
return c
return None
sage_root = find_sage_root()
if not sage_root:
print("ERROR: Cannot find Sage root directory")
sys.exit(1)
print("ERROR: Cannot find Sage root"); sys.exit(1)
python = os.path.join(sage_root, "py3", "bin", "python")
set_perm = os.path.join(sage_root, "set_role_perm.py")
py = os.path.join(sage_root, "py3", "bin", "python")
sp = os.path.join(sage_root, "set_role_perm.py")
paths_any = [
# 钉钉回调是公开endpoint
def run(role, paths):
for p in paths:
print(f" {role:30s} {p}")
subprocess.run([py, sp, role, p], cwd=sage_root, capture_output=True)
any_paths = [
"/dingdingflow/api/dingtalk_callback.dspy",
"/dingdingflow/menu.ui",
]
paths_logined = [
# webmaster: 提交审批
webmaster_paths = [
"/dingdingflow",
"/dingdingflow/index.ui",
"/dingdingflow/dd_approvals",
"/dingdingflow/dd_approvals/%",
"/dingdingflow/dd_approval_configs",
"/dingdingflow/dd_approval_configs/%",
"/dingdingflow/api/submit_approval.dspy",
"/dingdingflow/dd_approvals", "/dingdingflow/dd_approvals/%",
"/dingdingflow/api/dd_approvals_list.dspy",
]
# reviewer: 审批管理(查看全部 + 更新审批状态)
reviewer_paths = [
"/dingdingflow",
"/dingdingflow/index.ui",
"/dingdingflow/dd_approvals", "/dingdingflow/dd_approvals/%",
"/dingdingflow/api/dd_approvals_list.dspy",
"/dingdingflow/api/dd_approvals_update.dspy",
]
# supervisor: 审批配置管理 + 全部审批记录
supervisor_paths = [
"/dingdingflow",
"/dingdingflow/index.ui",
"/dingdingflow/dd_approvals", "/dingdingflow/dd_approvals/%",
"/dingdingflow/dd_approval_configs", "/dingdingflow/dd_approval_configs/%",
"/dingdingflow/api/dd_approvals_create.dspy",
"/dingdingflow/api/dd_approvals_update.dspy",
"/dingdingflow/api/dd_approvals_delete.dspy",
@ -48,13 +65,13 @@ paths_logined = [
"/dingdingflow/api/submit_approval.dspy",
]
def set_perms(role, paths):
for path in paths:
cmd = [python, set_perm, role, path]
print(f" {role:20s} {path}")
subprocess.run(cmd, cwd=sage_root, capture_output=True)
print("=== dingdingflow RBAC权限配置 ===")
set_perms("any", paths_any)
set_perms("logined", paths_logined)
print("完成")
print(f"\n--- any (匿名/钉钉回调) ---")
run("any", any_paths)
print(f"\n--- owner.webmaster ---")
run("owner.webmaster", webmaster_paths)
print(f"\n--- owner.reviewer ---")
run("owner.reviewer", reviewer_paths)
print(f"\n--- owner.supervisor ---")
run("owner.supervisor", supervisor_paths)
print("\n完成")

View File

@ -129,6 +129,44 @@ async def submit_lead(data):
return {'status': 'ok', 'id': data['id']}
# ===== CMS Sections CRUD =====
async def cms_sections_list(ns=None):
sor = _get_db()
ns = ns or {}
ns.setdefault('sort', 'sort_order asc')
rows = await sor.R('cms_sections', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_sections_create(data):
sor = _get_db()
data['id'] = getID()
await sor.C('cms_sections', data)
return data
async def cms_sections_update(data):
sor = _get_db()
await sor.U('cms_sections', data)
return data
async def cms_sections_delete(data):
sor = _get_db()
await sor.D('cms_sections', data)
return data
async def get_visible_sections():
"""获取所有可见栏目(公开接口)"""
sor = _get_db()
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
rows = await sor.R('cms_sections', ns)
import json as _json
for r in rows:
for field in ['display_config', 'style_config', 'static_content']:
v = r.get(field, None)
if v and isinstance(v, str):
try: r[field] = _json.loads(v)
except: pass
return rows
# ===== CMS Site Config CRUD =====
async def cms_site_config_list(ns=None):
sor = _get_db()
@ -251,6 +289,13 @@ def load_entcms():
env.cms_site_config_delete = cms_site_config_delete
env.get_site_config = get_site_config
# Sections CRUD
env.cms_sections_list = cms_sections_list
env.cms_sections_create = cms_sections_create
env.cms_sections_update = cms_sections_update
env.cms_sections_delete = cms_sections_delete
env.get_visible_sections = get_visible_sections
# Public APIs
env.get_published_content = get_published_content
env.get_latest_news = get_latest_news

View File

@ -112,5 +112,87 @@
"config_value": "开元云科技",
"config_type": "text"
}
],
"cms_sections": [
{
"id": "sec_hero",
"org_id": "0",
"section_key": "hero",
"title": "首屏Hero",
"section_type": "hero",
"content_type": "",
"sort_order": 1,
"is_visible": "1",
"display_config": "{\"layout\": \"fullscreen\", \"height\": \"100vh\", \"bg_glow\": true}",
"style_config": "{\"gradient\": \"#6C5CE7,#A29BFE,#74B9FF\", \"title_size\": \"56px\"}",
"static_content": "{\"tag_text\": \"AI \\u667a\\u80fd\\u4f53\\u670d\\u52a1\\u5e73\\u53f0\", \"slogan\": \"\\u4e00\\u4e2a\\u5e73\\u53f0\\uff0c\\u5343\\u884c\\u767e\\u4e1a \\u667a\\u80fd\\u8dc3\\u8fc1\", \"subtitle\": \"\\u57fa\\u4e8e\\u4e1c\\u6570\\u897f\\u7b97\\u56fd\\u5bb6\\u6218\\u7565\\uff0c\\u6253\\u9020\\u65b0\\u4e00\\u4ee3AI\\u667a\\u80fd\\u4f53\\u670d\\u52a1\\u5e73\\u53f0\", \"btn_primary\": \"\\u8054\\u7cfb\\u9500\\u552e\", \"btn_secondary\": \"\\u4e86\\u89e3\\u4ea7\\u54c1\\u67b6\\u6784\"}"
},
{
"id": "sec_products",
"org_id": "0",
"section_key": "products",
"title": "1+N+X 产品架构",
"subtitle": "一个AI平台 + N个行业模型 + X个智能体",
"section_type": "cards",
"content_type": "product",
"sort_order": 2,
"is_visible": "1",
"display_config": "{\"columns\": 3, \"expandable\": true, \"icon_position\": \"top\"}",
"style_config": "{\"card_bg\": \"#1A1A1A\", \"card_border\": \"#222\", \"hover_border\": \"#6C5CE7\"}",
"static_content": "{\"cards\": [{\"icon\": \"\\ud83e\\udde0\", \"title\": \"1 \\u4e2a AI \\u5e73\\u53f0\", \"desc\": \"\\u7edf\\u4e00AI\\u57fa\\u7840\\u8bbe\\u65bd\\u5e73\\u53f0\\uff0c\\u63d0\\u4f9b\\u7b97\\u529b\\u8c03\\u5ea6\\u3001\\u6a21\\u578b\\u7ba1\\u7406\\u3001\\u667a\\u80fd\\u4f53\\u7f16\\u6392\\u7b49\\u6838\\u5fc3\\u80fd\\u529b\", \"detail\": \"\\u57fa\\u4e8e\\u4e1c\\u6570\\u897f\\u7b97\\u56fd\\u5bb6\\u6218\\u7565\\u90e8\\u7f72\\uff0c\\u63d0\\u4f9b\\u9ad8\\u6027\\u80fd\\u3001\\u4f4e\\u6210\\u672c\\u7684AI\\u7b97\\u529b\\u670d\\u52a1\\u3002\"}, {\"icon\": \"\\ud83c\\udfed\", \"title\": \"N \\u4e2a\\u884c\\u4e1a\\u6a21\\u578b\", \"desc\": \"\\u9488\\u5bf9\\u5236\\u9020\\u3001\\u91d1\\u878d\\u3001\\u533b\\u7597\\u3001\\u6559\\u80b2\\u7b49\\u884c\\u4e1a\\u6df1\\u5ea6\\u5b9a\\u5236\\u7684\\u4e13\\u4e1aAI\\u6a21\\u578b\", \"detail\": \"\\u6bcf\\u4e2a\\u884c\\u4e1a\\u6a21\\u578b\\u90fd\\u7ecf\\u8fc7\\u5927\\u91cf\\u884c\\u4e1a\\u6570\\u636e\\u8bad\\u7ec3\\u548c\\u5fae\\u8c03\\uff0c\\u7406\\u89e3\\u884c\\u4e1a\\u672f\\u8bed\\u548c\\u4e1a\\u52a1\\u6d41\\u7a0b\\u3002\"}, {\"icon\": \"\\ud83e\\udd16\", \"title\": \"X \\u4e2a\\u667a\\u80fd\\u4f53\", \"desc\": \"\\u7075\\u6d3b\\u7ec4\\u5408\\u7684\\u667a\\u80fd\\u4f53\\u5e94\\u7528\\uff0c\\u8986\\u76d6\\u5ba2\\u670d\\u3001\\u5199\\u4f5c\\u3001\\u5206\\u6790\\u3001\\u7f16\\u7a0b\\u7b49\\u591a\\u79cd\\u573a\\u666f\", \"detail\": \"\\u667a\\u80fd\\u4f53\\u652f\\u6301\\u591a\\u6a21\\u6001\\u4ea4\\u4e92\\u3001\\u5de5\\u5177\\u8c03\\u7528\\u3001\\u591aAgent\\u534f\\u4f5c\\u3002\"}]}"
},
{
"id": "sec_cases",
"org_id": "0",
"section_key": "cases",
"title": "成功案例",
"subtitle": "看看AI如何改变这些行业",
"section_type": "grid",
"content_type": "case",
"sort_order": 3,
"is_visible": "1",
"display_config": "{\"columns\": 3, \"hover_effect\": \"lift\", \"show_cta\": true}",
"style_config": "{\"card_bg\": \"#1A1A1A\", \"hover_border\": \"#6C5CE7\"}",
"static_content": "{\"cta_text\": \"\\u60f3\\u4e86\\u89e3\\u8fd9\\u4e9b\\u65b9\\u6848\\u5982\\u4f55\\u843d\\u5730\\uff1f\", \"cta_btn\": \"\\u4e86\\u89e3\\u66f4\\u591a \\u2192 \\u8054\\u7cfb\\u9500\\u552e\"}"
},
{
"id": "sec_news",
"org_id": "0",
"section_key": "news",
"title": "企业动态",
"section_type": "list",
"content_type": "news",
"sort_order": 4,
"is_visible": "1",
"display_config": "{\"limit\": 2, \"show_view_all\": true}",
"style_config": "{\"item_bg\": \"#1A1A1A\", \"item_border\": \"#222\"}",
"static_content": ""
},
{
"id": "sec_footer",
"org_id": "0",
"section_key": "footer",
"title": "页脚",
"section_type": "footer",
"content_type": "",
"sort_order": 99,
"is_visible": "1",
"display_config": "{\"show_qrcode\": false}",
"style_config": "{\"border_top\": \"1px solid rgba(255,255,255,0.06)\"}",
"static_content": "{\"copyright\": \"\\u00a9 2026 \\u5f00\\u5143\\u4e91\\u79d1\\u6280 \\u00b7 \\u56fd\\u5bb6\\u7ea7\\u9ad8\\u65b0\\u6280\\u672f\\u4f01\\u4e1a \\u00b7 \\u4e13\\u7cbe\\u7279\\u65b0\\u4f01\\u4e1a\"}"
},
{
"id": "sec_float",
"org_id": "0",
"section_key": "float_contact",
"title": "浮动商机入口",
"section_type": "float",
"content_type": "",
"sort_order": 100,
"is_visible": "1",
"display_config": "{\"position\": \"fixed\", \"right\": 24, \"bottom\": 24}",
"style_config": "{\"avatar_bg\": \"linear-gradient(135deg, #6C5CE7, #A29BFE)\", \"size\": \"56px\"}",
"static_content": "{\"bubble_text\": \"\\u6709\\u4ec0\\u4e48\\u53ef\\u4ee5\\u5e2e\\u60a8\\uff1f\", \"panel_title\": \"\\u4e91\\u5b9d\\u5546\\u673a\\u52a9\\u624b\", \"options\": [\"\\u60a8\\u5bf9\\u54ea\\u4e9b\\u4ea7\\u54c1\\u611f\\u5174\\u8da3\\uff1f\", \"\\u7ed9\\u6211\\u4eec\\u7559\\u8a00\", \"\\u7559\\u4e0b\\u8054\\u7cfb\\u65b9\\u5f0f\"]}"
}
]
}

View File

@ -0,0 +1,92 @@
{
"tblname": "cms_sections",
"alias": "cms_sections_list",
"title": "栏目管理",
"params": {
"sortby": [
"sort_order asc"
],
"logined_userorgid": "org_id",
"browserfields": {
"exclouded": [
"display_config",
"style_config",
"static_content"
],
"alters": {
"section_type": {
"uitype": "code",
"data": [
{
"value": "hero",
"text": "首屏Hero"
},
{
"value": "cards",
"text": "卡片(1+N+X)"
},
{
"value": "grid",
"text": "网格(案例)"
},
{
"value": "list",
"text": "列表(新闻)"
},
{
"value": "banner",
"text": "横幅(CTA)"
},
{
"value": "float",
"text": "浮动入口"
},
{
"value": "footer",
"text": "页脚"
}
]
},
"content_type": {
"uitype": "code",
"data": [
{
"value": "",
"text": "(静态)"
},
{
"value": "product",
"text": "产品"
},
{
"value": "case",
"text": "案例"
},
{
"value": "news",
"text": "新闻"
}
]
},
"is_visible": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "显示"
},
{
"value": "0",
"text": "隐藏"
}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/cms_sections_create.dspy')}}",
"update_data_url": "{{entire_url('../api/cms_sections_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/cms_sections_delete.dspy')}}"
}
}
}

View File

@ -58,6 +58,11 @@
"name": "created_at",
"title": "创建时间",
"type": "timestamp"
},
{
"name": "display_config",
"title": "分类展示配置JSON",
"type": "text"
}
],
"indexes": [

View File

@ -0,0 +1,116 @@
{
"summary": [
{
"name": "cms_sections",
"title": "CMS栏目表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "org_id",
"title": "组织ID",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "section_key",
"title": "栏目标识(hero/products/cases/news/cta/footer)",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "title",
"title": "栏目标题",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "subtitle",
"title": "栏目副标题",
"type": "str",
"length": 255
},
{
"name": "section_type",
"title": "展示类型(hero/cards/grid/list/banner/float)",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "content_type",
"title": "关联内容类型(product/case/news/空=静态)",
"type": "str",
"length": 32
},
{
"name": "sort_order",
"title": "排序号",
"type": "int",
"default": "0"
},
{
"name": "is_visible",
"title": "是否显示(1/0)",
"type": "str",
"length": 1,
"default": "1"
},
{
"name": "display_config",
"title": "展示配置JSON",
"type": "text"
},
{
"name": "style_config",
"title": "样式配置JSON",
"type": "text"
},
{
"name": "static_content",
"title": "静态内容(用于hero/cta等固定内容栏目)",
"type": "text"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
}
],
"indexes": [
{
"name": "idx_sections_org_key",
"idxtype": "unique",
"idxfields": [
"org_id",
"section_key"
]
},
{
"name": "idx_sections_sort",
"idxtype": "index",
"idxfields": [
"sort_order"
]
}
],
"codes": []
}

View File

@ -1,33 +1,33 @@
"""
entcms RBAC权限配置
entcms RBAC权限配置 企业类型: owner
角色: superuser(继承全部), webmaster(内容管理), reviewer(审核),
supervisor(主管), customer-support(客服)
匿名: any (公开页面)
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py
"""
import os
import sys
import subprocess
import os, sys, subprocess
# 查找Sage根目录
def find_sage_root():
for candidate in [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
]:
if os.path.isdir(os.path.join(candidate, "wwwroot")) and \
os.path.isdir(os.path.join(candidate, "py3")):
return candidate
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
if os.path.isdir(os.path.join(c, "wwwroot")) and os.path.isdir(os.path.join(c, "py3")):
return c
return None
sage_root = find_sage_root()
if not sage_root:
print("ERROR: Cannot find Sage root directory")
sys.exit(1)
print("ERROR: Cannot find Sage root"); sys.exit(1)
python = os.path.join(sage_root, "py3", "bin", "python")
set_perm = os.path.join(sage_root, "set_role_perm.py")
py = os.path.join(sage_root, "py3", "bin", "python")
sp = os.path.join(sage_root, "set_role_perm.py")
# 权限配置
paths_any = [
# 公开页面和静态资源
def run(role, paths):
for p in paths:
print(f" {role:30s} {p}")
subprocess.run([py, sp, role, p], cwd=sage_root, capture_output=True)
# ─── anonymous (any) — 公开页面 + 公开API ───
any_paths = [
"/entcms/index.ui",
"/entcms/news.ui",
"/entcms/news_detail.ui",
@ -36,54 +36,105 @@ paths_any = [
"/entcms/cms_styles.css",
"/entcms/cms_scripts.js",
"/entcms/menu.ui",
# 公开API
"/entcms/api/submit_lead.dspy",
"/entcms/api/get_config.dspy",
"/entcms/api/get_published_content.dspy",
"/entcms/api/get_content_detail.dspy",
"/entcms/api/get_sections.dspy",
]
paths_logined = [
# 管理后台
# ─── webmaster — 内容/分类/栏目/配置/线索 全部CRUD ───
webmaster_paths = [
"/entcms",
"/entcms/admin.ui",
# CRUD页面
"/entcms/cms_content_list",
"/entcms/cms_content_list/%",
"/entcms/cms_categories_list",
"/entcms/cms_categories_list/%",
"/entcms/cms_leads_list",
"/entcms/cms_leads_list/%",
"/entcms/cms_site_config_list",
"/entcms/cms_site_config_list/%",
# 管理API
# 内容
"/entcms/cms_content_list", "/entcms/cms_content_list/%",
"/entcms/api/cms_content_create.dspy",
"/entcms/api/cms_content_update.dspy",
"/entcms/api/cms_content_delete.dspy",
"/entcms/api/cms_content_list.dspy",
# 分类
"/entcms/cms_categories_list", "/entcms/cms_categories_list/%",
"/entcms/api/cms_categories_create.dspy",
"/entcms/api/cms_categories_update.dspy",
"/entcms/api/cms_categories_delete.dspy",
"/entcms/api/cms_categories_list.dspy",
"/entcms/api/category_options.dspy",
"/entcms/api/cms_leads_create.dspy",
"/entcms/api/cms_leads_update.dspy",
"/entcms/api/cms_leads_delete.dspy",
"/entcms/api/cms_leads_list.dspy",
# 栏目
"/entcms/cms_sections_list", "/entcms/cms_sections_list/%",
"/entcms/api/cms_sections_create.dspy",
"/entcms/api/cms_sections_update.dspy",
"/entcms/api/cms_sections_delete.dspy",
"/entcms/api/cms_sections_list.dspy",
# 站点配置
"/entcms/cms_site_config_list", "/entcms/cms_site_config_list/%",
"/entcms/api/cms_site_config_create.dspy",
"/entcms/api/cms_site_config_update.dspy",
"/entcms/api/cms_site_config_delete.dspy",
"/entcms/api/cms_site_config_list.dspy",
# 线索管理
"/entcms/cms_leads_list", "/entcms/cms_leads_list/%",
"/entcms/api/cms_leads_create.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",
]
def set_perms(role, paths):
for path in paths:
cmd = [python, set_perm, role, path]
print(f" {role:20s} {path}")
subprocess.run(cmd, cwd=sage_root, capture_output=True)
# ─── reviewer — 查看内容 + 审批(只改status) ───
reviewer_paths = [
"/entcms",
"/entcms/admin.ui",
"/entcms/cms_content_list", "/entcms/cms_content_list/%",
"/entcms/api/cms_content_list.dspy",
"/entcms/api/cms_content_update.dspy", # 仅更新status字段
"/entcms/api/category_options.dspy",
]
# ─── supervisor — 查看全部 + 审批配置 + 线索管理 ───
supervisor_paths = [
"/entcms",
"/entcms/admin.ui",
# 只读
"/entcms/cms_content_list", "/entcms/cms_content_list/%",
"/entcms/cms_categories_list", "/entcms/cms_categories_list/%",
"/entcms/cms_sections_list", "/entcms/cms_sections_list/%",
"/entcms/cms_site_config_list", "/entcms/cms_site_config_list/%",
# 列表API(只读)
"/entcms/api/cms_content_list.dspy",
"/entcms/api/cms_categories_list.dspy",
"/entcms/api/cms_sections_list.dspy",
"/entcms/api/cms_site_config_list.dspy",
"/entcms/api/category_options.dspy",
# 线索全权
"/entcms/cms_leads_list", "/entcms/cms_leads_list/%",
"/entcms/api/cms_leads_create.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 — 线索查看和更新 ───
support_paths = [
"/entcms",
"/entcms/admin.ui",
"/entcms/cms_leads_list", "/entcms/cms_leads_list/%",
"/entcms/api/cms_leads_list.dspy",
"/entcms/api/cms_leads_update.dspy",
]
print("=== entcms RBAC权限配置 ===")
set_perms("any", paths_any)
set_perms("logined", paths_logined)
print("完成")
print(f"\n--- any (匿名用户) ---")
run("any", any_paths)
print(f"\n--- owner.webmaster (内容管理员) ---")
run("owner.webmaster", webmaster_paths)
print(f"\n--- owner.reviewer (内容审核) ---")
run("owner.reviewer", reviewer_paths)
print(f"\n--- owner.supervisor (主管) ---")
run("owner.supervisor", supervisor_paths)
print(f"\n--- owner.customer-support (客服) ---")
run("owner.customer-support", support_paths)
print("\n完成")

View File

@ -87,6 +87,61 @@
}
]
},
{
"widgettype": "Button",
"options": {
"css": "card",
"padding": "20px",
"borderRadius": "12px",
"border": "none"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.sage_main_content",
"options": {
"url": "{{entire_url('/entcms/cms_sections_list')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"alignItems": "flex-start",
"gap": "8px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "🎨",
"fontSize": "32px"
}
},
{
"widgettype": "Text",
"options": {
"text": "栏目管理",
"fontSize": "18px",
"fontWeight": "bold"
}
},
{
"widgettype": "Text",
"options": {
"text": "栏目排序、显示隐藏、展示风格",
"fontSize": "13px",
"color": "#999"
}
}
]
}
]
},
{
"widgettype": "Button",
"options": {

View File

@ -0,0 +1,44 @@
import json
from appPublic.uniqueID import getID
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
sor = DBPools().sqlorContext(dbname)
data = {'id': getID()}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('section_key', None)
if v is not None:
data['section_key'] = v
v = params_kw.get('title', None)
if v is not None:
data['title'] = v
v = params_kw.get('subtitle', None)
if v is not None:
data['subtitle'] = v
v = params_kw.get('section_type', None)
if v is not None:
data['section_type'] = v
v = params_kw.get('content_type', None)
if v is not None:
data['content_type'] = v
v = params_kw.get('sort_order', None)
if v is not None:
data['sort_order'] = v
v = params_kw.get('is_visible', None)
if v is not None:
data['is_visible'] = v
v = params_kw.get('display_config', None)
if v is not None:
data['display_config'] = v
v = params_kw.get('style_config', None)
if v is not None:
data['style_config'] = v
v = params_kw.get('static_content', None)
if v is not None:
data['static_content'] = v
await sor.C('cms_sections', data)
print(json.dumps({'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}, ensure_ascii=False))

View File

@ -0,0 +1,14 @@
import json
from appPublic.uniqueID import getID
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
sor = DBPools().sqlorContext(dbname)
_id = params_kw.get('id', '')
if not _id:
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
return
await sor.D('cms_sections', {'id': _id})
print(json.dumps({'widgettype': 'Message', 'options': {'text': '删除成功', 'messagetype': 'success'}}, ensure_ascii=False))

View File

@ -0,0 +1,22 @@
import json
from appPublic.uniqueID import getID
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
sor = DBPools().sqlorContext(dbname)
ns = {'sort': 'sort_order asc'}
filter_json = params_kw.get('data_filter', None)
if filter_json:
from sqlor.filter import DBFilter
try:
filter_def = json.loads(filter_json) if isinstance(filter_json, str) else filter_json
dbf = DBFilter(filter_def)
conds = dbf.gen(params_kw)
ns.update(conds)
ns.update(dbf.consts)
except Exception:
pass
rows = await sor.R('cms_sections', ns)
print(json.dumps({'status': 'ok', 'rows': rows, 'total': len(rows)}, ensure_ascii=False))

View File

@ -0,0 +1,47 @@
import json
from appPublic.uniqueID import getID
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
sor = DBPools().sqlorContext(dbname)
data = {'id': params_kw.get('id', '')}
if not data['id']:
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
return
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('section_key', None)
if v is not None:
data['section_key'] = v
v = params_kw.get('title', None)
if v is not None:
data['title'] = v
v = params_kw.get('subtitle', None)
if v is not None:
data['subtitle'] = v
v = params_kw.get('section_type', None)
if v is not None:
data['section_type'] = v
v = params_kw.get('content_type', None)
if v is not None:
data['content_type'] = v
v = params_kw.get('sort_order', None)
if v is not None:
data['sort_order'] = v
v = params_kw.get('is_visible', None)
if v is not None:
data['is_visible'] = v
v = params_kw.get('display_config', None)
if v is not None:
data['display_config'] = v
v = params_kw.get('style_config', None)
if v is not None:
data['style_config'] = v
v = params_kw.get('static_content', None)
if v is not None:
data['static_content'] = v
await sor.U('cms_sections', data)
print(json.dumps({'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}, ensure_ascii=False))

View File

@ -0,0 +1,20 @@
import json
from appPublic.uniqueID import getID
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
sor = DBPools().sqlorContext(dbname)
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
rows = await sor.R('cms_sections', ns)
# Parse JSON fields
for r in rows:
for field in ['display_config', 'style_config', 'static_content']:
v = r.get(field, None)
if v and isinstance(v, str):
try:
r[field] = json.loads(v)
except:
pass
print(json.dumps({'status': 'ok', 'rows': rows, 'total': len(rows)}, ensure_ascii=False))

View File

@ -9,6 +9,12 @@
"url": "{{entire_url('/entcms/cms_content_list')}}",
"target": "app.sage_main_content"
},
{
"name": "cms_sections_list",
"label": "栏目管理",
"url": "{{entire_url('/entcms/cms_sections_list')}}",
"target": "app.sage_main_content"
},
{
"name": "cms_categories_list",
"label": "内容分类",

84
scripts/init_superuser.py Normal file
View File

@ -0,0 +1,84 @@
"""
初始化超级用户
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/scripts/init_superuser.py [username] [password]
默认: admin / admin123
"""
import os, sys, asyncio
from sqlor.dbpools import DBPools
from appPublic.jsonConfig import getConfig
from appPublic.uniqueID import getID
# Sage环境导入password_encode
sys.path.insert(0, os.path.expanduser("~/repos/sage"))
from appPublic.password import password_encode
async def main():
username = sys.argv[1] if len(sys.argv) > 1 else "admin"
password = sys.argv[2] if len(sys.argv) > 2 else "admin123"
config = getConfig('.')
db = DBPools(config.databases)
async with db.sqlorContext('sage') as sor:
# 检查用户是否存在
existing = await sor.R('users', {'username': username})
if existing:
print(f"用户 {username} 已存在 (id={existing[0]['id']})")
# 更新密码
await sor.U('users', {
'id': existing[0]['id'],
'passwd': password_encode(password)
})
print(f"密码已更新为: {password}")
else:
user_id = getID()
await sor.C('users', {
'id': user_id,
'username': username,
'passwd': password_encode(password),
'orgid': '0',
'orgtypeid': 'owner',
'status': '1',
})
print(f"用户已创建: {username} (id={user_id})")
# 查找或创建superuser角色
roles = await sor.R('role', {'orgtypeid': 'owner', 'name': 'superuser'})
if not roles:
role_id = getID()
await sor.C('role', {
'id': role_id,
'orgtypeid': 'owner',
'name': 'superuser'
})
print(f"角色 owner.superuser 已创建 (id={role_id})")
else:
role_id = roles[0]['id']
print(f"角色 owner.superuser 已存在 (id={role_id})")
# 获取用户ID
users = await sor.R('users', {'username': username})
uid = users[0]['id']
# 查找或创建用户-角色关联 (userrole表)
# 检查userrole表是否存在
try:
ur = await sor.R('userrole', {'userid': uid, 'roleid': role_id})
if not ur:
await sor.C('userrole', {
'id': getID(),
'userid': uid,
'roleid': role_id,
})
print(f"已分配角色 owner.superuser 给用户 {username}")
else:
print(f"用户 {username} 已拥有 owner.superuser 角色")
except Exception as e:
print(f"注意: userrole表操作异常: {e}")
print("可能需要手动分配角色")
print(f"\n登录信息:")
print(f" 用户名: {username}")
print(f" 密码: {password}")
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())