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:
parent
5cfb0e867b
commit
52f4632dfc
@ -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完成")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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\"]}"
|
||||
}
|
||||
]
|
||||
}
|
||||
92
entcms/json/cms_sections_list.json
Normal file
92
entcms/json/cms_sections_list.json
Normal 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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,6 +58,11 @@
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "display_config",
|
||||
"title": "分类展示配置JSON",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
|
||||
116
entcms/models/cms_sections.json
Normal file
116
entcms/models/cms_sections.json
Normal 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": []
|
||||
}
|
||||
@ -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完成")
|
||||
|
||||
@ -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": {
|
||||
|
||||
44
entcms/wwwroot/api/cms_sections_create.dspy
Normal file
44
entcms/wwwroot/api/cms_sections_create.dspy
Normal 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))
|
||||
14
entcms/wwwroot/api/cms_sections_delete.dspy
Normal file
14
entcms/wwwroot/api/cms_sections_delete.dspy
Normal 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))
|
||||
22
entcms/wwwroot/api/cms_sections_list.dspy
Normal file
22
entcms/wwwroot/api/cms_sections_list.dspy
Normal 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))
|
||||
47
entcms/wwwroot/api/cms_sections_update.dspy
Normal file
47
entcms/wwwroot/api/cms_sections_update.dspy
Normal 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))
|
||||
20
entcms/wwwroot/api/get_sections.dspy
Normal file
20
entcms/wwwroot/api/get_sections.dspy
Normal 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))
|
||||
@ -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": "内容分类",
|
||||
@ -35,4 +41,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
84
scripts/init_superuser.py
Normal file
84
scripts/init_superuser.py
Normal 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())
|
||||
Loading…
x
Reference in New Issue
Block a user