初始化项目

This commit is contained in:
zwf
2026-06-02 17:46:38 +08:00
commit 646a4d02c0
240 changed files with 33662 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
"""
OA 系统对接模块。
"""
class PushException(Exception):
"""
推送异常,用于发给OA系统。
"""
def __init__(self, message, flow_token=None, return_code=None):
super().__init__(message)
self.flow_token = flow_token
self.return_code = return_code
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+87
View File
@@ -0,0 +1,87 @@
"""
OA 对接 API 基础功能。
"""
import asyncio
import datetime
import json
import apps
import dock
from paste.core import config
from paste.core.logging import echo_log
ApiUrl = config.get_config(f'dock.oa.env.{apps.get_active_env()}.api_url')
"""
对接 API 根目录。
"""
TokenPlatform = config.get_config(f'dock.oa.env.{apps.get_active_env()}.token_platform')
"""
OA Token 平台。
"""
async def new_api_request(api_url: str, request_body: dict, method: str = 'POST',
timeout: float = dock.DEFAULT_TIMEOUT, use_form: bool = False, headers: dict = None):
"""
构造一个 API 请求对象
:param api_url: API 地址,以斜杠开头的 URI 地址,非完整 URL
:param request_body: 请求体,即所有请求参数
:param method: 请求提交方式
:param timeout: 超时时长
:param use_form: 是否使用表单(Form)方式提交
:param headers: 头数据,最高优先级
:return: HTTPRequest 对象
"""
# Token
from dock.oa import oa_security
token = await oa_security.get_token(TokenPlatform)
# 构建扩展头
user_agent, browser_ver, os_name = dock.get_random_user_agent()
extra_headers = {
'Content-Type': 'application/json',
'Token': token,
'User-Agent': user_agent,
}
if headers is not None:
extra_headers = {**extra_headers, **headers}
try:
echo_log(json.dumps(request_body))
except Exception:
echo_log(str(request_body))
# 构造请求对象
request = dock.new_http_request(
url=f"{ApiUrl}{api_url}",
body=request_body,
method=method,
timeout=timeout,
use_form=use_form,
extra_headers=extra_headers,
)
return request
# 使用 asyncio.Lock 保证线(协)程安全
_lock = asyncio.Lock()
# _cache 作为内存缓存,结构 {date: counter}
_cache = {}
async def generate_serial_number():
"""
取得当日流水号。
:return: 流水号字符串
"""
today = datetime.datetime.now().strftime("%Y%m%d")
async with _lock:
if today not in _cache:
_cache[today] = 1
else:
_cache[today] += 1
counter = _cache[today]
return f"{today}{counter:05d}"
+469
View File
@@ -0,0 +1,469 @@
"""
创建 OA 接口请求对象。
对应文档接口:7、推送附件信息
"""
import io
import os
from typing import Union
from dock.oa import oa_api
from paste.core.logging import echo_log
from paste.util import uimg
async def get_push_order_request(dcm_tasks: list):
"""
取得推送待办工单列表的请求对象。
对应文档接口:2、推送待办工单列表。
接口文档说明:接收数字城管待办列表数据,并保存到 OA 系统。
接口请求方式:POST
接口返回格式:JSON
Args:
dcm_tasks: 待办工单列表,工单对象须包含以下键值:
- gdId (str): 待办工单ID,雪花ID。
- attachmentList (list[dict]): 附件列表
- taskNum (str): 任务号
- otherTaskNum (str): 第三方任务号
- bundleDeadlineTimeStr (str): 捆绑截止时间
- rollbackDeadlineStr (str): 拒绝超时截止时间
- eventSrcName (str): 问题来源
- recTypeName (str): 案件类型
- eventTypeName (str): 问题类型
- mainTypeName (str): 大类名称
- subTypeName (str): 小类名称
- urgencyLevel (str): 紧急程度
- eventDesc (str): 问题描述
- address (str): 地址描述
- disposalTimeLimit (str): 处置时限
- districtName (str): 所属区域
- newInstCondName (str): 立案条件
- closingConditions (str): 结案条件
- reporterName (str): 举报人
- reporterContact (str): 举报电话
- replyIntime (str): 是否两小时回复
- firstDepartName (str): 一级专业部门
- secondDepartName (str): 二级专业部门
- bundleWarningTimeStr (str): 捆绑警告时间
- actArdStateName (str): 阶段授权状态
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/digitalCM/pushWaitingSignatureOrder"
request_no = await oa_api.generate_serial_number()
request_body = {
"requestNo": request_no,
"toDoList": dcm_tasks,
}
# 构造 API 请求
return await oa_api.new_api_request(api_url, request_body)
async def get_upload_request(file: Union[str, io.IOBase], file_name: str, first_save: bool = True):
"""
取得文件上传接口的请求对象。
对应文档接口:3、文件上传接口。
接口文档说明:上传指定的文件,返回文件在服务器上的id。
接口请求方式:POST
接口返回格式:JSON
Args:
file: 文件路径、URL 或文件对象,支持三种输入类型:
- str (本地文件路径): 如 "/tmp/file.pdf",先从本地文件读取,再写入请求对象
- str (远程 URL): 如 "https://example.com/file.pdf",先同步从远程地址下载,然后再写入请求对象
- io.IOBase: 如 open(..., 'rb') 或 io.BytesIO
file_name: 文件名
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = "/attachment?applicationCategory=66"
if first_save:
api_url += '&firstSave=true'
# 如果是远程 URL,先下载内容
if isinstance(file, str) and (file.startswith("http://") or file.startswith("https://")):
echo_log(f"正在从 URL 下载附件: {file}")
response, content_type = uimg.fetch_image(file) # 获取二进制内容
file_content = b''.join(response.iter_content(1024))
file_obj = io.BytesIO(file_content)
# 如果是本地文件路径,打开为二进制流
elif isinstance(file, str) and os.path.exists(file):
echo_log(f"正在从本地路径读取附件: {file}")
with open(file, 'rb') as f:
file_obj = io.BytesIO(f.read())
# 如果是文件对象(如 BytesIO, BufferedReader),直接使用
elif hasattr(file, 'read') and callable(file.read):
echo_log("正在使用传入的文件对象")
file_obj = io.BytesIO(file.read())
else:
raise TypeError(f"不支持的文件类型:{type(file)},应提供:文件路径,URL或文件对象.")
request_body = {
file_name: file_obj,
}
# 这里启用了 multipart/form-data 上传
return await oa_api.new_api_request(
api_url=api_url,
request_body=request_body,
method='POST',
use_form=True,
)
async def get_download_request(media_id: str):
"""
取得文件下载接口的请求对象。
对应文档接口:4、文件下载接口。
接口文档说明:上传指定的文件,返回文件在服务器上的id。
接口请求方式:POST
接口返回格式:application/octet-stream;charset=UTF-8
Args:
media_id: 文件 Media ID。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/attachment/file/{media_id}"
return await oa_api.new_api_request(api_url=api_url, request_body={}, method='GET')
async def get_push_order_detail_request(**kwargs):
"""
取得推送工单详情的请求对象。
对应文档接口:5、推送工单详情。
接口文档说明:接收数字城管工单详情。
接口请求方式:POST
接口返回格式:JSON
Args:
**kwargs: 请求参数,须包含以下键值:
- gdId (str): 待办工单ID,雪花ID。
- partCode (str): 部件编码。
- funcLimitChar (str): 小类时限。
- reporterName (str): 举报人。
- mediaUploadTotalNum (str): 上传附件数。
- returnVisitFlag (str): 是否回访。
- undertakeUserName (str): 承办人员。
- violationTaskNoDd (str): 市容违规任务号。
- telReply (str): 回访电话。
- funcForbidReporterInfoFlag (str): 是否公开。
- dealPersonOrg (str): 承办部门。
- contactNumberDd (str): 联系电话。
- reportNumberDd (str): 举报电话。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/digitalCM/pushOrderDetail"
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_push_process_info_request(**kwargs):
"""
取得推送办理经过的请求对象。
对应文档接口:6、推送办理经过。
接口文档说明:接收数字城管工单办理经过,保存到子表。
接口请求方式:POST
接口返回格式:JSON
Args:
**kwargs: 请求参数,须包含以下键值:
- gdId (str): 待办工单ID,雪花ID。
- checkContent (str): 核查内容,拼接第一条办理经过得到
- handlingProcessList (list[dict]): 办理经过列表,每个元素须包含以下字段:
- id (str): 唯一标识符,雪花ID。
- actionTime (str): 操作时间,格式为 yyyy-MM-dd HH:mm:ss。
- actDefName (str): 实际操作名称。
- humanName (str): 经办人姓名。
- unitName (str): 办理部门名称。
- actionName (str): 操作类型名称。
- nextActDefName (str): 下一环节名称。
- detail (str): 操作意见或备注内容。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/digitalCM/pushProcessLog"
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_push_attachment_request(**kwargs):
"""
取得推送附件信息的请求对象。
对应文档接口:7、推送附件信息。
接口文档说明:接收数字城管工单附件信息列表。
接口请求方式:POST
接口返回格式:JSON
Args:
**kwargs: 请求参数,须包含以下键值:
- gdId (str): 待办工单ID,雪花ID。
- attachmentList (list[dict]): 附件列表,每个元素须包含以下字段:
- id (str): 唯一标识符,雪花ID。
- mediaId (str): 媒体资源的唯一标识符,上传文件后,从 OA 平台取得,对应响应为:fileUrl。
- mediaUsage (str): 使用场景,例如:上报、回退。
- actDefName (str): 流程节点名称,例如:各区平台、一级专业部门、二级专业部门。
- uploadCreateTime (str): 附件上传时间,可选。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/digitalCM/pushAttachmentInfo"
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_push_more_info_request(**kwargs):
"""
取得推送更多信息的请求对象。
对应文档接口:8、推送更多信息。
接口文档说明:接收数字城管工单更多信息列表。
接口请求方式:POST
接口返回格式:JSON
Args:
**kwargs: 请求参数,须包含以下键值:
- gdId (str): 待办工单ID,雪花ID。
- moreInfoList (list[dict]): 更多信息列表,每个元素须包含以下字段:
- id (str): 唯一标识符,雪花ID。
- content (str): 内容。
- time (str): 时间。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/digitalCM/pushMoreInfo"
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_push_extend_info_request(**kwargs):
"""
取得推送扩展信息的请求对象。
对应文档接口:9、推送扩展信息。
接口文档说明:接收数字城管工单扩展信息列表。
接口请求方式:POST
接口返回格式:JSON
Args:
**kwargs: 请求参数,须包含以下键值:
- gdId (str): 待办工单ID,雪花ID。
- extendList (list[dict]): 扩展信息列表,每个元素须包含以下字段:
- id (str): 唯一标识符,雪花ID。
- fieldName: 属性。
- fieldValue: 值。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = '/externalWorkOrder/digitalCM/pushExtendInfo'
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_result_notify_request(flow_token: str, message: str, return_code: int):
"""
取得上报单条工单的操作结果的请求对象。
对应文档接口:10、上报单条工单的操作结果。
接口文档说明:皓凯平台调用接口推送接口处理结果。
接口请求方式:POST
接口返回格式:JSON
Args:
flow_token (str): 工作流令牌
message (str): 接口调用返回说明
return_code (int): 操作类型,相关值说明如下:
- 1: 成功。
- 2: 回退,超过3次,超过3次失败人为干预(3)。
- 3: 人为干预。
- 4: 失败。
- 5: 停止。
- 6: 取消。
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f'/flow/notification/{flow_token}'
# 请求体参数
request_body = {
"message": message,
"returnCode": return_code
}
# 构造 API 请求
return await oa_api.new_api_request(api_url, request_body)
async def get_sign_task_request(task_id: Union[str, int]):
"""
取得签收工单的请求对象。
对应文档接口:11、签收。
接口文档说明:皓凯平台调用接口实现签收工单。
接口请求方式:POST
接口返回格式:JSON
Args:
task_id: 待办工单ID,雪花ID
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f'/externalWorkOrder/digitalCM/gdSign?gdId={task_id}'
# 构造 API 请求
return await oa_api.new_api_request(api_url, {})
async def get_update_process_delay_request(task_id: str, bundle_deadline_time_str: str, rollback_deadline_str: str):
"""
取得更新流程延期信息的请求对象。
对应文档接口:12、更新流程延期信息。
接口文档说明:更新流程延期信息。
接口请求方式:POST
接口返回格式:JSON
Args:
task_id (str): 待办工单ID,雪花ID。
bundle_deadline_time_str (str): 捆绑截止时间,格式:yyyy-MM-dd HH:mm:ss
rollback_deadline_str (str): 拒绝超时截止时间,格式:yyyy-MM-dd HH:mm:ss
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
api_url = '/externalWorkOrder/digitalCM/updateProcessDelayInfo'
request_body = {
'gdId': str(task_id),
'bundleDeadlineTimeStr': bundle_deadline_time_str,
'rollbackDeadlineStr': rollback_deadline_str
}
# 构造 API 请求
return await oa_api.new_api_request(api_url, request_body)
async def get_push_govs_order_master_request(govs_tasks: list):
"""
获取推送12345待签收工单到OA的请求
对应文档接口:2、推送待签收工单列表
接口文档说明:皓凯平台调用接口推送待签收工单列表给OA
接口请求方式:POST
接口返回格式:JSON
Args:
govs_tasks: 待签收工单列表
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = '/externalWorkOrder/pushWaitingSignatureOrder'
# 构造 API 请求
request_no = await oa_api.generate_serial_number()
request_body = {
"requestNo": request_no,
"toBeSignedList": govs_tasks,
}
# 构造 API 请求
return await oa_api.new_api_request(api_url, request_body)
async def get_push_govs_order_detail_request(**kwargs):
"""
取得推送12345工单详情的请求对象。
对应文档接口:5、推送工单详情。
接口文档说明:接收12345工单详情。
接口请求方式:POST
接口返回格式:JSON
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = '/externalWorkOrder/pushOrderDetail'
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_push_govs_process_request(**kwargs):
"""
取得推送办理经过的请求对象。
对应文档接口:6、推送工单处理流程列表。
接口文档说明:接收12345工单办理经过,保存到子表。
接口请求方式:POST
接口返回格式:JSON
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/pushProcessLog"
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_push_gov_process_request(**kwargs):
"""
取得推送省12345办理经过的请求对象。
对应文档接口:6、推送工单处理流程列表。
接口文档说明:接收省12345工单办理经过,保存到子表。
接口请求方式:POST
接口返回格式:JSON
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f"/externalWorkOrder/pushProcessLog"
# 构造 API 请求
return await oa_api.new_api_request(api_url, kwargs)
async def get_sign_govs_task_request(task_id: Union[str, int]):
"""
取得签收工单的请求对象。
对应文档接口:11、签收。
接口文档说明:皓凯平台调用接口实现签收工单。
接口请求方式:POST
接口返回格式:JSON
Args:
task_id: 待办工单ID,雪花ID
Returns:
HTTPRequest: 构造好的 HTTP 请求对象,用于后续异步调用。
"""
# 接口地址
api_url = f'/externalWorkOrder/gdSign?gdId={task_id}'
# 构造 API 请求
return await oa_api.new_api_request(api_url, {})
+55
View File
@@ -0,0 +1,55 @@
"""
上报 D3I 与 DCM 接口对接结果。
对应文档接口:10、上报单条工单的操作结果
"""
import asyncio
import json
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
from dock.oa import oa_api_request
from paste.core.logging import echo_log
from paste.util import udict
from paste.web import requests
async def after_result_notify_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
上报工单操作结果响应后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
body = response.body.decode()
echo_log(body)
body_data = json.loads(body)
code = udict.get_by_path(body_data, 'code')
message = udict.get_by_path(body_data, 'msg')
if code == 200:
echo_log(f"上报工单操作结果成功.")
else:
echo_log(f"上报工单操作结果失败:{message}")
if retry_queue:
echo_log(f"上报工单操作结果重试队列中有:{retry_queue.qsize()} 个请求在等待.")
async def push_result_notify(flow_token: str, message: str, return_code: int):
"""
操作工单完成后,推送处理结果
:param flow_token:OA调用本项目接口时提供
:param message:接口调用返回说明
:param return_code:操作类型
"""
echo_log(f"正在准备推送工单操作结果...")
request = await oa_api_request.get_result_notify_request(flow_token, message, return_code)
push_queue = asyncio.Queue()
await push_queue.put(request)
await requests.async_concurrency(
push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT,
after_request=after_result_notify_request
)
echo_log(f"推送工单操作结果完成...")
+96
View File
@@ -0,0 +1,96 @@
"""
安全模块。
"""
import asyncio
import json
from tornado.httpclient import HTTPResponse, HTTPRequest
import apps
import dock
from dock.oa import oa_api
from models.token import TokenModel
from paste.core import config
from paste.core.logging import echo_log
from paste.util import udict
from paste.web import requests
async def login():
"""
登录 DCM 系统并获取认证 Token 和响应数据。
Args:
无参数。
Returns:
tuple: 包含两个元素的元组:
- str: 请求头中的 Token 字符串(如 "JSESSIONID=abc; ..."
- dict: DCM 接口返回的完整 JSON 响应数据
Raises:
AssertionError: 登录失败(`token` 为 '-1'
ValueError: 响应体非合法 JSON
HTTPError: 网络请求失败(由 `async_request` 抛出)
"""
_username = config.get_config(f"dock.oa.env.{apps.get_active_env()}.username")
_password = config.get_config(f"dock.oa.env.{apps.get_active_env()}.password")
_login_name = config.get_config(f"dock.oa.env.{apps.get_active_env()}.login_name")
login_url = f"{oa_api.ApiUrl}/token/{_username}/{_password}?loginName={_login_name}"
# 构建扩展头
user_agent, browser_ver, os_name = dock.get_random_user_agent()
extra_headers = {
'User-Agent': user_agent,
}
# 构造请求
request_body = {}
# 构造请求对象
request = dock.new_http_request(
url=login_url,
body=request_body,
method='GET',
timeout=dock.DEFAULT_TIMEOUT,
use_form=True,
extra_headers=extra_headers,
)
queue = asyncio.Queue()
await queue.put(request)
await requests.async_concurrency(
queue, con_count=1, retry=dock.MAX_RETRY_COUNT,
after_request=after_login
)
async def after_login(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
response_body = response.body.decode()
response_data = json.loads(response_body)
token = udict.get_by_path(response_data, 'id', '')
if token and token != '-1':
await TokenModel.refresh(platform=f'{oa_api.TokenPlatform}', token=token)
echo_log(f"成功刷新 OA 登录令牌.")
else:
echo_log(f"OA 登录失败,无法刷新令牌,响应:{response_body}")
if retry_queue:
echo_log(f"登录重试队列中有:{retry_queue.qsize()} 个请求在等待.")
return response_data, token
async def get_token(platform: str = 'OA'):
"""
取得可用 Cookies。
:param platform: 要查询的平台,默认是:OA
:return: Cookies 字符串
"""
_token = await TokenModel.find_by_platform(platform)
return _token.token
if __name__ == "__main__":
from paste.core import aio_pool
_runner = aio_pool.get_aio_runner()
_runner(login())