diff --git a/dingdingflow/scripts/load_path.py b/dingdingflow/scripts/load_path.py index c4fcbc3..56987c5 100644 --- a/dingdingflow/scripts/load_path.py +++ b/dingdingflow/scripts/load_path.py @@ -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完成") diff --git a/entcms/entcms/init.py b/entcms/entcms/init.py index a1d2bb9..0f87842 100644 --- a/entcms/entcms/init.py +++ b/entcms/entcms/init.py @@ -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 diff --git a/entcms/init/data.json b/entcms/init/data.json index 63774e1..6b15507 100644 --- a/entcms/init/data.json +++ b/entcms/init/data.json @@ -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\"]}" + } ] } \ No newline at end of file diff --git a/entcms/json/cms_sections_list.json b/entcms/json/cms_sections_list.json new file mode 100644 index 0000000..6365275 --- /dev/null +++ b/entcms/json/cms_sections_list.json @@ -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')}}" + } + } +} \ No newline at end of file diff --git a/entcms/models/cms_categories.json b/entcms/models/cms_categories.json index e2b282e..d5f0c1f 100644 --- a/entcms/models/cms_categories.json +++ b/entcms/models/cms_categories.json @@ -58,6 +58,11 @@ "name": "created_at", "title": "创建时间", "type": "timestamp" + }, + { + "name": "display_config", + "title": "分类展示配置JSON", + "type": "text" } ], "indexes": [ diff --git a/entcms/models/cms_sections.json b/entcms/models/cms_sections.json new file mode 100644 index 0000000..108dc4d --- /dev/null +++ b/entcms/models/cms_sections.json @@ -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": [] +} \ No newline at end of file diff --git a/entcms/scripts/load_path.py b/entcms/scripts/load_path.py index 532133a..c88b97f 100644 --- a/entcms/scripts/load_path.py +++ b/entcms/scripts/load_path.py @@ -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完成") diff --git a/entcms/wwwroot/admin.ui b/entcms/wwwroot/admin.ui index 8c1bb6e..1f0a29f 100644 --- a/entcms/wwwroot/admin.ui +++ b/entcms/wwwroot/admin.ui @@ -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": { diff --git a/entcms/wwwroot/api/cms_sections_create.dspy b/entcms/wwwroot/api/cms_sections_create.dspy new file mode 100644 index 0000000..0aeab2f --- /dev/null +++ b/entcms/wwwroot/api/cms_sections_create.dspy @@ -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)) diff --git a/entcms/wwwroot/api/cms_sections_delete.dspy b/entcms/wwwroot/api/cms_sections_delete.dspy new file mode 100644 index 0000000..430a2eb --- /dev/null +++ b/entcms/wwwroot/api/cms_sections_delete.dspy @@ -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)) diff --git a/entcms/wwwroot/api/cms_sections_list.dspy b/entcms/wwwroot/api/cms_sections_list.dspy new file mode 100644 index 0000000..7862860 --- /dev/null +++ b/entcms/wwwroot/api/cms_sections_list.dspy @@ -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)) diff --git a/entcms/wwwroot/api/cms_sections_update.dspy b/entcms/wwwroot/api/cms_sections_update.dspy new file mode 100644 index 0000000..1e55095 --- /dev/null +++ b/entcms/wwwroot/api/cms_sections_update.dspy @@ -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)) diff --git a/entcms/wwwroot/api/get_sections.dspy b/entcms/wwwroot/api/get_sections.dspy new file mode 100644 index 0000000..6711189 --- /dev/null +++ b/entcms/wwwroot/api/get_sections.dspy @@ -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)) diff --git a/entcms/wwwroot/menu.ui b/entcms/wwwroot/menu.ui index d95f42c..8a353b3 100644 --- a/entcms/wwwroot/menu.ui +++ b/entcms/wwwroot/menu.ui @@ -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 @@ } ] } -} +} \ No newline at end of file diff --git a/scripts/init_superuser.py b/scripts/init_superuser.py new file mode 100644 index 0000000..874ff3a --- /dev/null +++ b/scripts/init_superuser.py @@ -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())