refactor: 从独立webapp重构为纯Sage模块

- cms/: Python包(合并原entcms+dingdingflow)
  - init.py: 791行,load_cms()注册所有CRUD+审批函数
  - dingtalk_client.py: 钉钉API客户端
- models/: 7个表定义JSON(5个CMS+2个DD)
- json/: 7个CRUD定义JSON
- wwwroot/: 管理后台CRUD页面和API(37个dspy)
- init/data.yaml: 模块初始数据(appcodes/appcodes_kv/分类/栏目/配置)
- scripts/load_path.py: RBAC权限配置
- pyproject.toml: pip-installable包定义
- 删除: app/, conf/, build.sh, entcms/, dingdingflow/等webapp文件
- 数据库访问统一为DBPools()+_get_dbname()动态模式
This commit is contained in:
Hermes Agent 2026-06-15 11:06:11 +08:00
parent f70e8e4d26
commit 4495e9589b
92 changed files with 1461 additions and 3855 deletions

View File

@ -1,58 +1,60 @@
# 开元云科技 - 企业官网CMS系统
# CMS 内容管理模块
企业官网内容管理系统 + 钉钉审批流程基于Sage/bricks-framework开发。
企业官网内容管理与钉钉审批工作流模块基于Sage/bricks-framework开发。
## 模块
## 功能
| 模块 | 说明 |
- **内容管理**: 新闻/案例/产品/Banner的统一CRUD带发布审批状态流
- **分类管理**: 按content_type分组的层级分类
- **栏目管理**: 官网页面栏目配置
- **商机线索**: 网站访客提交 + AI抽取
- **站点配置**: Hero标语、页脚等KV配置
- **钉钉审批**: 内容发布审批工作流
## 数据库表
| 表名 | 用途 |
|------|------|
| **entcms** | 企业CMS - 新闻/案例/产品/Banner/线索管理 |
| **dingdingflow** | 钉钉审批流程 - 内容发布审批工作流 |
| cms_content | 统一内容表 |
| cms_categories | 内容分类 |
| cms_sections | 栏目管理 |
| cms_leads | 商机线索 |
| cms_site_config | 站点配置 |
| dd_approvals | 审批记录 |
| dd_approval_configs | 审批流程配置 |
## 目录结构
```
cms/
├── conf/config.json # 应用配置
├── wwwroot/ # 前端静态文件(统一目录)
│ ├── index.ui, news.ui, ... # 企业官网页面
│ ├── api/*.dspy # CMS后端API
│ └── dingdingflow/ # 钉钉审批模块前端
│ ├── index.ui, menu.ui
│ └── api/*.dspy
├── entcms/ # 企业CMS Python模块
├── dingdingflow/ # 钉钉审批Python模块
├── bricks -> pkgs/bricks/dist # 前端框架(符号链接)
├── build.sh # 构建脚本
├── start.sh / stop.sh # 启停脚本
├── init_superuser_permissions.py # superuser权限初始化
├── init_any_permissions.py # any权限初始化
└── scripts/init_superuser.py # 超级用户账号初始化
```
## 快速开始
## 安装
```bash
# 1. 构建并安装
cd ~/repos/cms && ./build.sh
# 2. 配置RBAC权限
cd ~/repos/sage
./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py
./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py
# 3. 重启Sage
./stop.sh && ./start.sh
pip install -e ~/repos/cms
```
## 文档
- [系统架构](docs/architecture.md)
- [测试用例](docs/test-cases.md)
- [开发日志](docs/)
## 集成
在Web应用(app/portal.py)中:
```python
from cms.init import load_cms
def init():
load_cms()
```
## 初始数据
`init/data.yaml` 包含:
- appcodes/appcodes_kv: 枚举编码(content_type, content_status, lead_status等)
- cms_categories: 默认分类
- cms_site_config: 默认站点配置
- cms_sections: 默认栏目配置
- dd_approval_configs: 默认审批配置
## 环境变量 (钉钉审批)
## 环境变量 (dingdingflow)
```
DINGTALK_APP_KEY=xxx
DINGTALK_APP_SECRET=xxx
DINGTALK_AGENT_ID=xxx
```
缺少环境变量时自动使用mock响应。

View File

@ -1,52 +0,0 @@
"""
开元云科技CMS 独立Web应用主入口
启动: py3/bin/python app/cms.py -p 9090 -w $(pwd)
"""
import os, sys
# 添加应用根目录到Python路径
app_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(app_dir)
sys.path.insert(0, root_dir)
# Ensure app/ is in path for local imports
sys.path.insert(0, app_dir)
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 get_module_dbname(m):
return 'ocai_cms'
def init():
rf = RegisterFunction()
set_globalvariable()
env = ServerEnv()
env.get_module_dbname = get_module_dbname
load_pybricks()
load_appbase()
load_rbac()
load_entcms()
load_dingdingflow()
if __name__ == '__main__':
webapp(init)

View File

@ -1,56 +0,0 @@
"""
CMS全局函数 注册到ServerEnv供.dspy和.ui调用
"""
from ahserver.serverenv import ServerEnv
def get_module_dbname(mname):
"""CMS应用统一使用ocai_cms数据库"""
return 'ocai_cms'
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

198
build.sh
View File

@ -1,198 +0,0 @@
#!/usr/bin/env bash
# 开元云科技CMS — 独立Web应用构建脚本
# 用法: cd ~/repos/cms && ./build.sh
set -e
cdir=$(pwd)
uname=$(id -un)
gname=$(id -gn)
echo "============================================"
echo " 开元云科技CMS — 独立Web应用构建"
echo "============================================"
# ===========================================
# 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
cd $m
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
done
# bricks前端
echo " install bricks..."
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
# ===========================================
# Step 3: RBAC + AppBase模块(认证依赖)
# ===========================================
echo ""
echo "--- Step 3: 安装RBAC/AppBase模块 ---"
for m in appbase rbac checklang
do
echo " install $m..."
cd $cdir/pkgs
if [ ! -d "$m" ]; then
git clone https://git.opencomputing.cn/yumoqing/$m
fi
cd $m
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
done
# ===========================================
# Step 4: CMS业务模块
# ===========================================
echo ""
echo "--- Step 4: 安装CMS业务模块 ---"
# entcms模块
echo " install entcms..."
cd $cdir/entcms
$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. 初始化权限:"
echo " py3/bin/python init_superuser_permissions.py"
echo " py3/bin/python init_any_permissions.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"

8
cms/__init__.py Normal file
View File

@ -0,0 +1,8 @@
"""
CMS - 企业官网内容管理与审批工作流模块
合并原 entcms + dingdingflow 两个子模块
"""
from .init import load_cms
__all__ = ['load_cms']

791
cms/init.py Normal file
View File

