refactor: 钉钉审批独立为 dingdingflow 模块,cms 侧剔除
- 删除 cms/dingtalk_client.py、dd_approvals*/dd_approval_configs* 的 models/json/dspy(共14文件) - cms/init.py 删钉钉段 355 行(792->450) - submit_content_for_approval 改走 ServerEnv 软依赖调 submit_approval, 未加载 dingdingflow 时只置 pending 不报错 - 新增 on_content_approval_done 回调钩子:原先硬编码在钉钉回调里的 cms_content 回写逻辑归位到 cms 自己,由 load_cms() 注册到 content_publish - load_path 移除 11 条钉钉路径;i18n 移除钉钉专属文案;README 更新依赖说明
This commit is contained in:
parent
ea92c8f142
commit
20602fadfb
23
README.md
23
README.md
@ -1,6 +1,6 @@
|
||||
# CMS 内容管理模块
|
||||
|
||||
企业官网内容管理与钉钉审批工作流模块,基于Sage/bricks-framework开发。
|
||||
企业官网内容管理模块,基于Sage/bricks-framework开发。
|
||||
|
||||
## 功能
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
- **栏目管理**: 官网页面栏目配置
|
||||
- **商机线索**: 网站访客提交 + AI抽取
|
||||
- **站点配置**: Hero标语、页脚等KV配置
|
||||
- **钉钉审批**: 内容发布审批工作流
|
||||
- **内容审批**: 发起发布审批(审批能力由 dingdingflow 独立模块提供)
|
||||
|
||||
## 数据库表
|
||||
|
||||
@ -20,8 +20,6 @@
|
||||
| cms_sections | 栏目管理 |
|
||||
| cms_leads | 商机线索 |
|
||||
| cms_site_config | 站点配置 |
|
||||
| dd_approvals | 审批记录 |
|
||||
| dd_approval_configs | 审批流程配置 |
|
||||
|
||||
## 安装
|
||||
|
||||
@ -49,12 +47,17 @@ def init():
|
||||
- cms_sections: 默认栏目配置
|
||||
- dd_approval_configs: 默认审批配置
|
||||
|
||||
## 环境变量 (钉钉审批)
|
||||
## 审批依赖
|
||||
|
||||
```
|
||||
DINGTALK_APP_KEY=xxx
|
||||
DINGTALK_APP_SECRET=xxx
|
||||
DINGTALK_AGENT_ID=xxx
|
||||
内容审批走 **dingdingflow** 独立模块(软依赖):
|
||||
|
||||
```python
|
||||
from cms.init import load_cms
|
||||
from dingdingflow.init import load_dingdingflow
|
||||
|
||||
load_dingdingflow() # 必须先加载,cms 才能注册审批回调钩子
|
||||
load_cms()
|
||||
```
|
||||
|
||||
缺少环境变量时自动使用mock响应。
|
||||
未加载 dingdingflow 时,`submit_content_for_approval` 只把内容置为 pending
|
||||
并返回提示,不报错。钉钉凭据/审批模板配置见 dingdingflow 的 README。
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""
|
||||
CMS - 企业官网内容管理与审批工作流模块
|
||||
合并原 entcms + dingdingflow 两个子模块
|
||||
"""CMS - 企业官网内容管理模块
|
||||
|
||||
钉钉审批已独立为 dingdingflow 模块。本模块通过 ServerEnv 软依赖调用其
|
||||
submit_approval,并注册 content_publish 的审批结果回调钩子。
|
||||
"""
|
||||
|
||||
from .init import load_cms
|
||||
|
||||
@ -1,240 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DingTalk API Client for approval workflow integration.
|
||||
Reads credentials from environment variables (DINGTALK_APP_KEY, DINGTALK_APP_SECRET, DINGTALK_AGENT_ID).
|
||||
Gracefully handles missing credentials by returning mock responses in dev mode.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DingTalkClient:
|
||||
"""Client for DingTalk Open API - approval workflow operations."""
|
||||
|
||||
BASE_URL = "https://oapi.dingtalk.com"
|
||||
|
||||
def __init__(self):
|
||||
self.app_key = os.environ.get("DINGTALK_APP_KEY", "")
|
||||
self.app_secret = os.environ.get("DINGTALK_APP_SECRET", "")
|
||||
self.agent_id = os.environ.get("DINGTALK_AGENT_ID", "")
|
||||
self.callback_token = os.environ.get("DINGTALK_CALLBACK_TOKEN", "")
|
||||
self._access_token = None
|
||||
self._token_expires_at = 0
|
||||
|
||||
if not self.app_key or not self.app_secret:
|
||||
logger.warning(
|
||||
"DingTalk credentials not configured (DINGTALK_APP_KEY / DINGTALK_APP_SECRET). "
|
||||
"Running in dev/mock mode - API calls will return mock responses."
|
||||
)
|
||||
|
||||
@property
|
||||
def is_dev_mode(self):
|
||||
"""Return True if credentials are missing (dev/mock mode)."""
|
||||
return not self.app_key or not self.app_secret
|
||||
|
||||
def _http_post(self, url, data=None, params=None):
|
||||
"""Make HTTP POST request and return parsed JSON response."""
|
||||
if params:
|
||||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||
url = f"{url}?{query}"
|
||||
|
||||
body = json.dumps(data).encode("utf-8") if data else b""
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
resp_data = resp.read().decode("utf-8")
|
||||
return json.loads(resp_data)
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8", errors="replace")
|
||||
logger.error("DingTalk API HTTP error %s: %s", e.code, error_body)
|
||||
return {"errcode": e.code, "errmsg": f"HTTP {e.code}: {error_body}"}
|
||||
except urllib.error.URLError as e:
|
||||
logger.error("DingTalk API connection error: %s", str(e))
|
||||
return {"errcode": -1, "errmsg": f"Connection error: {str(e)}"}
|
||||
except Exception as e:
|
||||
logger.error("DingTalk API unexpected error: %s", str(e))
|
||||
return {"errcode": -1, "errmsg": str(e)}
|
||||
|
||||
def _http_get(self, url, params=None):
|
||||
"""Make HTTP GET request and return parsed JSON response."""
|
||||
if params:
|
||||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||
url = f"{url}?{query}"
|
||||
|
||||
req = urllib.request.Request(url, headers={"Content-Type": "application/json"})
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
resp_data = resp.read().decode("utf-8")
|
||||
return json.loads(resp_data)
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8", errors="replace")
|
||||
logger.error("DingTalk API HTTP error %s: %s", e.code, error_body)
|
||||
return {"errcode": e.code, "errmsg": f"HTTP {e.code}: {error_body}"}
|
||||
except Exception as e:
|
||||
logger.error("DingTalk API unexpected error: %s", str(e))
|
||||
return {"errcode": -1, "errmsg": str(e)}
|
||||
|
||||
def get_access_token(self):
|
||||
"""
|
||||
Get DingTalk access token. Caches token until expiry.
|
||||
Returns token string or empty string on failure.
|
||||
"""
|
||||
if self.is_dev_mode:
|
||||
logger.info("Dev mode: returning mock access token")
|
||||
return "mock_access_token_dev"
|
||||
|
||||
# Return cached token if still valid (with 5-min buffer)
|
||||
now = time.time()
|
||||
if self._access_token and now < self._token_expires_at - 300:
|
||||
return self._access_token
|
||||
|
||||
url = f"{self.BASE_URL}/gettoken"
|
||||
params = {"appkey": self.app_key, "appsecret": self.app_secret}
|
||||
result = self._http_get(url, params=params)
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
self._access_token = result.get("access_token", "")
|
||||
expires_in = result.get("expires_in", 7200)
|
||||
self._token_expires_at = now + expires_in
|
||||
logger.info("DingTalk access token obtained, expires in %ss", expires_in)
|
||||
return self._access_token
|
||||
else:
|
||||
logger.error(
|
||||
"Failed to get DingTalk access token: %s",
|
||||
result.get("errmsg", "unknown error"),
|
||||
)
|
||||
return ""
|
||||
|
||||
def create_approval_instance(self, process_code, form_data, originator_user_id):
|
||||
"""
|
||||
Create a DingTalk approval instance.
|
||||
|
||||
Args:
|
||||
process_code: DingTalk approval template code (from dd_approval_configs)
|
||||
form_data: List of form component values, e.g.:
|
||||
[{"name": "审批类型", "value": "内容发布"}, ...]
|
||||
originator_user_id: DingTalk user ID of the applicant
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success (bool)
|
||||
- instance_id (str) - DingTalk process instance ID
|
||||
- errmsg (str) - error message if failed
|
||||
"""
|
||||
if self.is_dev_mode:
|
||||
mock_instance_id = f"mock_instance_{int(time.time())}"
|
||||
logger.info(
|
||||
"Dev mode: mock approval instance created: %s (process_code=%s)",
|
||||
mock_instance_id,
|
||||
process_code,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"instance_id": mock_instance_id,
|
||||
"errmsg": "",
|
||||
}
|
||||
|
||||
token = self.get_access_token()
|
||||
if not token:
|
||||
return {"success": False, "instance_id": "", "errmsg": "Failed to get access token"}
|
||||
|
||||
url = f"{self.BASE_URL}/topapi/processinstance/create"
|
||||
payload = {
|
||||
"agent_id": int(self.agent_id) if self.agent_id else 0,
|
||||
"process_code": process_code,
|
||||
"originator_user_id": originator_user_id,
|
||||
"dept_id": -1,
|
||||
"form_component_values": form_data,
|
||||
}
|
||||
|
||||
result = self._http_post(url, data=payload, params={"access_token": token})
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
instance_id = result.get("process_instance_id", "")
|
||||
logger.info("DingTalk approval instance created: %s", instance_id)
|
||||
return {"success": True, "instance_id": instance_id, "errmsg": ""}
|
||||
else:
|
||||
errmsg = result.get("errmsg", "unknown error")
|
||||
logger.error("Failed to create DingTalk approval: %s", errmsg)
|
||||
return {"success": False, "instance_id": "", "errmsg": errmsg}
|
||||
|
||||
def get_approval_instance(self, instance_id):
|
||||
"""
|
||||
Get DingTalk approval instance details.
|
||||
|
||||
Args:
|
||||
instance_id: DingTalk process instance ID
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success (bool)
|
||||
- status (str) - NEW/RUNNING/COMPLETED/TERMINATED
|
||||
- result (str) - agree/refuse (only when status=COMPLETED)
|
||||
- data (dict) - full instance data
|
||||
- errmsg (str) - error message if failed
|
||||
"""
|
||||
if self.is_dev_mode:
|
||||
logger.info("Dev mode: mock get approval instance: %s", instance_id)
|
||||
return {
|
||||
"success": True,
|
||||
"status": "COMPLETED",
|
||||
"result": "agree",
|
||||
"data": {
|
||||
"process_instance_id": instance_id,
|
||||
"status": "COMPLETED",
|
||||
"result": "agree",
|
||||
},
|
||||
"errmsg": "",
|
||||
}
|
||||
|
||||
token = self.get_access_token()
|
||||
if not token:
|
||||
return {"success": False, "status": "", "result": "", "data": {}, "errmsg": "Failed to get access token"}
|
||||
|
||||
url = f"{self.BASE_URL}/topapi/processinstance/get"
|
||||
payload = {"process_instance_id": instance_id}
|
||||
|
||||
result = self._http_post(url, data=payload, params={"access_token": token})
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
pi = result.get("process_instance", {})
|
||||
status = pi.get("status", "")
|
||||
pi_result = pi.get("result", "")
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"result": pi_result,
|
||||
"data": pi,
|
||||
"errmsg": "",
|
||||
}
|
||||
else:
|
||||
errmsg = result.get("errmsg", "unknown error")
|
||||
logger.error("Failed to get DingTalk approval instance: %s", errmsg)
|
||||
return {"success": False, "status": "", "result": "", "data": {}, "errmsg": errmsg}
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
_client_instance = None
|
||||
|
||||
|
||||
def get_dingtalk_client():
|
||||
"""Get or create the DingTalkClient singleton."""
|
||||
global _client_instance
|
||||
if _client_instance is None:
|
||||
_client_instance = DingTalkClient()
|
||||
return _client_instance
|
||||
417
cms/init.py
417
cms/init.py
@ -9,8 +9,7 @@ cms - 企业CMS内容管理与钉钉审批工作流模块
|
||||
- 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)
|
||||
- 内容审批发起(审批能力由 dingdingflow 独立模块提供,本模块只注册结果回调钩子)
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
@ -19,7 +18,6 @@ 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__)
|
||||
|
||||
@ -353,374 +351,47 @@ async def get_content_detail(content_id):
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
async def submit_content_for_approval(content_id, title, applicant_id):
|
||||
"""提交内容审批(调用钉钉审批流程)"""
|
||||
"""提交内容审批。
|
||||
|
||||
审批能力由 dingdingflow 独立模块提供,通过 ServerEnv 调用(软依赖):
|
||||
未安装/未加载该模块时只把内容置为 pending,不报错。
|
||||
"""
|
||||
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'):
|
||||
|
||||
env = ServerEnv()
|
||||
submit_approval = getattr(env, 'submit_approval', None)
|
||||
if submit_approval is None:
|
||||
logger.warning('submit_content_for_approval: dingdingflow 未加载,'
|
||||
'内容已置 pending 但未发起审批')
|
||||
return {'success': False, 'message': '审批模块(dingdingflow)未加载'}
|
||||
|
||||
result = await submit_approval('content_publish', content_id, title, applicant_id)
|
||||
if result and result.get('approval_id'):
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# DD Approvals CRUD (原 dingdingflow)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
async def on_content_approval_done(biz_id, status, approval_id, comment):
|
||||
"""dingdingflow 审批结果回调钩子(biz_type='content_publish')。
|
||||
|
||||
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'):
|
||||
由 load_cms() 注册:审批通过→published,驳回→draft。
|
||||
这是原先硬编码在钉钉回调里的 cms_content 回写逻辑,现归位到 cms 自己。
|
||||
"""
|
||||
提交审批请求:
|
||||
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):
|
||||
"""查询钉钉审批最新状态并同步到本地"""
|
||||
content_status = {'approved': 'published', 'rejected': 'draft'}.get(status, '')
|
||||
if not content_status:
|
||||
return
|
||||
data = {'id': biz_id, 'status': content_status}
|
||||
if content_status == 'published':
|
||||
data['published_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.U('cms_content', data)
|
||||
logger.info('cms_content %s -> %s (approval %s)', biz_id, content_status, approval_id)
|
||||
|
||||
# 获取本地记录
|
||||
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"""
|
||||
@ -766,26 +437,12 @@ def load_cms():
|
||||
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
|
||||
# 审批结果回调钩子(审批能力在 dingdingflow 独立模块)
|
||||
register_biz_handler = getattr(env, 'register_biz_handler', None)
|
||||
if register_biz_handler:
|
||||
register_biz_handler('content_publish', on_content_approval_done)
|
||||
else:
|
||||
logger.info('dingdingflow 未加载,跳过 content_publish 审批钩子注册')
|
||||
|
||||
logger.info('cms module loaded (v%s)', MODULE_VERSION)
|
||||
return True
|
||||
|
||||
@ -96,7 +96,4 @@ title is required: title is required
|
||||
配置值: 配置值
|
||||
配置组(hero/footer/contact/seo): 配置组(hero/footer/contact/seo)
|
||||
配置键: 配置键
|
||||
钉钉审批实例ID: 钉钉审批实例ID
|
||||
钉钉审批模板编码: 钉钉审批模板编码
|
||||
钉钉应用AgentId: 钉钉应用AgentId
|
||||
静态内容(用于hero/cta等固定内容栏目): 静态内容(用于hero/cta等固定内容栏目)
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"tblname": "dd_approval_configs",
|
||||
"alias": "dd_approval_configs",
|
||||
"title": "审批流程配置",
|
||||
"params": {
|
||||
"sortby": ["biz_type"],
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "form_config"],
|
||||
"alters": {
|
||||
"is_active": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "1", "text": "启用"},
|
||||
{"value": "0", "text": "停用"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"tblname": "dd_approvals",
|
||||
"alias": "dd_approvals",
|
||||
"title": "审批记录",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "status", "op": "=", "var": "status_filter"},
|
||||
{"field": "biz_type", "op": "=", "var": "biz_type_filter"},
|
||||
{"field": "title", "op": "LIKE", "var": "title_filter"}
|
||||
]
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": ["id"],
|
||||
"alters": {
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "pending", "text": "待审批"},
|
||||
{"value": "approved", "text": "已通过"},
|
||||
{"value": "rejected", "text": "已拒绝"},
|
||||
{"value": "cancelled", "text": "已取消"}
|
||||
]
|
||||
},
|
||||
"biz_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "content_publish", "text": "内容发布"},
|
||||
{"value": "content_update", "text": "内容修改"},
|
||||
{"value": "content_delete", "text": "内容删除"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "dd_approval_configs",
|
||||
"title": "审批流程配置表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "biz_type",
|
||||
"title": "业务类型",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "biz_type_title",
|
||||
"title": "业务类型名称",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "process_code",
|
||||
"title": "钉钉审批模板编码",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "agent_id",
|
||||
"title": "钉钉应用AgentId",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "form_config",
|
||||
"title": "表单字段配置JSON",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "is_active",
|
||||
"title": "是否启用(1/0)",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_apvcfg_org_type",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"biz_type"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "dd_approvals",
|
||||
"title": "审批记录表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "biz_type",
|
||||
"title": "业务类型(content_publish等)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "biz_id",
|
||||
"title": "业务数据ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "审批标题",
|
||||
"type": "str",
|
||||
"length": 255,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "applicant_id",
|
||||
"title": "申请人ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "approver_id",
|
||||
"title": "审批人ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "dingtalk_instance_id",
|
||||
"title": "钉钉审批实例ID",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态(pending/approved/rejected/cancelled)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "pending"
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"title": "审批意见",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "completed_at",
|
||||
"title": "完成时间",
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_approval_biz",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"biz_type",
|
||||
"biz_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_approval_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_approval_applicant",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"applicant_id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -78,20 +78,9 @@ PATHS = [
|
||||
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",
|
||||
# Export (in portal wwwroot/api/, not /cms/ prefix)
|
||||
f"/api/export_leads.dspy",
|
||||
f"/api/export_content.dspy",
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
biz_type_title = params_kw.get('biz_type_title', '')
|
||||
process_code = params_kw.get('process_code', '')
|
||||
|
||||
if not biz_type:
|
||||
result['options'] = {'title': 'Error', 'message': 'biz_type is required', 'type': 'error'}
|
||||
else:
|
||||
new_id = getID()
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe(
|
||||
"INSERT INTO dd_approval_configs (id, org_id, biz_type, biz_type_title, process_code, agent_id, form_config, is_active, created_at, updated_at) VALUES (${id}$, ${org_id}$, ${biz_type}$, ${biz_type_title}$, ${process_code}$, ${agent_id}$, ${form_config}$, ${is_active}$, ${created_at}$, ${updated_at}$)",
|
||||
{
|
||||
'id': new_id,
|
||||
'org_id': org_id,
|
||||
'biz_type': biz_type,
|
||||
'biz_type_title': biz_type_title,
|
||||
'process_code': process_code,
|
||||
'agent_id': params_kw.get('agent_id', ''),
|
||||
'form_config': params_kw.get('form_config', ''),
|
||||
'is_active': params_kw.get('is_active', '1'),
|
||||
'created_at': now_str,
|
||||
'updated_at': now_str
|
||||
}
|
||||
)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置创建成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
@ -1,16 +0,0 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("DELETE FROM dd_approval_configs WHERE id=${id}$", {'id': record_id})
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置删除成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
@ -1,54 +0,0 @@
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
where_clauses = []
|
||||
where_ns = {}
|
||||
|
||||
# Optional filtering
|
||||
is_active = params_kw.get('is_active', '')
|
||||
if is_active:
|
||||
where_clauses.append("is_active=${is_active}$")
|
||||
where_ns['is_active'] = is_active
|
||||
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
if biz_type:
|
||||
where_clauses.append("biz_type=${biz_type}$")
|
||||
where_ns['biz_type'] = biz_type
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
where_prefix = " WHERE " if where_clauses else ""
|
||||
|
||||
# Count query
|
||||
count_sql = "SELECT count(*) rcnt FROM dd_approval_configs" + where_prefix + where_sql
|
||||
count_rows = await sor.sqlExe(count_sql, where_ns)
|
||||
total = 0
|
||||
if count_rows and len(count_rows) > 0:
|
||||
r = count_rows[0]
|
||||
total = getattr(r, 'rcnt', 0)
|
||||
|
||||
if total > 0:
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': params_kw.get('sort', 'biz_type')
|
||||
}
|
||||
sql = "SELECT id, org_id, biz_type, biz_type_title, process_code, agent_id, form_config, is_active, created_at, updated_at FROM dd_approval_configs" + where_prefix + where_sql
|
||||
query_ns = dict(list(ns.items()) + list(where_ns.items()))
|
||||
rows = await sor.sqlExe(sql, query_ns)
|
||||
|
||||
if isinstance(rows, dict):
|
||||
result['rows'] = rows.get('rows', [])
|
||||
result['total'] = rows.get('total', total)
|
||||
elif rows:
|
||||
result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows]
|
||||
result['total'] = total
|
||||
else:
|
||||
result['total'] = 0
|
||||
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return result
|
||||
@ -1,32 +0,0 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
update_fields = []
|
||||
update_ns = {'id': record_id}
|
||||
|
||||
for field in ['biz_type', 'biz_type_title', 'process_code', 'agent_id', 'form_config', 'is_active']:
|
||||
val = params_kw.get(field)
|
||||
if val is not None:
|
||||
update_fields.append(f"{field}=${field}$")
|
||||
update_ns[field] = val
|
||||
|
||||
# Always update updated_at
|
||||
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
update_fields.append("updated_at=${updated_at}$")
|
||||
update_ns['updated_at'] = now_str
|
||||
|
||||
if update_fields:
|
||||
set_clause = ", ".join(update_fields)
|
||||
await sor.sqlExe(f"UPDATE dd_approval_configs SET {set_clause} WHERE id=${id}$", update_ns)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置更新成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
@ -1,39 +0,0 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
biz_id = params_kw.get('biz_id', '')
|
||||
title = params_kw.get('title', '')
|
||||
applicant_id = params_kw.get('applicant_id', '')
|
||||
|
||||
if not biz_type or not title:
|
||||
result['options'] = {'title': 'Error', 'message': 'biz_type and title are required', 'type': 'error'}
|
||||
else:
|
||||
new_id = getID()
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe(
|
||||
"INSERT INTO dd_approvals (id, org_id, biz_type, biz_id, title, applicant_id, approver_id, dingtalk_instance_id, status, comment, created_at) VALUES (${id}$, ${org_id}$, ${biz_type}$, ${biz_id}$, ${title}$, ${applicant_id}$, ${approver_id}$, ${dingtalk_instance_id}$, ${status}$, ${comment}$, ${created_at}$)",
|
||||
{
|
||||
'id': new_id,
|
||||
'org_id': org_id,
|
||||
'biz_type': biz_type,
|
||||
'biz_id': biz_id,
|
||||
'title': title,
|
||||
'applicant_id': applicant_id,
|
||||
'approver_id': params_kw.get('approver_id', ''),
|
||||
'dingtalk_instance_id': params_kw.get('dingtalk_instance_id', ''),
|
||||
'status': params_kw.get('status', 'pending'),
|
||||
'comment': params_kw.get('comment', ''),
|
||||
'created_at': now_str
|
||||
}
|
||||
)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录创建成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
@ -1,16 +0,0 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("DELETE FROM dd_approvals WHERE id=${id}$", {'id': record_id})
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录删除成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
@ -1,72 +0,0 @@
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# Build WHERE clause from data_filter or direct params
|
||||
where_clauses = []
|
||||
where_ns = {}
|
||||
|
||||
# Support data_filter from CRUD search popup
|
||||
data_filter_str = params_kw.get('data_filter', '')
|
||||
if data_filter_str:
|
||||
# Individual filter values passed alongside data_filter
|
||||
status_filter = params_kw.get('status_filter', '')
|
||||
biz_type_filter = params_kw.get('biz_type_filter', '')
|
||||
title_filter = params_kw.get('title_filter', '')
|
||||
|
||||
if status_filter:
|
||||
where_clauses.append("status=${status_filter}$")
|
||||
where_ns['status_filter'] = status_filter
|
||||
if biz_type_filter:
|
||||
where_clauses.append("biz_type=${biz_type_filter}$")
|
||||
where_ns['biz_type_filter'] = biz_type_filter
|
||||
if title_filter:
|
||||
where_clauses.append("title LIKE ${title_filter}$")
|
||||
where_ns['title_filter'] = f'%{title_filter}%'
|
||||
else:
|
||||
# Direct param filtering
|
||||
status = params_kw.get('status', '')
|
||||
if status:
|
||||
where_clauses.append("status=${status}$")
|
||||
where_ns['status'] = status
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
if biz_type:
|
||||
where_clauses.append("biz_type=${biz_type}$")
|
||||
where_ns['biz_type'] = biz_type
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
where_prefix = " WHERE " if where_clauses else ""
|
||||
|
||||
# Count query
|
||||
count_sql = "SELECT count(*) rcnt FROM dd_approvals" + where_prefix + where_sql
|
||||
count_rows = await sor.sqlExe(count_sql, where_ns)
|
||||
total = 0
|
||||
if count_rows and len(count_rows) > 0:
|
||||
r = count_rows[0]
|
||||
total = getattr(r, 'rcnt', 0)
|
||||
|
||||
if total > 0:
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': params_kw.get('sort', 'created_at desc')
|
||||
}
|
||||
sql = "SELECT id, org_id, biz_type, biz_id, title, applicant_id, approver_id, dingtalk_instance_id, status, comment, created_at, completed_at FROM dd_approvals" + where_prefix + where_sql
|
||||
query_ns = dict(list(ns.items()) + list(where_ns.items()))
|
||||
rows = await sor.sqlExe(sql, query_ns)
|
||||
|
||||
if isinstance(rows, dict):
|
||||
result['rows'] = rows.get('rows', [])
|
||||
result['total'] = rows.get('total', total)
|
||||
elif rows:
|
||||
result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows]
|
||||
result['total'] = total
|
||||
else:
|
||||
result['total'] = 0
|
||||
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return result
|
||||
@ -1,32 +0,0 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
update_fields = []
|
||||
update_ns = {'id': record_id}
|
||||
|
||||
for field in ['biz_type', 'biz_id', 'title', 'applicant_id', 'approver_id', 'dingtalk_instance_id', 'status', 'comment']:
|
||||
val = params_kw.get(field)
|
||||
if val is not None:
|
||||
update_fields.append(f"{field}=${field}$")
|
||||
update_ns[field] = val
|
||||
|
||||
completed_at = params_kw.get('completed_at')
|
||||
if completed_at:
|
||||
update_fields.append("completed_at=${completed_at}$")
|
||||
update_ns['completed_at'] = completed_at
|
||||
|
||||
if update_fields:
|
||||
set_clause = ", ".join(update_fields)
|
||||
await sor.sqlExe(f"UPDATE dd_approvals SET {set_clause} WHERE id=${id}$", update_ns)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录更新成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return result
|
||||
@ -1,45 +0,0 @@
|
||||
result = {'success': False, 'message': 'Invalid callback'}
|
||||
|
||||
try:
|
||||
# DingTalk callback data comes via params_kw (POST body auto-parsed)
|
||||
callback_data = {}
|
||||
|
||||
# params_kw contains the parsed POST body fields
|
||||
process_instance_id = params_kw.get('processInstanceId', '')
|
||||
callback_type = params_kw.get('type', '')
|
||||
callback_result = params_kw.get('result', '')
|
||||
staff_id = params_kw.get('staffId', '')
|
||||
process_code = params_kw.get('processCode', '')
|
||||
remark = params_kw.get('remark', '')
|
||||
|
||||
# Also handle nested JSON body case where entire body is under a key
|
||||
if not process_instance_id:
|
||||
body = params_kw.get('body', None)
|
||||
if isinstance(body, dict):
|
||||
process_instance_id = body.get('processInstanceId', '')
|
||||
callback_type = body.get('type', '')
|
||||
callback_result = body.get('result', '')
|
||||
staff_id = body.get('staffId', '')
|
||||
process_code = body.get('processCode', '')
|
||||
remark = body.get('remark', '')
|
||||
|
||||
callback_data = {
|
||||
'processInstanceId': process_instance_id,
|
||||
'type': callback_type,
|
||||
'result': callback_result,
|
||||
'staffId': staff_id,
|
||||
'processCode': process_code,
|
||||
'remark': remark,
|
||||
}
|
||||
|
||||
if not process_instance_id:
|
||||
result = {'success': False, 'message': 'Missing processInstanceId'}
|
||||
else:
|
||||
# Call the handle_dingtalk_callback function registered via load_dingdingflow()
|
||||
callback_result_data = await handle_dingtalk_callback(callback_data)
|
||||
result = callback_result_data
|
||||
|
||||
except Exception as e:
|
||||
result = {'success': False, 'message': f'Callback processing error: {str(e)}'}
|
||||
|
||||
return result
|
||||
Loading…
x
Reference in New Issue
Block a user