@ -0,0 +1,791 @@
"""
cms - 企业CMS内容管理与钉钉审批工作流模块
合并原 entcms + dingdingflow 两个子模块
提供:
- CMS Content CRUD (cms_content_*)
- CMS Categories CRUD (cms_categories_*)
- CMS Sections CRUD (cms_sections_*)
- CMS Leads CRUD (cms_leads_*)
- CMS Site Config CRUD (cms_site_config_*)
- 公开API (get_published_content, get_latest_news, get_content_detail, submit_lead, get_visible_sections, get_site_config, get_category_options)
- 钉钉审批 CRUD (dd_approvals_*, dd_approval_configs_*)
- 审批业务逻辑 (submit_approval, get_approval_status, handle_dingtalk_callback)
"""
import json
import logging
import datetime
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
from .dingtalk_client import get_dingtalk_client
logger = logging.getLogger(__name__)
MODULE_NAME = 'cms'
MODULE_VERSION = '2.0.0'
def _get_dbname():
"""Get the database name for this module (dynamic, not hardcoded)."""
env = ServerEnv()
return env.get_module_dbname(MODULE_NAME)
# ═══════════════════════════════════════════════════════════════════════════════
# CMS Content CRUD
# ═══════════════════════════════════════════════════════════════════════════════
async def cms_content_list(ns=None):
"""查询内容列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = ns or {}
ns.setdefault('sort', 'sort_order asc, created_at desc')
rows = await sor.R('cms_content', ns)
total = len(rows)
return {'rows': rows, 'total': total}
async def cms_content_create(data):
"""创建内容"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
data['id'] = getID()
await sor.C('cms_content', data)
return data
async def cms_content_update(data):
"""更新内容"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('cms_content', data)
return data
async def cms_content_delete(data):
"""删除内容"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('cms_content', data)
return data
# ═══════════════════════════════════════════════════════════════════════════════
# CMS Categories CRUD
# ═══════════════════════════════════════════════════════════════════════════════
async def cms_categories_list(ns=None):
"""查询分类列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = ns or {}
ns.setdefault('sort', 'sort_order asc')
rows = await sor.R('cms_categories', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_categories_create(data):
"""创建分类"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
data['id'] = getID()
await sor.C('cms_categories', data)
return data
async def cms_categories_update(data):
"""更新分类"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('cms_categories', data)
return data
async def cms_categories_delete(data):
"""删除分类"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('cms_categories', data)
return data
async def get_category_options(content_type=None):
"""获取分类下拉选项"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = {'sort': 'sort_order asc'}
if content_type:
ns['content_type'] = content_type
rows = await sor.R('cms_categories', ns)
options = [{'value': r['id'], 'text': r['name']} for r in rows]
return options
# ═══════════════════════════════════════════════════════════════════════════════
# CMS Sections CRUD
# ═══════════════════════════════════════════════════════════════════════════════
async def cms_sections_list(ns=None):
"""查询栏目列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
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):
"""创建栏目"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
data['id'] = getID()
await sor.C('cms_sections', data)
return data
async def cms_sections_update(data):
"""更新栏目"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('cms_sections', data)
return data
async def cms_sections_delete(data):
"""删除栏目"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('cms_sections', data)
return data
async def get_visible_sections():
"""获取所有可见栏目(公开接口)"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
rows = await sor.R('cms_sections', ns)
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 Exception:
pass
return rows
# ═══════════════════════════════════════════════════════════════════════════════
# CMS Leads CRUD
# ═══════════════════════════════════════════════════════════════════════════════
async def cms_leads_list(ns=None):
"""查询线索列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = ns or {}
ns.setdefault('sort', 'created_at desc')
rows = await sor.R('cms_leads', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_leads_create(data):
"""创建线索"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
data['id'] = getID()
await sor.C('cms_leads', data)
return data
async def cms_leads_update(data):
"""更新线索"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('cms_leads', data)
return data
async def cms_leads_delete(data):
"""删除线索"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('cms_leads', data)
return data
async def submit_lead(data):
"""公开接口 - 网站访客提交线索"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
data['id'] = getID()
data.setdefault('status', 'new')
data.setdefault('source', 'website')
await sor.C('cms_leads', data)
return {'status': 'ok', 'id': data['id']}
# ═══════════════════════════════════════════════════════════════════════════════
# CMS Site Config CRUD
# ═══════════════════════════════════════════════════════════════════════════════
async def cms_site_config_list(ns=None):
"""查询站点配置列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = ns or {}
ns.setdefault('sort', 'config_group asc, sort_order asc')
rows = await sor.R('cms_site_config', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_site_config_create(data):
"""创建站点配置"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
data['id'] = getID()
await sor.C('cms_site_config', data)
return data
async def cms_site_config_update(data):
"""更新站点配置"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('cms_site_config', data)
return data
async def cms_site_config_delete(data):
"""删除站点配置"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('cms_site_config', data)
return data
async def get_site_config(group=None):
"""获取站点配置(公开接口)"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = {'sort': 'sort_order asc'}
if group:
ns['config_group'] = group
rows = await sor.R('cms_site_config', ns)
result = {}
for r in rows:
g = r.get('config_group', '')
if g not in result:
result[g] = {}
result[g][r.get('config_key', '')] = r.get('config_value', '')
return result
# ═══════════════════════════════════════════════════════════════════════════════
# Public Content APIs
# ═══════════════════════════════════════════════════════════════════════════════
async def get_published_content(content_type=None, limit=10):
"""获取已发布内容(公开接口)"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = {'status': 'published', 'sort': 'sort_order asc, published_at desc'}
if content_type:
ns['content_type'] = content_type
rows = await sor.R('cms_content', ns)
if limit:
rows = rows[:limit]
return rows
async def get_latest_news(limit=2):
"""获取最新新闻(公开接口)"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = {'status': 'published', 'content_type': 'news', 'sort': 'published_at desc'}
rows = await sor.R('cms_content', ns)
return rows[:limit]
async def get_content_detail(content_id):
"""获取内容详情(公开接口)"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = {'id': content_id, 'status': 'published'}
rows = await sor.R('cms_content', ns)
return rows[0] if rows else None
# ═══════════════════════════════════════════════════════════════════════════════
# Content Approval Integration
# ═══════════════════════════════════════════════════════════════════════════════
async def submit_content_for_approval(content_id, title, applicant_id):
"""提交内容审批(调用钉钉审批流程)"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
# 更新内容状态为pending
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
# 调用审批流程
result = await submit_approval('content_publish', content_id, title, applicant_id)
# 保存审批ID
if result and result.get('approval_id'):
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
return result
# ═══════════════════════════════════════════════════════════════════════════════
# DD Approvals CRUD (原 dingdingflow)
# ═══════════════════════════════════════════════════════════════════════════════
async def dd_approvals_create(data):
"""创建审批记录"""
dbname = _get_dbname()
db = DBPools()
data['id'] = getID()
if 'org_id' not in data:
data['org_id'] = '0'
if 'status' not in data:
data['status'] = 'pending'
if 'created_at' not in data:
data['created_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
async with db.sqlorContext(dbname) as sor:
await sor.C('dd_approvals', data)
return {'id': data['id']}
async def dd_approvals_update(data):
"""更新审批记录"""
dbname = _get_dbname()
db = DBPools()
record_id = data.get('id')
if not record_id:
raise ValueError('id is required for update')
async with db.sqlorContext(dbname) as sor:
await sor.U('dd_approvals', data)
return {'id': record_id}
async def dd_approvals_delete(data):
"""删除审批记录"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('dd_approvals', data)
return data
async def dd_approvals_list(ns=None):
"""查询审批记录列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = ns or {}
ns.setdefault('sort', 'created_at desc')
rows = await sor.R('dd_approvals', ns)
return {'rows': rows, 'total': len(rows)}
# ═══════════════════════════════════════════════════════════════════════════════
# DD Approval Configs CRUD (原 dingdingflow)
# ═══════════════════════════════════════════════════════════════════════════════
async def dd_approval_configs_create(data):
"""创建审批配置"""
dbname = _get_dbname()
db = DBPools()
data['id'] = getID()
if 'org_id' not in data:
data['org_id'] = '0'
if 'is_active' not in data:
data['is_active'] = '1'
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if 'created_at' not in data:
data['created_at'] = now_str
data['updated_at'] = now_str
async with db.sqlorContext(dbname) as sor:
await sor.C('dd_approval_configs', data)
return {'id': data['id']}
async def dd_approval_configs_update(data):
"""更新审批配置"""
dbname = _get_dbname()
db = DBPools()
record_id = data.get('id')
if not record_id:
raise ValueError('id is required for update')
data['updated_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
async with db.sqlorContext(dbname) as sor:
await sor.U('dd_approval_configs', data)
return {'id': record_id}
async def dd_approval_configs_delete(data):
"""删除审批配置"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('dd_approval_configs', data)
return data
async def dd_approval_configs_list(ns=None):
"""查询审批配置列表"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
ns = ns or {}
ns.setdefault('sort', 'biz_type')
rows = await sor.R('dd_approval_configs', ns)
return {'rows': rows, 'total': len(rows)}
async def get_approval_config_by_type(org_id, biz_type):
"""根据org_id和biz_type获取审批配置"""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
rows = await sor.R('dd_approval_configs', {'org_id': org_id, 'biz_type': biz_type})
if rows:
return rows[0]
return None
# ═══════════════════════════════════════════════════════════════════════════════
# Approval Workflow Business Logic (原 dingdingflow)
# ═══════════════════════════════════════════════════════════════════════════════
async def submit_approval(biz_type, biz_id, title, applicant_id, org_id='0'):
"""
提交审批请求:
1. 查找审批配置
2. 创建审批记录
3. 调用钉钉API创建审批实例
4. 保存钉钉实例ID
"""
client = get_dingtalk_client()
# 查找审批配置
config = await get_approval_config_by_type(org_id, biz_type)
if not config:
logger.error('No approval config found for org_id=%s, biz_type=%s', org_id, biz_type)
return {'success': False, 'message': f'No approval config found for biz_type={biz_type}'}
process_code = config.get('process_code', '') if isinstance(config, dict) else getattr(config, 'process_code', '') or ''
form_config_raw = config.get('form_config', '') if isinstance(config, dict) else getattr(config, 'form_config', '') or ''
# 从form_config构建表单数据
form_data = []
if form_config_raw:
try:
form_config = json.loads(form_config_raw) if isinstance(form_config_raw, str) else form_config_raw
if isinstance(form_config, list):
form_data = form_config
except (json.JSONDecodeError, TypeError) as e:
logger.warning('Failed to parse form_config: %s', str(e))
# 无表单数据时创建最小表单
if not form_data:
form_data = [
{'name': '审批标题', 'value': title},
{'name': '业务类型', 'value': biz_type},
]
# 调用钉钉API
result = client.create_approval_instance(process_code, form_data, applicant_id)
if not result['success']:
# API失败仍然创建记录
approval_data = {
'biz_type': biz_type,
'biz_id': biz_id,
'title': title,
'applicant_id': applicant_id,
'org_id': org_id,
'status': 'pending',
'dingtalk_instance_id': '',
'comment': f"DingTalk API error: {result.get('errmsg', '')}",
}
approval = await dd_approvals_create(approval_data)
return {
'success': False,
'message': f"DingTalk API failed: {result.get('errmsg', '')}",
'approval_id': approval['id'],
}
# 创建审批记录
approval_data = {
'biz_type': biz_type,
'biz_id': biz_id,
'title': title,
'applicant_id': applicant_id,
'org_id': org_id,
'status': 'pending',
'dingtalk_instance_id': result['instance_id'],
}
approval = await dd_approvals_create(approval_data)
logger.info(
'Approval submitted: id=%s, instance=%s, biz=%s/%s',
approval['id'], result['instance_id'], biz_type, biz_id,
)
return {
'success': True,
'message': 'Approval submitted successfully',
'approval_id': approval['id'],
'instance_id': result['instance_id'],
}
async def get_approval_status(approval_id):
"""查询钉钉审批最新状态并同步到本地"""
dbname = _get_dbname()
db = DBPools()
# 获取本地记录
async with db.sqlorContext(dbname) as sor:
rows = await sor.R('dd_approvals', {'id': approval_id})
if not rows:
return {'success': False, 'message': 'Approval record not found'}
record = rows[0]
instance_id = record.get('dingtalk_instance_id', '') if isinstance(record, dict) else getattr(record, 'dingtalk_instance_id', '')
current_status = record.get('status', '') if isinstance(record, dict) else getattr(record, 'status', '')
# 已完成无需再查
if current_status in ('approved', 'rejected', 'cancelled'):
return {
'success': True,
'status': current_status,
'approval_id': approval_id,
'instance_id': instance_id,
}
if not instance_id:
return {
'success': True,
'status': current_status,
'approval_id': approval_id,
'instance_id': '',
'message': 'No DingTalk instance ID, cannot sync',
}
# 查询钉钉
client = get_dingtalk_client()
dt_result = client.get_approval_instance(instance_id)
if not dt_result['success']:
return {
'success': False,
'message': f"DingTalk query failed: {dt_result.get('errmsg', '')}",
'status': current_status,
}
# 映射钉钉状态
dt_status = dt_result.get('status', '')
dt_result_val = dt_result.get('result', '')
new_status = current_status
if dt_status == 'COMPLETED':
if dt_result_val == 'agree':
new_status = 'approved'
elif dt_result_val == 'refuse':
new_status = 'rejected'
elif dt_status == 'TERMINATED':
new_status = 'cancelled'
# 更新本地记录
if new_status != current_status:
update_data = {'id': approval_id, 'status': new_status}
if new_status in ('approved', 'rejected', 'cancelled'):
update_data['completed_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
await dd_approvals_update(update_data)
logger.info('Approval %s status synced: %s -> %s', approval_id, current_status, new_status)
return {
'success': True,
'status': new_status,
'approval_id': approval_id,
'instance_id': instance_id,
}
async def handle_dingtalk_callback(data):
"""
处理钉钉webhook回调
钉钉在审批状态变化时发送回调
"""
logger.info('DingTalk callback received: %s', json.dumps(data, ensure_ascii=False))
instance_id = data.get('processInstanceId', '')
if not instance_id:
return {'success': False, 'message': 'Missing processInstanceId'}
callback_type = data.get('type', '')
if callback_type != 'bpms_instance_change':
logger.info('Ignoring callback type: %s', callback_type)
return {'success': True, 'message': f'Ignored callback type: {callback_type}'}
# 查找本地审批记录
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
rows = await sor.R('dd_approvals', {'dingtalk_instance_id': instance_id})
if not rows:
logger.warning('No local approval found for instance_id=%s', instance_id)
return {'success': False, 'message': f'No approval found for instance {instance_id}'}
record = rows[0]
record_id = record.get('id', '') if isinstance(record, dict) else getattr(record, 'id', '')
current_status = record.get('status', '') if isinstance(record, dict) else getattr(record, 'status', '')
# 映射回调状态
dt_result = data.get('result', '')
new_status = current_status
if dt_result == 'agree':
new_status = 'approved'
elif dt_result == 'refuse':
new_status = 'rejected'
elif callback_type == 'terminate':
new_status = 'cancelled'
# 更新记录
if new_status != current_status:
update_data = {
'id': record_id,
'status': new_status,
'comment': data.get('remark', ''),
}
if new_status in ('approved', 'rejected', 'cancelled'):
update_data['completed_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
await dd_approvals_update(update_data)
logger.info('Callback: approval %s updated to %s', record_id, new_status)
# 通知CMS内容状态变更
biz_type = record.get('biz_type', '') if isinstance(record, dict) else getattr(record, 'biz_type', '')
biz_id = record.get('biz_id', '') if isinstance(record, dict) else getattr(record, 'biz_id', '')
if biz_type == 'content_publish' and biz_id:
content_status = 'published' if new_status == 'approved' else 'draft' if new_status == 'rejected' else ''
if content_status:
content_update = {'id': biz_id, 'status': content_status}
if content_status == 'published':
content_update['published_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
async with db.sqlorContext(dbname) as sor:
await sor.U('cms_content', content_update)
logger.info('Callback: cms_content %s updated to %s', biz_id, content_status)
return {
'success': True,
'message': f'Approval {record_id} updated to {new_status}',
'approval_id': record_id,
'status': new_status,
}
# ═══════════════════════════════════════════════════════════════════════════════
# Module Loader
# ═══════════════════════════════════════════════════════════════════════════════
def load_cms():
"""注册所有CMS模块函数到ServerEnv"""
env = ServerEnv()
# Content CRUD
env.cms_content_list = cms_content_list
env.cms_content_create = cms_content_create
env.cms_content_update = cms_content_update
env.cms_content_delete = cms_content_delete
# Categories CRUD
env.cms_categories_list = cms_categories_list
env.cms_categories_create = cms_categories_create
env.cms_categories_update = cms_categories_update
env.cms_categories_delete = cms_categories_delete
env.get_category_options = get_category_options
# 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
# Leads CRUD
env.cms_leads_list = cms_leads_list
env.cms_leads_create = cms_leads_create
env.cms_leads_update = cms_leads_update
env.cms_leads_delete = cms_leads_delete
env.submit_lead = submit_lead
# Site Config CRUD
env.cms_site_config_list = cms_site_config_list
env.cms_site_config_create = cms_site_config_create
env.cms_site_config_update = cms_site_config_update
env.cms_site_config_delete = cms_site_config_delete
env.get_site_config = get_site_config
# Public Content APIs
env.get_published_content = get_published_content
env.get_latest_news = get_latest_news
env.get_content_detail = get_content_detail
env.submit_content_for_approval = submit_content_for_approval
# DD Approvals CRUD
env.dd_approvals_create = dd_approvals_create
env.dd_approvals_update = dd_approvals_update
env.dd_approvals_delete = dd_approvals_delete
env.dd_approvals_list = dd_approvals_list
# DD Approval Configs CRUD
env.dd_approval_configs_create = dd_approval_configs_create
env.dd_approval_configs_update = dd_approval_configs_update
env.dd_approval_configs_delete = dd_approval_configs_delete
env.dd_approval_configs_list = dd_approval_configs_list
env.get_approval_config_by_type = get_approval_config_by_type
# Approval Business Logic
env.submit_approval = submit_approval
env.get_approval_status = get_approval_status
env.handle_dingtalk_callback = handle_dingtalk_callback
# DingTalk Client
env.get_dingtalk_client = get_dingtalk_client
logger.info('cms module loaded (v%s)', MODULE_VERSION)
return True

View File

@ -1,76 +0,0 @@
{
"password_key":"!@#$%^&*(*&^%$QWERTYUIqwertyui234567",
"logger": {
"name": "cms",
"levelname": "info",
"logfile": "$[workdir]$/logs/cms.log"
},
"filesroot": "$[workdir]$/files",
"databases": {
"ocai_cms": {
"driver": "mysql",
"async_mode": true,
"coding": "utf8",
"dbname": "ocai_cms",
"kwargs": {
"user": "test",
"db": "ocai_cms",
"password": "SS+C1MDMJrslBwGzYIv3nQ==",
"host": "db"
}
}
},
"website": {
"paths": [
[
"$[workdir]$/wwwroot",
""
],
[
"$[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,20 +0,0 @@
# dingdingflow - 钉钉审批流程
为CMS内容发布提供钉钉审批工作流。
## 数据表
- dd_approvals: 审批记录
- dd_approval_configs: 审批流程配置
## 环境变量
```
DINGTALK_APP_KEY=钉钉应用AppKey
DINGTALK_APP_SECRET=钉钉应用AppSecret
DINGTALK_AGENT_ID=钉钉应用AgentId
```
未配置时自动进入开发模式(mock响应)。
## API
- POST /dingdingflow/api/submit_approval.dspy - 提交审批
- POST /dingdingflow/api/dingtalk_callback.dspy - 钉钉回调(公开)

View File

@ -1 +0,0 @@
# dingdingflow module

View File

@ -1,436 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dingdingflow module initialization.
Registers all module functions with ServerEnv for use in .ui and .dspy files.
"""
import json
import logging
import datetime
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
from dingdingflow.dingtalk_client import get_dingtalk_client
logger = logging.getLogger(__name__)
MODULE_NAME = "dingdingflow"
MODULE_VERSION = "1.0.0"
def _get_dbname():
"""Get the database name for this module."""
env = ServerEnv()
return env.get_module_dbname(MODULE_NAME)
# ─── CRUD: dd_approvals ───────────────────────────────────────────────────────
async def create_dd_approval(data):
"""Create a new approval record."""
new_id = getID()
data["id"] = new_id
if "org_id" not in data:
data["org_id"] = "0"
if "status" not in data:
data["status"] = "pending"
if "created_at" not in data:
data["created_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
await sor.C("dd_approvals", data)
return {"id": new_id}
async def update_dd_approval(data):
"""Update an existing approval record."""
record_id = data.get("id")
if not record_id:
raise ValueError("id is required for update")
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
await sor.U("dd_approvals", {"id": record_id}, data)
return {"id": record_id}
async def delete_dd_approval(record_id):
"""Delete an approval record by ID."""
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
await sor.D("dd_approvals", {"id": record_id})
return True
async def get_dd_approval(record_id):
"""Get a single approval record by ID."""
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
rows = await sor.R("dd_approvals", {"id": record_id})
if rows:
return rows[0]
return None
async def list_dd_approvals(filters=None, page=1, rows=20, sort="created_at desc"):
"""List approval records with optional filters."""
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
query_filters = filters or {}
ns = {"page": page, "rows": rows, "sort": sort}
ns.update(query_filters)
result = await sor.R("dd_approvals", query_filters, page=page, rows=rows, sort=sort)
return result
# ─── CRUD: dd_approval_configs ────────────────────────────────────────────────
async def create_dd_approval_config(data):
"""Create a new approval config record."""
new_id = getID()
data["id"] = new_id
if "org_id" not in data:
data["org_id"] = "0"
if "is_active" not in data:
data["is_active"] = "1"
if "created_at" not in data:
data["created_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
data["updated_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
await sor.C("dd_approval_configs", data)
return {"id": new_id}
async def update_dd_approval_config(data):
"""Update an existing approval config record."""
record_id = data.get("id")
if not record_id:
raise ValueError("id is required for update")
data["updated_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
await sor.U("dd_approval_configs", {"id": record_id}, data)
return {"id": record_id}
async def delete_dd_approval_config(record_id):
"""Delete an approval config record by ID."""
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
await sor.D("dd_approval_configs", {"id": record_id})
return True
async def get_dd_approval_config(record_id):
"""Get a single approval config record by ID."""
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
rows = await sor.R("dd_approval_configs", {"id": record_id})
if rows:
return rows[0]
return None
async def get_approval_config_by_type(org_id, biz_type):
"""Get approval config by org_id and biz_type (unique constraint)."""
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
rows = await sor.R("dd_approval_configs", {"org_id": org_id, "biz_type": biz_type})
if rows:
return rows[0]
return None
# ─── Business Logic: Approval Workflow ────────────────────────────────────────
async def submit_approval(biz_type, biz_id, title, applicant_id, org_id="0"):
"""
Submit a new approval request.
1. Look up the approval config for this biz_type
2. Create a dd_approvals record
3. Call DingTalk API to create the approval instance
4. Store the DingTalk instance_id back in the record
Returns: dict with approval record details
"""
client = get_dingtalk_client()
# Look up config
config = await get_approval_config_by_type(org_id, biz_type)
if not config:
logger.error("No approval config found for org_id=%s, biz_type=%s", org_id, biz_type)
return {"success": False, "message": f"No approval config found for biz_type={biz_type}"}
process_code = getattr(config, "process_code", "") or ""
agent_id = getattr(config, "agent_id", "") or ""
form_config_raw = getattr(config, "form_config", "") or ""
# Build form data from form_config
form_data = []
if form_config_raw:
try:
form_config = json.loads(form_config_raw) if isinstance(form_config_raw, str) else form_config_raw
if isinstance(form_config, list):
form_data = form_config
except (json.JSONDecodeError, TypeError) as e:
logger.warning("Failed to parse form_config: %s", str(e))
# If no form_data, create minimal form with title
if not form_data:
form_data = [
{"name": "审批标题", "value": title},
{"name": "业务类型", "value": biz_type},
]
# Call DingTalk API
result = client.create_approval_instance(process_code, form_data, applicant_id)
if not result["success"]:
# Still create the record with failed status
approval_data = {
"biz_type": biz_type,
"biz_id": biz_id,
"title": title,
"applicant_id": applicant_id,
"org_id": org_id,
"status": "pending",
"dingtalk_instance_id": "",
"comment": f"DingTalk API error: {result.get('errmsg', '')}",
}
approval = await create_dd_approval(approval_data)
return {
"success": False,
"message": f"DingTalk API failed: {result.get('errmsg', '')}",
"approval_id": approval["id"],
}
# Create approval record with instance_id
approval_data = {
"biz_type": biz_type,
"biz_id": biz_id,
"title": title,
"applicant_id": applicant_id,
"org_id": org_id,
"status": "pending",
"dingtalk_instance_id": result["instance_id"],
}
approval = await create_dd_approval(approval_data)
logger.info(
"Approval submitted: id=%s, instance=%s, biz=%s/%s",
approval["id"],
result["instance_id"],
biz_type,
biz_id,
)
return {
"success": True,
"message": "Approval submitted successfully",
"approval_id": approval["id"],
"instance_id": result["instance_id"],
}
async def get_approval_status(approval_id):
"""
Query DingTalk for the latest approval status and sync to local DB.
Returns: dict with current status info
"""
# Get local record
record = await get_dd_approval(approval_id)
if not record:
return {"success": False, "message": "Approval record not found"}
instance_id = getattr(record, "dingtalk_instance_id", "")
current_status = getattr(record, "status", "")
# If already completed, no need to check DingTalk
if current_status in ("approved", "rejected", "cancelled"):
return {
"success": True,
"status": current_status,
"approval_id": approval_id,
"instance_id": instance_id,
}
if not instance_id:
return {
"success": True,
"status": current_status,
"approval_id": approval_id,
"instance_id": "",
"message": "No DingTalk instance ID, cannot sync",
}
# Query DingTalk
client = get_dingtalk_client()
dt_result = client.get_approval_instance(instance_id)
if not dt_result["success"]:
return {
"success": False,
"message": f"DingTalk query failed: {dt_result.get('errmsg', '')}",
"status": current_status,
}
# Map DingTalk status to local status
dt_status = dt_result.get("status", "")
dt_result_val = dt_result.get("result", "")
new_status = current_status
if dt_status == "COMPLETED":
if dt_result_val == "agree":
new_status = "approved"
elif dt_result_val == "refuse":
new_status = "rejected"
elif dt_status == "TERMINATED":
new_status = "cancelled"
# Update local record if status changed
if new_status != current_status:
update_data = {"status": new_status}
if new_status in ("approved", "rejected", "cancelled"):
update_data["completed_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
await update_dd_approval({"id": approval_id, **update_data})
logger.info("Approval %s status synced: %s -> %s", approval_id, current_status, new_status)
return {
"success": True,
"status": new_status,
"approval_id": approval_id,
"instance_id": instance_id,
}
async def handle_dingtalk_callback(data):
"""
Process DingTalk webhook callback.
DingTalk sends callbacks when approval status changes.
Expected data format:
{
"processInstanceId": "xxx",
"processCode": "xxx",
"type": "bpms_instance_change",
"result": "agree" / "refuse",
"staffId": "xxx",
...
}
"""
logger.info("DingTalk callback received: %s", json.dumps(data, ensure_ascii=False))
instance_id = data.get("processInstanceId", "")
if not instance_id:
return {"success": False, "message": "Missing processInstanceId"}
callback_type = data.get("type", "")
if callback_type != "bpms_instance_change":
logger.info("Ignoring callback type: %s", callback_type)
return {"success": True, "message": f"Ignored callback type: {callback_type}"}
# Find local approval record by DingTalk instance ID
dbname = _get_dbname()
db = ServerEnv().db
async with db.sqlorContext(dbname) as sor:
rows = await sor.R("dd_approvals", {"dingtalk_instance_id": instance_id})
if not rows:
logger.warning("No local approval found for instance_id=%s", instance_id)
return {"success": False, "message": f"No approval found for instance {instance_id}"}
record = rows[0]
record_id = getattr(record, "id", "")
current_status = getattr(record, "status", "")
# Map callback to status
dt_result = data.get("result", "")
new_status = current_status
if dt_result == "agree":
new_status = "approved"
elif dt_result == "refuse":
new_status = "rejected"
elif callback_type == "terminate":
new_status = "cancelled"
# Update record
if new_status != current_status:
update_data = {
"id": record_id,
"status": new_status,
"comment": data.get("remark", ""),
}
if new_status in ("approved", "rejected", "cancelled"):
update_data["completed_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
await update_dd_approval(update_data)
logger.info("Callback: approval %s updated to %s", record_id, new_status)
# Notify entcms module about status change
biz_type = getattr(record, "biz_type", "")
biz_id = getattr(record, "biz_id", "")
if biz_type == "content_publish" and biz_id:
content_status = "published" if new_status == "approved" else "draft" if new_status == "rejected" else ""
if content_status:
content_update = {"id": biz_id, "status": content_status}
if content_status == "published":
content_update["published_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
async with db.sqlorContext(dbname) as sor:
await sor.U("cms_content", content_update)
logger.info("Callback: cms_content %s updated to %s", biz_id, content_status)
return {
"success": True,
"message": f"Approval {record_id} updated to {new_status}",
"approval_id": record_id,
"status": new_status,
}
# ─── Module Loader ─────────────────────────────────────────────────────────────
def load_dingdingflow():
"""Register all dingdingflow functions with ServerEnv."""
env = ServerEnv()
# CRUD functions for dd_approvals
env.create_dd_approval = create_dd_approval
env.update_dd_approval = update_dd_approval
env.delete_dd_approval = delete_dd_approval
env.get_dd_approval = get_dd_approval
env.list_dd_approvals = list_dd_approvals
# CRUD functions for dd_approval_configs
env.create_dd_approval_config = create_dd_approval_config
env.update_dd_approval_config = update_dd_approval_config
env.delete_dd_approval_config = delete_dd_approval_config
env.get_dd_approval_config = get_dd_approval_config
env.get_approval_config_by_type = get_approval_config_by_type
# Business logic functions
env.submit_approval = submit_approval
env.get_approval_status = get_approval_status
env.handle_dingtalk_callback = handle_dingtalk_callback
# DingTalk client accessor
env.get_dingtalk_client = get_dingtalk_client
logger.info("dingdingflow module loaded (v%s)", MODULE_VERSION)
return True

View File

@ -1,18 +0,0 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "dingdingflow"
version = "1.0.0"
description = "钉钉审批流程模块 - 内容发布审批工作流"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
"requests",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["dingdingflow*"]

View File

@ -1,8 +0,0 @@
"""
dingdingflow RBAC权限配置 已废弃
dingdingflow模块的wwwroot内容已移到应用根目录的 wwwroot/dingdingflow/
请使用:
cd ~/repos/cms && py3/bin/python init_any_permissions.py
cd ~/repos/cms && py3/bin/python init_superuser_permissions.py
"""

View File

@ -1,175 +0,0 @@
# 开元云科技官网系统架构
## 项目概述
企业官网 + CMS内容管理 + 钉钉审批流程系统基于Sage平台开发。
## 模块组成
### 1. entcms - 企业CMS系统
管理官网所有内容新闻、案例、产品、Banner、商机线索。
**数据库表 (entcms)**:
| 表名 | 用途 |
|------|------|
| cms_content | 统一内容表(新闻/案例/产品/Banner带发布审批状态流 |
| cms_categories | 内容分类支持层级按content_type分组 |
| cms_sections | 栏目管理 |
| cms_leads | 商机线索(网站访客提交 + 未来AI抽取 |
| cms_site_config | 站点配置Hero标语、页脚信息等KV配置 |
**目录结构**:
```
wwwroot/ # 统一前端目录
├── index.ui # 官网首页7个模块导航/Hero/产品/案例/新闻/页脚/浮动入口)
├── products.ui # 产品架构列表
├── news.ui # 新闻列表
├── news_detail.ui # 新闻详情
├── cases.ui # 案例列表
├── admin.ui # 管理后台仪表盘
├── api/*.dspy # CMS后端API
└── dingdingflow/ # 钉钉审批模块
├── index.ui
├── menu.ui
└── api/*.dspy
```
**公开页面 (any权限)**:
- `/index.ui` - 官网首页7个模块导航/Hero/产品/案例/新闻/页脚/浮动入口)
- `/products.ui` - 产品架构列表
- `/news.ui` - 新闻列表
- `/news_detail.ui` - 新闻详情
- `/cases.ui` - 案例列表
**管理页面 (logined权限)**:
- `/admin.ui` - 管理后台仪表盘
- `/cms_content_list` - 内容CRUD
- `/cms_categories_list` - 分类CRUD
- `/cms_sections_list` - 栏目CRUD
- `/cms_leads_list` - 线索CRUD
- `/cms_site_config_list` - 配置CRUD
**内容审批流程**:
编辑创建内容(草稿) → 提交审批(status=pending) → 钉钉审批 → 审批通过(status=approved) → 发布(status=published)
### 2. dingdingflow - 钉钉审批流程
对接钉钉审批API为CMS内容发布提供审批工作流。
**数据库表 (dingdingflow)**:
| 表名 | 用途 |
|------|------|
| dd_approvals | 审批记录关联业务类型和ID记录钉钉审批实例ID |
| dd_approval_configs | 审批流程配置按biz_type配置钉钉模板编码等 |
**环境变量**:
- `DINGTALK_APP_KEY` - 钉钉应用AppKey
- `DINGTALK_APP_SECRET` - 钉钉应用AppSecret
- `DINGTALK_AGENT_ID` - 钉钉应用AgentId
- `DINGTALK_CALLBACK_TOKEN` - 钉钉回调Token
**开发模式**: 缺少环境变量时自动使用mock响应不影响CMS功能。
## 技术栈
| 层 | 技术 |
|----|------|
| 前端 | bricks-framework (JSON UI) + 自定义CSS/JS |
| 后端 | ahserver + sqlor + apppublic |
| 认证 | rbac (角色权限控制) |
| 基础设施 | appbase (公共函数) |
| 审批 | 钉钉开放API (预留接口) |
| AI能力 | 预留Agent接口商机抽取 |
## 前端设计
### 官网视觉规范
- 风格: 极简科技感参考OpenAI官网
- 主色: #6C5CE7 (紫色)
- 渐变: #6C5CE7#A29BFE#74B9FF
- 暗色背景: #0a0a0a
- 卡片背景: #1A1A1A
- 字体: Noto Sans SC
- 最大宽度: 1100px
- 响应式断点: 768px
- 云宝形象: SVG线稿占位符
### 官网页面结构
1. **导航栏** - 固定顶部,毛玻璃效果
2. **Hero区** - 品牌Slogan + 脉冲呼吸灯 + 双按钮 + 云宝占位
3. **1+N+X产品架构** - 3张可展开卡片
4. **成功案例** - 3列网格 + CTA横幅
5. **企业动态** - 2条最新新闻 + 查看全部链接
6. **页脚** - 版权信息
7. **浮动入口** - 云宝头像 + 联系面板(表单提交线索)
## 目录结构
```
~/repos/cms/
├── entcms/ # CMS模块
│ ├── entcms/
│ │ ├── __init__.py
│ │ └── init.py # 模块初始化 + ServerEnv注册
│ ├── wwwroot/
│ │ ├── index.ui # 官网首页
│ │ ├── news.ui # 新闻列表
│ │ ├── news_detail.ui # 新闻详情
│ │ ├── cases.ui # 案例列表
│ │ ├── admin.ui # 管理后台
│ │ ├── menu.ui # 管理菜单
│ │ ├── cms_styles.css # 官网样式
│ │ ├── cms_scripts.js # 官网交互脚本
│ │ └── api/ # 22个.dspy API文件
│ ├── models/ # 4个表定义JSON
│ ├── json/ # 4个CRUD定义JSON
│ ├── init/data.json # 初始化数据
│ ├── scripts/load_path.py # RBAC权限配置
│ └── pyproject.toml
├── dingdingflow/ # 审批模块
│ ├── dingdingflow/
│ │ ├── __init__.py
│ │ ├── init.py
│ │ └── dingtalk_client.py # 钉钉API客户端
│ ├── wwwroot/
│ │ ├── index.ui
│ │ ├── menu.ui
│ │ └── api/ # 10个.dspy API文件
│ ├── models/ # 2个表定义JSON
│ ├── json/ # 2个CRUD定义JSON
│ ├── scripts/load_path.py
│ └── pyproject.toml
├── build.sh # 构建脚本
└── docs/ # 文档目录
```
## Sage集成步骤
### 1. app/sage.py
```python
from entcms.init import load_entcms
from dingdingflow.init import load_dingdingflow
# 在init()函数中:
load_entcms()
load_dingdingflow()
```
### 2. build.sh
```bash
for m in ... entcms dingdingflow
```
### 3. RBAC权限
```bash
cd ~/repos/sage
./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py
./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py
```
### 4. 数据库
```bash
cd ~/repos/cms/entcms && cat mysql.ddl.sql | mysql -u root -p sage
cd ~/repos/cms/dingdingflow && cat mysql.ddl.sql | mysql -u root -p sage
```
### 5. 重启
```bash
cd ~/repos/sage && ./stop.sh && ./start.sh
```

View File

@ -1,93 +0,0 @@
# 测试用例
## 一、entcms模块测试
### 1.1 数据库表验证
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T01 | cms_content表创建 | DDL执行成功 | ⬜ 待执行 |
| T02 | cms_categories表创建 | DDL执行成功 | ⬜ 待执行 |
| T03 | cms_leads表创建 | DDL执行成功 | ⬜ 待执行 |
| T04 | cms_site_config表创建 | DDL执行成功 | ⬜ 待执行 |
| T05 | 初始化数据导入 | 10条分类+5条配置写入成功 | ⬜ 待执行 |
### 1.2 CRUD API测试
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T06 | 创建新闻内容 | 返回Message成功 | ⬜ 待执行 |
| T07 | 创建产品内容 | 返回Message成功 | ⬜ 待执行 |
| T08 | 创建案例内容 | 返回Message成功 | ⬜ 待执行 |
| T09 | 查询内容列表 | 返回rows+total | ⬜ 待执行 |
| T10 | 按content_type筛选 | 只返回指定类型 | ⬜ 待执行 |
| T11 | 按status筛选 | 只返回指定状态 | ⬜ 待执行 |
| T12 | data_filter搜索 | LIKE/=操作符正常 | ⬜ 待执行 |
| T13 | 更新内容 | 字段更新成功 | ⬜ 待执行 |
| T14 | 删除内容 | 记录删除 | ⬜ 待执行 |
| T15 | 创建分类 | 返回成功 | ⬜ 待执行 |
| T16 | 分类下拉选项API | 返回value/text数组 | ⬜ 待执行 |
| T17 | 创建线索 | 返回成功 | ⬜ 待执行 |
| T18 | 线索列表 | 返回rows+total | ⬜ 待执行 |
| T19 | 更新线索状态 | 状态更新成功 | ⬜ 待执行 |
| T20 | 站点配置CRUD | 增删改查正常 | ⬜ 待执行 |
### 1.3 公开API测试
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T21 | 提交线索(无需登录) | 返回成功消息 | ⬜ 待执行 |
| T22 | 获取已发布内容 | 只返回status=published | ⬜ 待执行 |
| T23 | 获取最新新闻 | 按时间倒序,limit生效 | ⬜ 待执行 |
| T24 | 获取内容详情 | 返回单条完整数据 | ⬜ 待执行 |
| T25 | 获取站点配置 | 按group分组返回 | ⬜ 待执行 |
### 1.4 前端页面测试
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T26 | 首页加载 | 所有7个section渲染正常 | ⬜ 待执行 |
| T27 | Hero呼吸灯动画 | CSS动画正常运行 | ⬜ 待执行 |
| T28 | 产品卡片点击展开 | 点击展开/收起详情 | ⬜ 待执行 |
| T29 | 案例卡片hover效果 | 上移4px+边框变色 | ⬜ 待执行 |
| T30 | 浮动入口交互 | 悬停气泡+点击面板 | ⬜ 待执行 |
| T31 | 线索表单提交 | 数据写入cms_leads | ⬜ 待执行 |
| T32 | 导航锚点跳转 | 平滑滚动到目标section | ⬜ 待执行 |
| T33 | 新闻列表页 | 显示所有新闻 | ⬜ 待执行 |
| T34 | 新闻详情页 | 显示单条文章 | ⬜ 待执行 |
| T35 | 案例列表页 | 显示所有案例 | ⬜ 待执行 |
| T36 | 响应式-桌面端 | 3列grid,1100px最大宽度 | ⬜ 待执行 |
| T37 | 响应式-移动端 | 单列堆叠,32px标题 | ⬜ 待执行 |
| T38 | 滚动动画 | fade-in元素可见时出现 | ⬜ 待执行 |
### 1.5 RBAC权限测试
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T39 | 未登录访问首页 | 200正常显示 | ⬜ 待执行 |
| T40 | 未登录提交线索 | 200正常写入 | ⬜ 待执行 |
| T41 | 未登录访问管理页 | 401拒绝 | ⬜ 待执行 |
| T42 | 已登录访问管理页 | 200正常显示 | ⬜ 待执行 |
| T43 | 已登录CRUD操作 | 正常执行 | ⬜ 待执行 |
## 二、dingdingflow模块测试
### 2.1 审批流程测试
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T44 | 提交审批(dd_approvals写入) | 记录创建,status=pending | ⬜ 待执行 |
| T45 | 开发模式(无钉钉凭证) | mock响应,不影响流程 | ⬜ 待执行 |
| T46 | 获取审批状态 | 返回当前状态 | ⬜ 待执行 |
| T47 | 钉钉回调(审批通过) | 状态更新为approved | ⬜ 待执行 |
| T48 | 钉钉回调(审批拒绝) | 状态更新为rejected | ⬜ 待执行 |
| T49 | 审批配置CRUD | 增删改查正常 | ⬜ 待执行 |
### 2.2 集成测试
| # | 测试项 | 预期 | 状态 |
|---|--------|------|------|
| T50 | CMS提交审批→dingdingflow | 内容状态变pending,审批记录创建 | ⬜ 待执行 |
| T51 | 审批通过→CMS状态更新 | 内容状态变approved | ⬜ 待执行 |
| T52 | 审批拒绝→CMS状态不变 | 内容保持pending | ⬜ 待执行 |
| T53 | dingdingflow未安装→CMS降级 | CMS提示审批模块未安装 | ⬜ 待执行 |
## 测试汇总
- 总用例数: 53
- 通过: 0
- 失败: 0
- 待执行: 53
- 通过率: 待部署后统计

View File

@ -1,46 +0,0 @@
# 开发日志
## 2026-05-27 - 项目初始化与核心开发
### 范围
企业官网CMS系统 (entcms + dingdingflow) 从零搭建。
### 完成内容
**entcms模块**:
- 4个数据库表定义 (cms_content, cms_categories, cms_leads, cms_site_config)
- init.py 模块初始化 + 25个ServerEnv注册函数
- 4个CRUD JSON定义
- 22个.dspy API文件 (含公开API和data_filter支持)
- 4个公开页面 (index.ui, news.ui, news_detail.ui, cases.ui)
- 1个管理后台 (admin.ui)
- 1个菜单 (menu.ui)
- 完整营销站点CSS (cms_styles.css) + 交互JS (cms_scripts.js)
- RBAC权限配置脚本
- 初始化数据 (10条分类 + 5条站点配置)
**dingdingflow模块**:
- 2个数据库表定义 (dd_approvals, dd_approval_configs)
- init.py + dingtalk_client.py (钉钉API客户端)
- 2个CRUD JSON定义
- 10个.dspy API文件 (含公开回调endpoint)
- 管理UI (index.ui, menu.ui)
- RBAC权限配置脚本
- 开发模式: 无凭证时自动mock
**基础设施**:
- build.sh 构建脚本
- pyproject.toml x2
- 架构文档
- 53条测试用例
### 技术决策
1. 官网前端使用bricks框架 + Html widget渲染营销页面内容
2. 自定义CSS/JS实现营销设计暗色主题、渐变、动画
3. 统一cms_content表存储所有内容类型通过content_type区分
4. 钉钉API凭证从环境变量获取开发模式mock响应
5. 线索表预留raw_text字段用于未来AI商机抽取
### 当前状态
- 代码完整待部署到Sage进行集成测试
- 分支: main (首次提交)

View File

@ -1,22 +0,0 @@
# entcms - 企业CMS系统
管理开元云科技官网所有内容。
## 数据表
- cms_content: 内容(新闻/案例/产品/Banner
- cms_categories: 分类
- cms_leads: 商机线索
- cms_site_config: 站点配置
## 公开页面 (无需登录)
- /entcms/index.ui - 官网首页
- /entcms/news.ui - 新闻列表
- /entcms/news_detail.ui - 新闻详情
- /entcms/cases.ui - 案例列表
## 管理页面 (需登录)
- /entcms/admin.ui - 管理后台
- /entcms/cms_content_list - 内容管理
- /entcms/cms_categories_list - 分类管理
- /entcms/cms_leads_list - 线索管理
- /entcms/cms_site_config_list - 配置管理

View File

@ -1,328 +0,0 @@
"""
entcms - 企业CMS系统模块
企业官网内容管理新闻案例产品Banner商机线索
"""
import json
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
MODULE_NAME = "entcms"
MODULE_VERSION = "1.0.0"
DBNAME = "ocai_cms"
# ===== CMS Content CRUD =====
async def cms_content_list(ns=None):
"""查询内容列表"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = ns or {}
ns.setdefault('sort', 'sort_order asc, created_at desc')
rows = await sor.R('cms_content', ns)
total = len(rows)
return {'rows': rows, 'total': total}
async def cms_content_create(data):
"""创建内容"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
data['id'] = getID()
await sor.C('cms_content', data)
return data
async def cms_content_update(data):
"""更新内容"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.U('cms_content', data)
return data
async def cms_content_delete(data):
"""删除内容"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.D('cms_content', data)
return data
# ===== CMS Categories CRUD =====
async def cms_categories_list(ns=None):
"""查询分类列表"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = ns or {}
ns.setdefault('sort', 'sort_order asc')
rows = await sor.R('cms_categories', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_categories_create(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
data['id'] = getID()
await sor.C('cms_categories', data)
return data
async def cms_categories_update(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.U('cms_categories', data)
return data
async def cms_categories_delete(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.D('cms_categories', data)
return data
async def get_category_options(content_type=None):
"""获取分类下拉选项"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = {'sort': 'sort_order asc'}
if content_type:
ns['content_type'] = content_type
rows = await sor.R('cms_categories', ns)
options = [{'value': r['id'], 'text': r['name']} for r in rows]
return options
# ===== CMS Leads CRUD =====
async def cms_leads_list(ns=None):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = ns or {}
ns.setdefault('sort', 'created_at desc')
rows = await sor.R('cms_leads', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_leads_create(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
data['id'] = getID()
await sor.C('cms_leads', data)
return data
async def cms_leads_update(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.U('cms_leads', data)
return data
async def cms_leads_delete(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.D('cms_leads', data)
return data
async def submit_lead(data):
"""公开接口 - 网站访客提交线索"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
data['id'] = getID()
data.setdefault('status', 'new')
data.setdefault('source', 'website')
await sor.C('cms_leads', data)
return {'status': 'ok', 'id': data['id']}
# ===== CMS Sections CRUD =====
async def cms_sections_list(ns=None):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
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):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
data['id'] = getID()
await sor.C('cms_sections', data)
return data
async def cms_sections_update(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.U('cms_sections', data)
return data
async def cms_sections_delete(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.D('cms_sections', data)
return data
async def get_visible_sections():
"""获取所有可见栏目(公开接口)"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
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):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = ns or {}
ns.setdefault('sort', 'config_group asc, sort_order asc')
rows = await sor.R('cms_site_config', ns)
return {'rows': rows, 'total': len(rows)}
async def cms_site_config_create(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
data['id'] = getID()
await sor.C('cms_site_config', data)
return data
async def cms_site_config_update(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.U('cms_site_config', data)
return data
async def cms_site_config_delete(data):
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
await sor.D('cms_site_config', data)
return data
async def get_site_config(group=None):
"""获取站点配置(公开接口)"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = {'sort': 'sort_order asc'}
if group:
ns['config_group'] = group
rows = await sor.R('cms_site_config', ns)
result = {}
for r in rows:
g = r.get('config_group', '')
if g not in result:
result[g] = {}
result[g][r.get('config_key', '')] = r.get('config_value', '')
return result
# ===== Public Content APIs =====
async def get_published_content(content_type=None, limit=10):
"""获取已发布内容(公开接口)"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = {'status': 'published', 'sort': 'sort_order asc, published_at desc'}
if content_type:
ns['content_type'] = content_type
rows = await sor.R('cms_content', ns)
if limit:
rows = rows[:limit]
return rows
async def get_latest_news(limit=2):
"""获取最新新闻(公开接口)"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = {'status': 'published', 'content_type': 'news', 'sort': 'published_at desc'}
rows = await sor.R('cms_content', ns)
return rows[:limit]
async def get_content_detail(content_id):
"""获取内容详情(公开接口)"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
ns = {'id': content_id, 'status': 'published'}
rows = await sor.R('cms_content', ns)
return rows[0] if rows else None
# ===== Submit for approval =====
async def submit_content_for_approval(content_id, title, applicant_id):
"""提交内容审批调用dingdingflow"""
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
# 更新内容状态为pending
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
# 调用dingdingflow的submit_approval
try:
from dingdingflow.init import submit_approval
result = await submit_approval('content_publish', content_id, title, applicant_id)
# 保存审批ID
if result and result.get('approval_id'):
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
return result
except ImportError:
return {'status': 'error', 'message': 'dingdingflow模块未安装'}
def load_entcms():
"""注册所有函数到ServerEnv"""
env = ServerEnv()
# Content CRUD
env.cms_content_list = cms_content_list
env.cms_content_create = cms_content_create
env.cms_content_update = cms_content_update
env.cms_content_delete = cms_content_delete
# Categories CRUD
env.cms_categories_list = cms_categories_list
env.cms_categories_create = cms_categories_create
env.cms_categories_update = cms_categories_update
env.cms_categories_delete = cms_categories_delete
env.get_category_options = get_category_options
# Leads CRUD
env.cms_leads_list = cms_leads_list
env.cms_leads_create = cms_leads_create
env.cms_leads_update = cms_leads_update
env.cms_leads_delete = cms_leads_delete
env.submit_lead = submit_lead
# Site Config CRUD
env.cms_site_config_list = cms_site_config_list
env.cms_site_config_create = cms_site_config_create
env.cms_site_config_update = cms_site_config_update
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
env.get_content_detail = get_content_detail
env.submit_content_for_approval = submit_content_for_approval
return True

View File

@ -1,198 +0,0 @@
{
"cms_categories": [
{
"id": "cat_product_platform",
"org_id": "0",
"name": "AI平台",
"content_type": "product",
"sort_order": 1
},
{
"id": "cat_product_model",
"org_id": "0",
"name": "行业模型",
"content_type": "product",
"sort_order": 2
},
{
"id": "cat_product_agent",
"org_id": "0",
"name": "智能体",
"content_type": "product",
"sort_order": 3
},
{
"id": "cat_case_mfg",
"org_id": "0",
"name": "智能制造",
"content_type": "case",
"sort_order": 1
},
{
"id": "cat_case_finance",
"org_id": "0",
"name": "金融科技",
"content_type": "case",
"sort_order": 2
},
{
"id": "cat_case_healthcare",
"org_id": "0",
"name": "医疗健康",
"content_type": "case",
"sort_order": 3
},
{
"id": "cat_case_education",
"org_id": "0",
"name": "教育培训",
"content_type": "case",
"sort_order": 4
},
{
"id": "cat_news_company",
"org_id": "0",
"name": "公司动态",
"content_type": "news",
"sort_order": 1
},
{
"id": "cat_news_industry",
"org_id": "0",
"name": "行业资讯",
"content_type": "news",
"sort_order": 2
},
{
"id": "cat_news_product",
"org_id": "0",
"name": "产品更新",
"content_type": "news",
"sort_order": 3
}
],
"cms_site_config": [
{
"id": "cfg_hero_slogan",
"org_id": "0",
"config_group": "hero",
"config_key": "slogan",
"config_value": "一个平台,千行百业 智能跃迁",
"config_type": "text"
},
{
"id": "cfg_hero_subtitle",
"org_id": "0",
"config_group": "hero",
"config_key": "subtitle",
"config_value": "基于东数西算国家战略打造新一代AI智能体服务平台",
"config_type": "text"
},
{
"id": "cfg_hero_tag",
"org_id": "0",
"config_group": "hero",
"config_key": "tag_text",
"config_value": "AI 智能体服务平台",
"config_type": "text"
},
{
"id": "cfg_footer_copyright",
"org_id": "0",
"config_group": "footer",
"config_key": "copyright",
"config_value": "© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业",
"config_type": "text"
},
{
"id": "cfg_contact_company",
"org_id": "0",
"config_group": "contact",
"config_key": "company_name",
"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

@ -1,92 +0,0 @@
{
"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

@ -1,63 +0,0 @@
{
"tblname": "cms_site_config",
"alias": "cms_site_config_list",
"title": "站点配置",
"params": {
"sortby": [
"config_group asc",
"sort_order asc"
],
"logined_userorgid": "org_id",
"browserfields": {
"alters": {
"config_group": {
"uitype": "code",
"data": [
{
"value": "hero",
"text": "首屏Hero"
},
{
"value": "footer",
"text": "页脚"
},
{
"value": "contact",
"text": "联系信息"
},
{
"value": "seo",
"text": "SEO设置"
}
]
},
"config_type": {
"uitype": "code",
"data": [
{
"value": "text",
"text": "文本"
},
{
"value": "image",
"text": "图片"
},
{
"value": "html",
"text": "HTML"
},
{
"value": "json",
"text": "JSON"
}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('api/cms_site_config_create.dspy')}}",
"update_data_url": "{{entire_url('api/cms_site_config_update.dspy')}}",
"delete_data_url": "{{entire_url('api/cms_site_config_delete.dspy')}}"
}
}
}

View File

@ -1,17 +0,0 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "entcms"
version = "1.0.0"
description = "企业CMS系统 - 开元云科技官网内容管理"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["entcms*"]

View File

@ -1,8 +0,0 @@
"""
entcms RBAC权限配置 已废弃
entcms模块的wwwroot内容已移到应用根目录的 wwwroot/
请使用:
cd ~/repos/cms && py3/bin/python init_any_permissions.py
cd ~/repos/cms && py3/bin/python init_superuser_permissions.py
"""

295
init/data.yaml Normal file
View File

@ -0,0 +1,295 @@
appcodes:
- id: content_type
name: 内容类型
hierarchy_flg: 0
- id: content_status
name: 内容状态
hierarchy_flg: 0
- id: lead_status
name: 线索状态
hierarchy_flg: 0
- id: lead_source
name: 线索来源
hierarchy_flg: 0
- id: config_type
name: 配置类型
hierarchy_flg: 0
- id: approval_status
name: 审批状态
hierarchy_flg: 0
appcodes_kv:
# content_type
- id: content_type_news
parentid: content_type
k: news
v: 新闻
- id: content_type_case
parentid: content_type
k: case
v: 案例
- id: content_type_product
parentid: content_type
k: product
v: 产品
- id: content_type_banner
parentid: content_type
k: banner
v: Banner
# content_status
- id: content_status_draft
parentid: content_status
k: draft
v: 草稿
- id: content_status_pending
parentid: content_status
k: pending
v: 待审批
- id: content_status_approved
parentid: content_status
k: approved
v: 已审批
- id: content_status_published
parentid: content_status
k: published
v: 已发布
- id: content_status_rejected
parentid: content_status
k: rejected
v: 已拒绝
# lead_status
- id: lead_status_new
parentid: lead_status
k: new
v: 新线索
- id: lead_status_contacted
parentid: lead_status
k: contacted
v: 已联系
- id: lead_status_qualified
parentid: lead_status
k: qualified
v: 已确认
- id: lead_status_converted
parentid: lead_status
k: converted
v: 已转化
- id: lead_status_closed
parentid: lead_status
k: closed
v: 已关闭
# lead_source
- id: lead_source_website
parentid: lead_source
k: website
v: 官网
- id: lead_source_referral
parentid: lead_source
k: referral
v: 转介绍
- id: lead_source_event
parentid: lead_source
k: event
v: 活动
- id: lead_source_ai_extract
parentid: lead_source
k: ai_extract
v: AI抽取
# config_type
- id: config_type_text
parentid: config_type
k: text
v: 文本
- id: config_type_html
parentid: config_type
k: html
v: HTML
- id: config_type_json
parentid: config_type
k: json
v: JSON
- id: config_type_image
parentid: config_type
k: image
v: 图片
# approval_status
- id: approval_status_pending
parentid: approval_status
k: pending
v: 待审批
- id: approval_status_approved
parentid: approval_status
k: approved
v: 已通过
- id: approval_status_rejected
parentid: approval_status
k: rejected
v: 已拒绝
- id: approval_status_cancelled
parentid: approval_status
k: cancelled
v: 已取消
cms_categories:
- id: cat_product_platform
org_id: "0"
name: AI平台
content_type: product
sort_order: 1
- id: cat_product_model
org_id: "0"
name: 行业模型
content_type: product
sort_order: 2
- id: cat_product_agent
org_id: "0"
name: 智能体
content_type: product
sort_order: 3
- id: cat_case_mfg
org_id: "0"
name: 智能制造
content_type: case
sort_order: 1
- id: cat_case_finance
org_id: "0"
name: 金融科技
content_type: case
sort_order: 2
- id: cat_case_healthcare
org_id: "0"
name: 医疗健康
content_type: case
sort_order: 3
- id: cat_case_education
org_id: "0"
name: 教育培训
content_type: case
sort_order: 4
- id: cat_news_company
org_id: "0"
name: 公司动态
content_type: news
sort_order: 1
- id: cat_news_industry
org_id: "0"
name: 行业资讯
content_type: news
sort_order: 2
- id: cat_news_product
org_id: "0"
name: 产品更新
content_type: news
sort_order: 3
cms_site_config:
- id: cfg_hero_slogan
org_id: "0"
config_group: hero
config_key: slogan
config_value: 一个平台,千行百业 智能跃迁
config_type: text
- id: cfg_hero_subtitle
org_id: "0"
config_group: hero
config_key: subtitle
config_value: 基于东数西算国家战略打造新一代AI智能体服务平台
config_type: text
- id: cfg_hero_tag
org_id: "0"
config_group: hero
config_key: tag_text
config_value: AI 智能体服务平台
config_type: text
- id: cfg_footer_copyright
org_id: "0"
config_group: footer
config_key: copyright
config_value: "© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业"
config_type: text
- id: cfg_contact_company
org_id: "0"
config_group: contact
config_key: company_name
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 智能体服务平台", "slogan": "一个平台,千行百业 智能跃迁", "subtitle": "基于东数西算国家战略打造新一代AI智能体服务平台", "btn_primary": "联系销售", "btn_secondary": "了解产品架构"}'
- 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"}'
- 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"}'
- 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"}'
- 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)"}'
- 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"}'
dd_approval_configs:
- id: apvcfg_content_publish
org_id: "0"
biz_type: content_publish
biz_type_title: 内容发布审批
process_code: ""
agent_id: ""
form_config: '[{"name": "审批类型", "value": "内容发布"}]'
is_active: "1"

View File

@ -1,158 +0,0 @@
"""
CMS RBAC权限初始化 any (匿名) 角色
自动扫描 wwwroot bricks 下所有文件授予 any 角色权限
规则:
- wwwroot/* /<file>
- wwwroot/dingdingflow/* /dingdingflow/<file>
- bricks/* /bricks/<file>
- 排除: .pyc, __pycache__, .git, 指向其他模块的符号链接
用法: cd ~/repos/cms && py3/bin/python init_any_permissions.py
"""
import os, sys, subprocess, re
def find_app_root():
return os.path.dirname(os.path.abspath(__file__))
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:
print("ERROR: 找不到Sage无法初始化权限")
sys.exit(1)
py = os.path.join(sage_root, "py3", "bin", "python")
sp = os.path.join(sage_root, "set_role_perm.py")
if not os.path.exists(sp):
print("ERROR: 找不到set_role_perm.py")
sys.exit(1)
SKIP_DIRS = {".git", "__pycache__", "node_modules", ".svn"}
SKIP_EXTS = {".pyc", ".pyo", ".swp", ".swo", ".bak", ".orig", ".log", ".pid", ".lock"}
def scan_wwwroot(wwwroot_dir, url_prefix):
"""扫描wwwroot目录下所有文件返回URL路径列表"""
paths = []
if not os.path.isdir(wwwroot_dir):
return paths
for root, dirs, files in os.walk(wwwroot_dir):
# 排除不需要扫描的目录
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for f in sorted(files):
# 跳过不需要的文件
_, ext = os.path.splitext(f)
if ext.lower() in SKIP_EXTS:
continue
if f.startswith("."):
continue
# 计算相对路径
rel_path = os.path.relpath(os.path.join(root, f), wwwroot_dir)
# 检查是否是符号链接指向其他模块
full_path = os.path.join(root, f)
if os.path.islink(full_path):
link_target = os.path.realpath(full_path)
# 如果链接目标不在CMS目录下跳过
if not link_target.startswith(app_root):
print(f" SKIP (外部链接): {rel_path} -> {link_target}")
continue
url = url_prefix + "/" + rel_path.replace(os.sep, "/")
paths.append(url)
return paths
def scan_bricks(bricks_dir):
"""扫描bricks目录下所有文件返回URL路径列表"""
paths = []
if not os.path.isdir(bricks_dir):
return paths
# 检查bricks是否为符号链接
real_bricks = os.path.realpath(bricks_dir)
for root, dirs, files in os.walk(bricks_dir):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for f in sorted(files):
_, ext = os.path.splitext(f)
if ext.lower() in SKIP_EXTS:
continue
if f.startswith("."):
continue
full_path = os.path.join(root, f)
# 跳过指向其他模块的符号链接
if os.path.islink(full_path):
link_target = os.path.realpath(full_path)
if not link_target.startswith(real_bricks) and not link_target.startswith(app_root):
continue
rel_path = os.path.relpath(full_path, bricks_dir)
url = "/bricks/" + rel_path.replace(os.sep, "/")
paths.append(url)
return paths
def set_any_perms(paths):
"""为路径列表设置any权限"""
count = 0
env = os.environ.copy()
env['SAGE_RBAC_DB'] = 'ocai_cms'
for p in paths:
result = subprocess.run(
[py, sp, "any", p],
cwd=sage_root,
capture_output=True,
text=True,
env=env
)
status = "" if result.returncode == 0 else ""
print(f" {status} any {p}")
count += 1
return count
print("=== CMS RBAC权限初始化 — any (匿名访问) ===")
print(f"Sage: {sage_root}")
print()
# 1. wwwroot/ 根目录文件 → / (排除dingdingflow子目录)
wwwroot_root = os.path.join(app_root, "wwwroot")
root_paths = []
if os.path.isdir(wwwroot_root):
for f in sorted(os.listdir(wwwroot_root)):
fpath = os.path.join(wwwroot_root, f)
if os.path.isfile(fpath) and not f.startswith("."):
_, ext = os.path.splitext(f)
if ext.lower() not in SKIP_EXTS:
root_paths.append("/" + f)
# api 子目录
api_dir = os.path.join(wwwroot_root, "api")
if os.path.isdir(api_dir):
for f in sorted(os.listdir(api_dir)):
fpath = os.path.join(api_dir, f)
if os.path.isfile(fpath) and not f.startswith("."):
_, ext = os.path.splitext(f)
if ext.lower() not in SKIP_EXTS:
root_paths.append("/api/" + f)
print(f"--- wwwroot/ → / ({len(root_paths)} 个文件) ---")
# 确保根路径 / 也有权限访问根路径时RBAC检查的是 '/' 而非 index.ui
root_paths.append("/")
n1 = set_any_perms(root_paths)
# 2. wwwroot/dingdingflow/ → /dingdingflow/
dd_paths = scan_wwwroot(
os.path.join(app_root, "wwwroot", "dingdingflow"),
"/dingdingflow"
)
print(f"\n--- wwwroot/dingdingflow/ → /dingdingflow ({len(dd_paths)} 个文件) ---")
n2 = set_any_perms(dd_paths)
# 3. bricks → /bricks
bricks_dir = os.path.join(app_root, "bricks")
bricks_paths = scan_bricks(bricks_dir)
if bricks_paths:
print(f"\n--- bricks → /bricks ({len(bricks_paths)} 个文件) ---")
n3 = set_any_perms(bricks_paths)
else:
n3 = 0
print(f"\n--- bricks → /bricks (目录不存在或未构建,跳过) ---")
total = n1 + n2 + n3
print(f"\n=== 完成: 共设置 {total} 个any权限 ===")

View File

@ -1,147 +0,0 @@
"""
CMS RBAC权限初始化 superuser角色
为owner.superuser授予CMS所有权限
用法: cd ~/repos/cms && py3/bin/python init_superuser_permissions.py
"""
import os, sys, subprocess
def find_app_root():
"""查找CMS应用根目录"""
script_dir = os.path.dirname(os.path.abspath(__file__))
return script_dir
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:
sage_root = app_root
py = os.path.join(sage_root, "py3", "bin", "python")
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):
assert sp is not None, "set_role_perm.py not found"
env = os.environ.copy()
env['SAGE_RBAC_DB'] = 'ocai_cms'
for p in paths:
print(f" {role:30s} {p}")
subprocess.run([py, sp, role, p], cwd=sage_root, capture_output=True, env=env)
# ─── superuser — 所有权限 ───
superuser_paths = [
# entcms 公开页面
"/index.ui",
"/news.ui",
"/news_detail.ui",
"/cases.ui",
"/products.ui",
"/cms_styles.css",
"/cms_scripts.js",
"/menu.ui",
"/admin.ui",
# entcms 内容管理
"/cms_content_list", "/cms_content_list/%",
"/api/cms_content_create.dspy",
"/api/cms_content_update.dspy",
"/api/cms_content_delete.dspy",
"/api/cms_content_list.dspy",
"/api/submit_content_approval.dspy",
# entcms 分类管理
"/cms_categories_list", "/cms_categories_list/%",
"/api/cms_categories_create.dspy",
"/api/cms_categories_update.dspy",
"/api/cms_categories_delete.dspy",
"/api/cms_categories_list.dspy",
"/api/category_options.dspy",
# entcms 栏目管理
"/cms_sections_list", "/cms_sections_list/%",
"/api/cms_sections_create.dspy",
"/api/cms_sections_update.dspy",
"/api/cms_sections_delete.dspy",
"/api/cms_sections_list.dspy",
# entcms 站点配置
"/cms_site_config_list", "/cms_site_config_list/%",
"/api/cms_site_config_create.dspy",
"/api/cms_site_config_update.dspy",
"/api/cms_site_config_delete.dspy",
"/api/cms_site_config_list.dspy",
# entcms 线索管理
"/cms_leads_list", "/cms_leads_list/%",
"/api/cms_leads_create.dspy",
"/api/cms_leads_update.dspy",
"/api/cms_leads_delete.dspy",
"/api/cms_leads_list.dspy",
# entcms 其他API
"/api/submit_lead.dspy",
"/api/get_config.dspy",
"/api/get_published_content.dspy",
"/api/get_content_detail.dspy",
"/api/get_sections.dspy",
# dingdingflow
"/dingdingflow",
"/dingdingflow/index.ui",
"/dingdingflow/menu.ui",
"/dingdingflow/api/dingtalk_callback.dspy",
"/dingdingflow/api/submit_approval.dspy",
# dingdingflow 审批配置
"/dingdingflow/dd_approval_configs", "/dingdingflow/dd_approval_configs/%",
"/dingdingflow/api/dd_approval_configs_create.dspy",
"/dingdingflow/api/dd_approval_configs_update.dspy",
"/dingdingflow/api/dd_approval_configs_delete.dspy",
"/dingdingflow/api/dd_approval_configs_list.dspy",
# dingdingflow 审批单
"/dingdingflow/dd_approvals", "/dingdingflow/dd_approvals/%",
"/dingdingflow/api/dd_approvals_create.dspy",
"/dingdingflow/api/dd_approvals_update.dspy",
"/dingdingflow/api/dd_approvals_delete.dspy",
"/dingdingflow/api/dd_approvals_list.dspy",
# appbase 系统基础模块
"/appbase/appcodes_kv",
"/appbase/appcodes_kv/get_appcodes_kv.dspy",
"/appbase/appcodes_kv/add_appcodes_kv.dspy",
"/appbase/appcodes_kv/update_appcodes_kv.dspy",
"/appbase/appcodes_kv/index.ui",
"/appbase/appcodes_kv/delete_appcodes_kv.dspy",
"/appbase/cron/index.ui",
"/appbase/appcodes",
"/appbase/appcodes/get_appcodes.dspy",
"/appbase/appcodes/add_appcodes.dspy",
"/appbase/appcodes/index.ui",
"/appbase/appcodes/update_appcodes.dspy",
"/appbase/appcodes/delete_appcodes.dspy",
"/appbase/params",
"/appbase/params/update_params.dspy",
"/appbase/params/get_params.dspy",
"/appbase/params/index.ui",
"/appbase/params/add_params.dspy",
"/appbase/params/delete_params.dspy",
"/appbase/svgicon",
"/appbase/svgicon/get_svgicon.dspy",
"/appbase/svgicon/delete_svgicon.dspy",
"/appbase/svgicon/add_svgicon.dspy",
"/appbase/svgicon/update_svgicon.dspy",
"/appbase/svgicon/index.ui",
]
print("=== CMS RBAC权限初始化 — superuser ===")
print(f"\\n--- owner.superuser (超级管理员) ---")
run("owner.superuser", superuser_paths)
print("\\n完成")

View File

@ -12,18 +12,9 @@
"content_type": {
"uitype": "code",
"data": [
{
"value": "product",
"text": "产品"
},
{
"value": "case",
"text": "案例"
},
{
"value": "news",
"text": "新闻"
}
{"value": "product", "text": "产品"},
{"value": "case", "text": "案例"},
{"value": "news", "text": "新闻"}
]
}
}
@ -34,4 +25,4 @@
"delete_data_url": "{{entire_url('api/cms_categories_delete.dspy')}}"
}
}
}
}

View File

@ -36,47 +36,20 @@
"content_type": {
"uitype": "code",
"data": [
{
"value": "banner",
"text": "Banner"
},
{
"value": "product",
"text": "产品"
},
{
"value": "case",
"text": "案例"
},
{
"value": "news",
"text": "新闻"
}
{"value": "banner", "text": "Banner"},
{"value": "product", "text": "产品"},
{"value": "case", "text": "案例"},
{"value": "news", "text": "新闻"}
]
},
"status": {
"uitype": "code",
"data": [
{
"value": "draft",
"text": "草稿"
},
{
"value": "pending",
"text": "待审批"
},
{
"value": "approved",
"text": "已审批"
},
{
"value": "published",
"text": "已发布"
},
{
"value": "archived",
"text": "已归档"
}
{"value": "draft", "text": "草稿"},
{"value": "pending", "text": "待审批"},
{"value": "approved", "text": "已审批"},
{"value": "published", "text": "已发布"},
{"value": "archived", "text": "已归档"}
]
},
"category_id": {
@ -94,4 +67,4 @@
"delete_data_url": "{{entire_url('api/cms_content_delete.dspy')}}"
}
}
}
}

View File

@ -39,47 +39,20 @@
"status": {
"uitype": "code",
"data": [
{
"value": "new",
"text": "新线索"
},
{
"value": "contacted",
"text": "已联系"
},
{
"value": "qualified",
"text": "已确认"
},
{
"value": "converted",
"text": "已转化"
},
{
"value": "closed",
"text": "已关闭"
}
{"value": "new", "text": "新线索"},
{"value": "contacted", "text": "已联系"},
{"value": "qualified", "text": "已确认"},
{"value": "converted", "text": "已转化"},
{"value": "closed", "text": "已关闭"}
]
},
"source": {
"uitype": "code",
"data": [
{
"value": "website",
"text": "官网"
},
{
"value": "phone",
"text": "电话"
},
{
"value": "referral",
"text": "转介绍"
},
{
"value": "ai_extract",
"text": "AI抽取"
}
{"value": "website", "text": "官网"},
{"value": "phone", "text": "电话"},
{"value": "referral", "text": "转介绍"},
{"value": "ai_extract", "text": "AI抽取"}
]
}
}
@ -90,4 +63,4 @@
"delete_data_url": "{{entire_url('api/cms_leads_delete.dspy')}}"
}
}
}
}

View File

@ -0,0 +1,53 @@
{
"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

@ -0,0 +1,39 @@
{
"tblname": "cms_site_config",
"alias": "cms_site_config_list",
"title": "站点配置",
"params": {
"sortby": [
"config_group asc",
"sort_order asc"
],
"logined_userorgid": "org_id",
"browserfields": {
"alters": {
"config_group": {
"uitype": "code",
"data": [
{"value": "hero", "text": "首屏Hero"},
{"value": "footer", "text": "页脚"},
{"value": "contact", "text": "联系信息"},
{"value": "seo", "text": "SEO设置"}
]
},
"config_type": {
"uitype": "code",
"data": [
{"value": "text", "text": "文本"},
{"value": "image", "text": "图片"},
{"value": "html", "text": "HTML"},
{"value": "json", "text": "JSON"}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('api/cms_site_config_create.dspy')}}",
"update_data_url": "{{entire_url('api/cms_site_config_update.dspy')}}",
"delete_data_url": "{{entire_url('api/cms_site_config_delete.dspy')}}"
}
}
}

View File

@ -17,9 +17,9 @@
}
},
"editable": {
"new_data_url": "{{entire_url('../api/dd_approval_configs_create.dspy')}}",
"update_data_url": "{{entire_url('../api/dd_approval_configs_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/dd_approval_configs_delete.dspy')}}"
"new_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_create.dspy')}}",
"update_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_delete.dspy')}}"
}
}
}

View File

@ -34,9 +34,9 @@
}
},
"editable": {
"new_data_url": "{{entire_url('../api/dd_approvals_create.dspy')}}",
"update_data_url": "{{entire_url('../api/dd_approvals_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/dd_approvals_delete.dspy')}}"
"new_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_create.dspy')}}",
"update_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_delete.dspy')}}"
}
}
}

View File

@ -151,4 +151,4 @@
"textfield": "name"
}
]
}
}

View File

@ -134,4 +134,4 @@
]
}
]
}
}

View File

@ -113,4 +113,4 @@
}
],
"codes": []
}
}

View File

@ -111,4 +111,4 @@
]
}
]
}
}

View File

@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
[project]
name = "kaiyuan-cms"
version = "1.0.0"
description = "开元云科技企业官网CMS系统 — 独立Web应用"
version = "2.0.0"
description = "CMS内容管理模块 - 企业官网内容管理与钉钉审批工作流"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
"requests",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["entcms*", "dingdingflow*"]
include = ["cms*"]

View File

@ -1,126 +0,0 @@
"""
初始化超级用户
用法:
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
"""
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 appPublic.jsonConfig import getConfig
from appPublic.uniqueID import getID
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']
# 分配角色
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("可能需要手动分配角色")
# 给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" 用户名: {username}")
print(f" 密码: {password}")
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

112
scripts/load_path.py Normal file
View File

@ -0,0 +1,112 @@
"""
cms 模块 RBAC 权限配置
CMS管理后台的CRUD页面和API挂载在 /cms/ 路径下
用法:
cd ~/repos/portal
py3/bin/python ~/repos/cms/scripts/load_path.py
"""
import subprocess, os, sys
def find_portal_root():
candidates = [
os.path.expanduser("~/repos/portal"),
os.path.expanduser("~/portal"),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")):
return c
# fallback: sage
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
if os.path.isdir(os.path.join(c, "py3")):
return c
return None
PORTAL_ROOT = find_portal_root()
if not PORTAL_ROOT:
print("ERROR: Cannot find Portal or Sage root")
sys.exit(1)
PYTHON = os.path.join(PORTAL_ROOT, "py3", "bin", "python")
SET_PERM = os.path.join(PORTAL_ROOT, "set_role_perm.py")
if not os.path.exists(SET_PERM):
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
sp = os.path.join(c, "set_role_perm.py")
if os.path.exists(sp):
SET_PERM = sp
break
MOD = "cms"
env = os.environ.copy()
env['SAGE_RBAC_DB'] = 'ocai_cms'
# CMS管理后台所有路径
PATHS = [
f"/{MOD}",
f"/{MOD}/admin.ui",
f"/{MOD}/menu.ui",
# Content
f"/{MOD}/cms_content_list", f"/{MOD}/cms_content_list/%",
f"/{MOD}/api/cms_content_create.dspy",
f"/{MOD}/api/cms_content_update.dspy",
f"/{MOD}/api/cms_content_delete.dspy",
f"/{MOD}/api/cms_content_list.dspy",
f"/{MOD}/api/submit_content_approval.dspy",
# Categories
f"/{MOD}/cms_categories_list", f"/{MOD}/cms_categories_list/%",
f"/{MOD}/api/cms_categories_create.dspy",
f"/{MOD}/api/cms_categories_update.dspy",
f"/{MOD}/api/cms_categories_delete.dspy",
f"/{MOD}/api/cms_categories_list.dspy",
f"/{MOD}/api/category_options.dspy",
# Sections
f"/{MOD}/cms_sections_list", f"/{MOD}/cms_sections_list/%",
f"/{MOD}/api/cms_sections_create.dspy",
f"/{MOD}/api/cms_sections_update.dspy",
f"/{MOD}/api/cms_sections_delete.dspy",
f"/{MOD}/api/cms_sections_list.dspy",
# Leads
f"/{MOD}/cms_leads_list", f"/{MOD}/cms_leads_list/%",
f"/{MOD}/api/cms_leads_create.dspy",
f"/{MOD}/api/cms_leads_update.dspy",
f"/{MOD}/api/cms_leads_delete.dspy",
f"/{MOD}/api/cms_leads_list.dspy",
# Site Config
f"/{MOD}/cms_site_config_list", f"/{MOD}/cms_site_config_list/%",
f"/{MOD}/api/cms_site_config_create.dspy",
f"/{MOD}/api/cms_site_config_update.dspy",
f"/{MOD}/api/cms_site_config_delete.dspy",
f"/{MOD}/api/cms_site_config_list.dspy",
# DD Approvals
f"/{MOD}/dd_approvals", f"/{MOD}/dd_approvals/%",
f"/{MOD}/api/dd_approvals_create.dspy",
f"/{MOD}/api/dd_approvals_update.dspy",
f"/{MOD}/api/dd_approvals_delete.dspy",
f"/{MOD}/api/dd_approvals_list.dspy",
# DD Approval Configs
f"/{MOD}/dd_approval_configs", f"/{MOD}/dd_approval_configs/%",
f"/{MOD}/api/dd_approval_configs_create.dspy",
f"/{MOD}/api/dd_approval_configs_update.dspy",
f"/{MOD}/api/dd_approval_configs_delete.dspy",
f"/{MOD}/api/dd_approval_configs_list.dspy",
# DingTalk
f"/{MOD}/api/submit_approval.dspy",
f"/{MOD}/api/dingtalk_callback.dspy",
]
def run(role, paths):
count = 0
for p in paths:
result = subprocess.run(
[PYTHON, SET_PERM, role, p],
cwd=PORTAL_ROOT, capture_output=True, text=True, env=env
)
status = "" if result.returncode == 0 else ""
print(f" {status} {role:20s} {p}")
count += 1
return count
print(f"=== CMS模块 RBAC权限初始化 ===")
print(f"Portal: {PORTAL_ROOT}")
n = run("owner.superuser", PATHS)
print(f"\n完成: {n} 个权限已设置")

View File

@ -1,11 +0,0 @@
#!/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
View File

@ -1,22 +0,0 @@
#!/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

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
content_type = params_kw.get('content_type', None)

View File

@ -1,34 +1,14 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': getID()}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('name', None)
if v is not None:
data['name'] = v
v = params_kw.get('parent_id', None)
if v is not None:
data['parent_id'] = v
v = params_kw.get('content_type', None)
if v is not None:
data['content_type'] = v
v = params_kw.get('description', None)
if v is not None:
data['description'] = v
v = params_kw.get('sort_order', None)
if v is not None:
data['sort_order'] = v
for field in ['org_id', 'name', 'parent_id', 'content_type', 'description', 'sort_order', 'display_config']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.C('cms_categories', data)
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
_id = params_kw.get('id', '')

View File

@ -1,30 +1,9 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
ns = {'sort': 'sort_order asc'}
# data_filter support
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
# Manual filter params
_content_type = params_kw.get('content_type', None)
if _content_type:
ns['content_type'] = _content_type
rows = await sor.R('cms_categories', ns)
total = len(rows)
return {'status': 'ok', 'rows': rows, 'total': total}
return {'status': 'ok', 'rows': rows, 'total': len(rows)}

View File

@ -1,36 +1,17 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': params_kw.get('id', '')}
if not data['id']:
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('name', None)
if v is not None:
data['name'] = v
v = params_kw.get('parent_id', None)
if v is not None:
data['parent_id'] = v
v = params_kw.get('content_type', None)
if v is not None:
data['content_type'] = v
v = params_kw.get('description', None)
if v is not None:
data['description'] = v
v = params_kw.get('sort_order', None)
if v is not None:
data['sort_order'] = v
for field in ['org_id', 'name', 'parent_id', 'content_type', 'description', 'sort_order', 'display_config']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.U('cms_categories', data)
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': getID()}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
_id = params_kw.get('id', '')

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
ns = {'sort': 'sort_order asc, created_at desc'}
@ -20,7 +20,6 @@ async with db.sqlorContext(dbname) as sor:
pass
# Manual filter params
_content_type = params_kw.get('content_type', None)
if _content_type:
ns['content_type'] = _content_type

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': params_kw.get('id', '')}

View File

@ -1,66 +1,16 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': getID()}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('source', None)
if v is not None:
data['source'] = v
v = params_kw.get('name', None)
if v is not None:
data['name'] = v
v = params_kw.get('company', None)
if v is not None:
data['company'] = v
v = params_kw.get('phone', None)
if v is not None:
data['phone'] = v
v = params_kw.get('email', None)
if v is not None:
data['email'] = v
v = params_kw.get('industry', None)
if v is not None:
data['industry'] = v
v = params_kw.get('region', None)
if v is not None:
data['region'] = v
v = params_kw.get('interest_products', None)
if v is not None:
data['interest_products'] = v
v = params_kw.get('message', None)
if v is not None:
data['message'] = v
v = params_kw.get('raw_text', None)
if v is not None:
data['raw_text'] = v
v = params_kw.get('status', None)
if v is not None:
data['status'] = v
v = params_kw.get('assigned_to', None)
if v is not None:
data['assigned_to'] = v
v = params_kw.get('notes', None)
if v is not None:
data['notes'] = v
for field in ['org_id', 'source', 'name', 'company', 'phone', 'email',
'industry', 'region', 'interest_products', 'message',
'status', 'assigned_to', 'notes']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.C('cms_leads', data)
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
_id = params_kw.get('id', '')

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
ns = {'sort': 'created_at desc'}
@ -19,16 +19,5 @@ async with db.sqlorContext(dbname) as sor:
except Exception:
pass
# Manual filter params
_status = params_kw.get('status', None)
if _status:
ns['status'] = _status
_source = params_kw.get('source', None)
if _source:
ns['source'] = _source
rows = await sor.R('cms_leads', ns)
total = len(rows)
return {'status': 'ok', 'rows': rows, 'total': total}
return {'status': 'ok', 'rows': rows, 'total': len(rows)}

View File

@ -1,68 +1,19 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': params_kw.get('id', '')}
if not data['id']:
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('source', None)
if v is not None:
data['source'] = v
v = params_kw.get('name', None)
if v is not None:
data['name'] = v
v = params_kw.get('company', None)
if v is not None:
data['company'] = v
v = params_kw.get('phone', None)
if v is not None:
data['phone'] = v
v = params_kw.get('email', None)
if v is not None:
data['email'] = v
v = params_kw.get('industry', None)
if v is not None:
data['industry'] = v
v = params_kw.get('region', None)
if v is not None:
data['region'] = v
v = params_kw.get('interest_products', None)
if v is not None:
data['interest_products'] = v
v = params_kw.get('message', None)
if v is not None:
data['message'] = v
v = params_kw.get('raw_text', None)
if v is not None:
data['raw_text'] = v
v = params_kw.get('status', None)
if v is not None:
data['status'] = v
v = params_kw.get('assigned_to', None)
if v is not None:
data['assigned_to'] = v
v = params_kw.get('notes', None)
if v is not None:
data['notes'] = v
for field in ['org_id', 'source', 'name', 'company', 'phone', 'email',
'industry', 'region', 'interest_products', 'message',
'status', 'assigned_to', 'notes']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.U('cms_leads', data)
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}

View File

@ -1,42 +1,15 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
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
for field in ['org_id', 'section_key', 'title', 'subtitle', 'section_type', 'content_type',
'sort_order', 'is_visible', 'display_config', 'style_config', 'static_content']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.C('cms_sections', data)
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
_id = params_kw.get('id', '')

View File

@ -1,20 +1,9 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
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)
return {'status': 'ok', 'rows': rows, 'total': len(rows)}

View File

@ -1,44 +1,18 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': params_kw.get('id', '')}
if not data['id']:
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
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
for field in ['org_id', 'section_key', 'title', 'subtitle', 'section_type', 'content_type',
'sort_order', 'is_visible', 'display_config', 'style_config', 'static_content']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.U('cms_sections', data)
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}

View File

@ -1,34 +1,14 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': getID()}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('config_group', None)
if v is not None:
data['config_group'] = v
v = params_kw.get('config_key', None)
if v is not None:
data['config_key'] = v
v = params_kw.get('config_value', None)
if v is not None:
data['config_value'] = v
v = params_kw.get('config_type', None)
if v is not None:
data['config_type'] = v
v = params_kw.get('sort_order', None)
if v is not None:
data['sort_order'] = v
for field in ['org_id', 'config_group', 'config_key', 'config_value', 'config_type', 'sort_order']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.C('cms_site_config', data)
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
_id = params_kw.get('id', '')

View File

@ -1,30 +1,9 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
ns = {'sort': 'config_group asc, sort_order asc'}
# data_filter support
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
# Manual filter params
_config_group = params_kw.get('config_group', None)
if _config_group:
ns['config_group'] = _config_group
rows = await sor.R('cms_site_config', ns)
total = len(rows)
return {'status': 'ok', 'rows': rows, 'total': total}
return {'status': 'ok', 'rows': rows, 'total': len(rows)}

View File

@ -1,36 +1,17 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {'id': params_kw.get('id', '')}
if not data['id']:
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
v = params_kw.get('org_id', None)
if v is not None:
data['org_id'] = v
v = params_kw.get('config_group', None)
if v is not None:
data['config_group'] = v
v = params_kw.get('config_key', None)
if v is not None:
data['config_key'] = v
v = params_kw.get('config_value', None)
if v is not None:
data['config_value'] = v
v = params_kw.get('config_type', None)
if v is not None:
data['config_type'] = v
v = params_kw.get('sort_order', None)
if v is not None:
data['sort_order'] = v
for field in ['org_id', 'config_group', 'config_key', 'config_value', 'config_type', 'sort_order']:
v = params_kw.get(field, None)
if v is not None:
data[field] = v
await sor.U('cms_site_config', data)
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
group = params_kw.get('group', None)

View File

@ -1,7 +1,6 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
_id = params_kw.get('id', '')

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
content_type = params_kw.get('content_type', None)

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
ns = {'is_visible': '1', 'sort': 'sort_order asc'}

View File

@ -1,7 +1,7 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
content_id = params_kw.get('content_id', '')
@ -10,10 +10,9 @@ async with db.sqlorContext(dbname) as sor:
else:
# Update status to pending
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
# Try to call dingdingflow
# Call submit_approval from cms module
try:
from dingdingflow.init import submit_approval
user_id = await get_user()
ns_detail = {'id': content_id}
rows = await sor.R('cms_content', ns_detail)
@ -22,5 +21,5 @@ async with db.sqlorContext(dbname) as sor:
if result and result.get('approval_id'):
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
return {'widgettype': 'Message', 'options': {'text': '已提交审批', 'messagetype': 'success'}}
except ImportError:
return {'widgettype': 'Message', 'options': {'text': '审批模块未安装,状态已改为待审批', 'messagetype': 'warning'}}
except Exception as e:
return {'widgettype': 'Message', 'options': {'text': f'审批提交失败: {str(e)},状态已改为待审批', 'messagetype': 'warning'}}

View File

@ -1,6 +1,6 @@
config = getConfig('.')
DBPools(config.databases)
dbname = get_module_dbname('entcms')
dbname = get_module_dbname('cms')
async with db.sqlorContext(dbname) as sor:
data = {
@ -9,7 +9,7 @@ async with db.sqlorContext(dbname) as sor:
'status': 'new',
'org_id': '0'
}
for field in ['name', 'company', 'phone', 'email', 'industry', 'region',
for field in ['name', 'company', 'phone', 'email', 'industry', 'region',
'interest_products', 'message']:
v = params_kw.get(field, None)
if v is not None:
@ -17,6 +17,6 @@ async with db.sqlorContext(dbname) as sor:
await sor.C('cms_leads', data)
return {
'widgettype': 'Message',
'widgettype': 'Message',
'options': {'text': '感谢您的留言,我们会尽快联系您!', 'messagetype': 'success'}
}

View File

@ -1,13 +0,0 @@
{% set all_cases = get_published_content('case', 20) %}
{
"widgettype": "VBox",
"options": {"width": "100%", "css": "site-root"},
"subwidgets": [
{
"widgettype": "Html",
"options": {
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">成功案例<\/h2><p class=\"section-desc\">AI正在改变千行百业<\/p><div class=\"cases-grid\">{% for c in all_cases %}<div class=\"case-card\"><div class=\"case-tag\">{{c.tags or '行业案例'}}<\/div><div class=\"case-title\">{{c.title}}<\/div><div class=\"case-desc\">{{c.summary_text}}<\/div><\/div>{% endfor %}<\/div><div class=\"cta-banner\" style=\"margin-top:40px\"><div class=\"cta-text\">想了解这些方案如何落地?<\/div><a class=\"btn-primary\" href=\"{{entire_url('index.ui')}}#contact\">了解更多 → 联系销售<\/a><\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
}
}
]
}

View File

@ -1,153 +0,0 @@
/* ===== 开元云科技 官网交互脚本 ===== */
document.addEventListener('DOMContentLoaded', function() {
initScrollAnimations();
initProductCards();
initFloatingWidget();
initSmoothScroll();
});
/* === Scroll Fade-in Animations === */
function initScrollAnimations() {
var elements = document.querySelectorAll('.fade-in');
if (!elements.length) return;
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1 });
elements.forEach(function(el) { observer.observe(el); });
}
/* === Product Card Expand === */
function initProductCards() {
var cards = document.querySelectorAll('.product-card');
cards.forEach(function(card) {
card.addEventListener('click', function() {
var wasActive = card.classList.contains('active');
// Close all
cards.forEach(function(c) { c.classList.remove('active'); });
// Toggle current
if (!wasActive) {
card.classList.add('active');
}
});
});
}
/* === Floating Contact Widget === */
function initFloatingWidget() {
var avatar = document.querySelector('.float-avatar');
var panel = document.querySelector('.float-panel');
if (!avatar || !panel) return;
avatar.addEventListener('click', function(e) {
e.stopPropagation();
panel.classList.toggle('active');
});
// Close panel on outside click
document.addEventListener('click', function(e) {
if (!panel.contains(e.target) && !avatar.contains(e.target)) {
panel.classList.remove('active');
}
});
// Panel option clicks -> show form
var options = panel.querySelectorAll('.panel-option');
options.forEach(function(opt) {
opt.addEventListener('click', function() {
var formType = opt.getAttribute('data-form');
var panelBody = panel.querySelector('.panel-body');
var panelForm = panel.querySelector('.panel-form');
if (panelBody) panelBody.style.display = 'none';
if (panelForm) {
panelForm.classList.add('active');
panelForm.setAttribute('data-type', formType);
}
});
});
// Back button
var backBtn = panel.querySelector('.back-btn');
if (backBtn) {
backBtn.addEventListener('click', function() {
var panelBody = panel.querySelector('.panel-body');
var panelForm = panel.querySelector('.panel-form');
if (panelBody) panelBody.style.display = 'block';
if (panelForm) panelForm.classList.remove('active');
});
}
// Form submit
var submitBtn = panel.querySelector('.submit-btn');
if (submitBtn) {
submitBtn.addEventListener('click', function(e) {
e.preventDefault();
var form = panel.querySelector('.panel-form');
var name = form.querySelector('[name="name"]');
var phone = form.querySelector('[name="phone"]');
var company = form.querySelector('[name="company"]');
var message = form.querySelector('[name="message"]');
if (!phone || !phone.value.trim()) {
alert('请填写联系电话');
return;
}
var data = {
name: name ? name.value : '',
phone: phone ? phone.value : '',
company: company ? company.value : '',
message: message ? message.value : '',
interest_products: form.getAttribute('data-type') || ''
};
// Submit via fetch
var submitUrl = form.getAttribute('data-submit-url');
if (!submitUrl) {
alert('提交成功,我们会尽快联系您!');
panel.classList.remove('active');
return;
}
fetch(submitUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(function(r) { return r.json(); })
.then(function(resp) {
alert('感谢您的留言,我们会尽快联系您!');
panel.classList.remove('active');
// Reset form
if (name) name.value = '';
if (phone) phone.value = '';
if (company) company.value = '';
if (message) message.value = '';
})
.catch(function(err) {
console.error('Submit error:', err);
alert('提交失败,请稍后重试');
});
});
}
}
/* === Smooth Scroll for Nav Links === */
function initSmoothScroll() {
var links = document.querySelectorAll('.nav-links a[href^="#"]');
links.forEach(function(link) {
link.addEventListener('click', function(e) {
var target = document.querySelector(this.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
}

View File

@ -1,640 +0,0 @@
/* ===== 开元云科技 官网样式系统 ===== */
/* 设计参考: OpenAI风格, 极简科技感 */
/* === CSS Variables === */
:root {
--brand-primary: #6C5CE7;
--brand-light: #A29BFE;
--brand-sky: #74B9FF;
--brand-gradient: linear-gradient(135deg, #6C5CE7, #A29BFE, #74B9FF);
--bg-dark: #0a0a0a;
--bg-card: #1A1A1A;
--bg-card-hover: #222222;
--border-dark: #222;
--border-active: #6C5CE7;
--text-primary: #FFFFFF;
--text-secondary: #999999;
--text-muted: #666666;
--max-width: 1100px;
--font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--transition: all 0.3s ease;
}
/* === Reset & Base === */
* { margin: 0; padding: 0; box-sizing: border-box; }
body, html {
font-family: var(--font-family);
background: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
/* === Navigation === */
.nav-bar {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background: rgba(10, 10, 10, 0.8);
border-bottom: 1px solid rgba(255,255,255,0.06);
padding: 0 48px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-logo {
font-size: 18px;
font-weight: 700;
background: var(--brand-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-links {
display: flex;
gap: 32px;
list-style: none;
}
.nav-links a {
font-size: 14px;
color: var(--text-secondary);
transition: var(--transition);
}
.nav-links a:hover { color: var(--text-primary); }
.nav-cta {
display: inline-flex;
align-items: center;
padding: 8px 20px;
background: var(--brand-primary);
color: white;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: var(--transition);
border: none;
cursor: pointer;
}
.nav-cta:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(108, 92, 231, 0.4);
}
/* === Hero Section === */
.hero-section {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 120px 48px 80px;
position: relative;
overflow: hidden;
}
.hero-bg-glow {
position: absolute;
width: 800px;
height: 800px;
border-radius: 50%;
background: radial-gradient(circle, rgba(108, 92, 231, 0.15) 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
.hero-content {
max-width: var(--max-width);
width: 100%;
position: relative;
z-index: 1;
}
.hero-tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
background: rgba(108, 92, 231, 0.15);
border: 1px solid rgba(108, 92, 231, 0.3);
border-radius: 20px;
font-size: 13px;
color: var(--brand-light);
margin-bottom: 24px;
}
.hero-tag .pulse-dot {
width: 8px;
height: 8px;
background: var(--brand-primary);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.hero-title {
font-size: 56px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 20px;
}
.hero-title .gradient-text {
background: var(--brand-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: 18px;
color: var(--text-secondary);
font-weight: 300;
max-width: 600px;
margin-bottom: 40px;
}
.hero-buttons {
display: flex;
gap: 16px;
}
.btn-primary {
display: inline-flex;
align-items: center;
padding: 12px 28px;
background: var(--brand-primary);
color: white;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
transition: var(--transition);
border: none;
cursor: pointer;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(108, 92, 231, 0.4);
}
.btn-outline {
display: inline-flex;
align-items: center;
padding: 12px 28px;
background: transparent;
color: var(--text-primary);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
font-size: 16px;
font-weight: 500;
transition: var(--transition);
cursor: pointer;
}
.btn-outline:hover {
border-color: var(--brand-primary);
transform: translateY(-2px);
}
.hero-mascot {
position: absolute;
right: 0;
bottom: 10%;
opacity: 0.12;
pointer-events: none;
}
/* === Section Common === */
.section {
padding: 80px 48px;
max-width: var(--max-width);
margin: 0 auto;
}
.section-title {
font-size: 36px;
font-weight: 700;
margin-bottom: 12px;
}
.section-desc {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 48px;
font-weight: 300;
}
/* === Products (1+N+X) === */
.products-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.product-card {
background: var(--bg-card);
border: 1px solid var(--border-dark);
border-radius: 12px;
padding: 32px 24px;
cursor: pointer;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.product-card::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 3px;
background: transparent;
transition: var(--transition);
}
.product-card:hover {
transform: translateX(4px);
border-color: var(--brand-primary);
}
.product-card:hover::before {
background: var(--brand-primary);
}
.product-card.active {
background: rgba(108, 92, 231, 0.1);
border-color: var(--brand-primary);
}
.product-card.active::before {
background: var(--brand-primary);
}
.product-card .card-icon {
font-size: 32px;
margin-bottom: 16px;
}
.product-card .card-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.product-card .card-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
.product-detail {
display: none;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border-dark);
font-size: 14px;
color: var(--text-secondary);
line-height: 1.8;
}
.product-card.active .product-detail {
display: block;
}
/* === Cases === */
.cases-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.case-card {
background: var(--bg-card);
border: 1px solid var(--border-dark);
border-radius: 12px;
padding: 24px;
transition: var(--transition);
cursor: pointer;
}
.case-card:hover {
transform: translateY(-4px);
border-color: var(--brand-primary);
}
.case-tag {
font-size: 11px;
color: var(--brand-light);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
font-weight: 500;
}
.case-title {
font-size: 17px;
font-weight: 600;
margin-bottom: 8px;
}
.case-desc {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
}
/* === CTA Banner === */
.cta-banner {
background: linear-gradient(135deg, rgba(108, 92, 231, 0.15), rgba(116, 185, 255, 0.1));
border: 1px solid rgba(108, 92, 231, 0.2);
border-radius: 16px;
padding: 40px;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 40px;
}
.cta-text {
font-size: 20px;
font-weight: 600;
}
/* === News === */
.news-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.news-view-all {
font-size: 14px;
color: var(--brand-light);
transition: var(--transition);
}
.news-view-all:hover { color: var(--brand-primary); }
.news-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.news-item {
display: flex;
align-items: flex-start;
gap: 20px;
padding: 20px;
background: var(--bg-card);
border: 1px solid var(--border-dark);
border-radius: 12px;
transition: var(--transition);
cursor: pointer;
}
.news-item:hover {
border-color: var(--brand-primary);
}
.news-date {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
min-width: 80px;
}
.news-title {
font-size: 14px;
font-weight: 500;
}
/* === Footer === */
.site-footer {
border-top: 1px solid rgba(255,255,255,0.06);
padding: 40px 48px;
text-align: center;
font-size: 13px;
color: var(--text-muted);
}
/* === Floating Contact Widget === */
.float-contact {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 200;
}
.float-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--brand-gradient);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 20px rgba(108, 92, 231, 0.3);
transition: var(--transition);
}
.float-avatar:hover {
transform: translateY(-3px) scale(1.05);
}
.float-avatar svg {
width: 32px;
height: 32px;
}
.float-bubble {
position: absolute;
right: 68px;
bottom: 12px;
background: white;
color: #333;
padding: 10px 16px;
border-radius: 12px;
font-size: 14px;
white-space: nowrap;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
opacity: 0;
transform: translateX(10px);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
}
.float-contact:hover .float-bubble {
opacity: 1;
transform: translateX(0);
}
.float-panel {
position: absolute;
bottom: 70px;
right: 0;
width: 300px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
overflow: hidden;
display: none;
animation: panelSlideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.float-panel.active { display: block; }
@keyframes panelSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.panel-header {
background: var(--brand-gradient);
padding: 20px;
color: white;
font-size: 16px;
font-weight: 600;
}
.panel-body {
padding: 16px;
}
.panel-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #f5f5f5;
border-radius: 10px;
margin-bottom: 8px;
cursor: pointer;
transition: var(--transition);
color: #333;
font-size: 14px;
}
.panel-option:hover {
background: var(--brand-primary);
color: white;
}
.panel-option svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.panel-form {
padding: 16px;
display: none;
}
.panel-form.active { display: block; }
.panel-form input,
.panel-form textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
margin-bottom: 10px;
font-family: var(--font-family);
outline: none;
transition: var(--transition);
}
.panel-form input:focus,
.panel-form textarea:focus {
border-color: var(--brand-primary);
}
.panel-form textarea { resize: vertical; min-height: 60px; }
.panel-form .submit-btn {
width: 100%;
padding: 10px;
background: var(--brand-primary);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.panel-form .submit-btn:hover {
background: #5a4bd1;
}
.panel-form .back-btn {
background: none;
border: none;
color: var(--brand-primary);
font-size: 13px;
cursor: pointer;
margin-bottom: 12px;
padding: 0;
}
/* === Responsive === */
@media (max-width: 768px) {
.nav-bar { padding: 0 20px; }
.nav-links { display: none; }
.hero-section { padding: 100px 20px 60px; }
.hero-title { font-size: 32px; }
.hero-subtitle { font-size: 16px; }
.hero-buttons { flex-direction: column; }
.section { padding: 60px 20px; }
.section-title { font-size: 28px; }
.products-grid { grid-template-columns: 1fr; }
.cases-grid { grid-template-columns: 1fr; }
.cta-banner {
flex-direction: column;
text-align: center;
gap: 20px;
}
.news-item { flex-direction: column; gap: 8px; }
.site-footer { padding: 30px 20px; }
.float-panel { width: 280px; right: -8px; }
}
/* === Scroll Animations === */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}

View File

@ -1,77 +0,0 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"label": "钉钉审批管理", "fontSize": "24px", "fontWeight": "bold", "marginBottom": "20px"}
},
{
"widgettype": "ResponsableBox",
"options": {"gap": "16px", "minWidth": "280px"},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.dingdingflow_content",
"options": {"url": "{{entire_url('dd_approvals/index.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{
"widgettype": "Text",
"options": {"label": "📋 审批记录", "fontSize": "18px", "fontWeight": "bold", "marginBottom": "8px"}
},
{
"widgettype": "Text",
"options": {"label": "查看和管理所有审批申请记录,包括待审批、已通过、已拒绝的审批", "fontSize": "13px", "color": "#666"}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.dingdingflow_content",
"options": {"url": "{{entire_url('dd_approval_configs/index.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{
"widgettype": "Text",
"options": {"label": "⚙️ 审批流程配置", "fontSize": "18px", "fontWeight": "bold", "marginBottom": "8px"}
},
{
"widgettype": "Text",
"options": {"label": "配置不同业务类型的钉钉审批模板,设置审批流程参数", "fontSize": "13px", "color": "#666"}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "app.dingdingflow_content",
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}
}
]
}

View File

@ -1,25 +0,0 @@
{
"widgettype": "Menu",
"options": {
"items": [
{
"title": "钉钉审批",
"icon": "icon-approve",
"items": [
{
"title": "审批管理",
"url": "{{entire_url('dingdingflow/index.ui')}}"
},
{
"title": "审批记录",
"url": "{{entire_url('dingdingflow/dd_approvals/index.ui')}}"
},
{
"title": "流程配置",
"url": "{{entire_url('dingdingflow/dd_approval_configs/index.ui')}}"
}
]
}
]
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
{% set news_items = get_published_content('news', 50) %}
{
"widgettype": "VBox",
"options": {"width": "100%", "css": "site-root"},
"subwidgets": [
{
"widgettype": "Html",
"options": {
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#news\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">企业动态<\/h2><p class=\"section-desc\">了解开元云最新资讯与行业洞察<\/p><div class=\"news-list\">{% for item in news_items %}<a class=\"news-item\" href=\"{{entire_url('news_detail.ui')}}?id={{item.id}}\"><span class=\"news-date\">{{item.published_at or item.created_at}}<\/span><div><span class=\"news-title\">{{item.title}}<\/span>{% if item.summary_text %}<p style=\"font-size:13px;color:#666;margin-top:4px\">{{item.summary_text}}<\/p>{% endif %}<\/div><\/a>{% endfor %}<\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
}
}
]
}

View File

@ -1,13 +0,0 @@
{% set article = get_content_detail(params_kw.get('id', '')) %}
{
"widgettype": "VBox",
"options": {"width": "100%", "css": "site-root"},
"subwidgets": [
{
"widgettype": "Html",
"options": {
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px;max-width:800px\">{% if article %}<a href=\"{{entire_url('news.ui')}}\" style=\"color:#A29BFE;font-size:14px;margin-bottom:24px;display:inline-block\">← 返回新闻列表<\/a><h1 class=\"section-title\" style=\"margin-bottom:12px\">{{article.title}}<\/h1><p style=\"font-size:13px;color:#666;margin-bottom:32px\">{{article.published_at or article.created_at}}{% if article.tags %} · {{article.tags}}{% endif %}<\/p>{% if article.image_url %}<img src=\"{{article.image_url}}\" style=\"width:100%;border-radius:12px;margin-bottom:32px\" /><\/img>{% endif %}<div style=\"font-size:16px;line-height:1.8;color:#ccc\">{{article.body or article.summary_text or ''}}<\/div>{% else %}<p style=\"color:#999\">文章不存在或已下线<\/p><a href=\"{{entire_url('news.ui')}}\" style=\"color:#A29BFE\">← 返回新闻列表<\/a>{% endif %}<\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
}
}
]
}

View File

@ -1,13 +0,0 @@
{% set products = get_published_content('product', 50) %}
{
"widgettype": "VBox",
"options": {"width": "100%", "css": "site-root"},
"subwidgets": [
{
"widgettype": "Html",
"options": {
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">产品架构<\/h2><p class=\"section-desc\">AI基础设施全栈解决方案<\/p><div class=\"cases-grid\">{% for p in products %}<div class=\"case-card\"><div class=\"case-tag\">{{p.tags or '核心产品'}}<\/div><div class=\"case-title\">{{p.title}}<\/div><div class=\"case-desc\">{{p.summary_text}}<\/div><\/div>{% endfor %}<\/div><div class=\"cta-banner\" style=\"margin-top:40px\"><div class=\"cta-text\">需要定制化方案?<\/div><a class=\"btn-primary\" href=\"{{entire_url('index.ui')}}#contact\">联系解决方案团队 →<\/a><\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
}
}
]
}