初始化项目

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
+3
View File
@@ -0,0 +1,3 @@
"""
省12345对接模块。
"""
Binary file not shown.
Binary file not shown.
Binary file not shown.
+61
View File
@@ -0,0 +1,61 @@
"""
省12345对接 API 基础功能。
"""
from tornado.httpclient import AsyncHTTPClient
import dock
from paste.core import config
ApiUrl = "http://172.26.192.104/api"
"""
对接 API 根目录。
"""
ProxyConfig = config.get_config('dock.govs.proxy')
"""
代理服务器配置。
"""
if ProxyConfig and ProxyConfig.get('proxy_host', None) and ProxyConfig.get('proxy_port', None):
# 切换到底层实现,以便代理服务器生效
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
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.govs import govs_security
token = await govs_security.get_token()
# 构建扩展头
user_agent, browser_ver, os_name = dock.get_random_user_agent()
extra_headers = {
"Authorization": f"Bearer {token}",
'Content-Type': 'application/json; charset=UTF-8',
'User-Agent': user_agent,
}
if headers is not None:
extra_headers = {**extra_headers, **headers}
# 构造请求对象
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,
** ProxyConfig
)
return request
+113
View File
@@ -0,0 +1,113 @@
import asyncio
import logging
import json
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import apps
from dock.govs import govs_api
from dock.oa import oa_result_notify, PushException
from models.govs_order_master import GovsOrderMaster
from models.govs_create_delay import GovsApplicationForDelay
from paste.core.logging import echo_log
from paste.web import requests
async def get_create_delay_request(govs_delay: GovsApplicationForDelay, govs_order: GovsOrderMaster):
"""
创建申请延期请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param govs_delay: 申请延期对象
:param govs_order: 工单对象
:return: HTTPRequest 对象
"""
api_url = '/orderhandler/OrderDelayApply/createOrderDelay'
body = {
"finallyTimeAfterApprove": govs_delay.finally_time_after_approve,
"finallyTimeBeforeApprove": govs_delay.finally_time_before_approve,
"requestDelay": govs_delay.request_delay,
"isNatureDay": govs_delay.is_nature_day,
"alreadyNotifyOrderUser": govs_delay.already_notify_order_user,
"requestReason": govs_delay.request_reason,
"remarks": govs_delay.remarks,
"contactName": govs_delay.contact_name,
"contactTime": govs_delay.contact_time,
"contactType": govs_delay.contact_type,
"contactTypeName": govs_delay.contact_type_name,
"replyScript": govs_delay.reply_script,
"fileList": [],
"masterId": govs_order.master_id,
"orderNo": govs_order.order_no,
"processInstanceId": govs_order.process_instance_id,
"requestDelayTime": govs_delay.request_delay_time,
"id": "",
"orderId": govs_order.order_id
}
return await govs_api.new_api_request(api_url, body)
async def after_create_delay_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
提交省12345后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
echo_log(response.body.decode())
echo_log('申请延期请求成功.')
async def create_delay(govs_delay: GovsApplicationForDelay, govs_order: GovsOrderMaster):
"""
推送申请延期请求。
:param govs_delay: 保存在数据库的申请延期对象
:param govs_order: 数据库中的工单对象
"""
try:
delay_request = await get_create_delay_request(govs_delay, govs_order)
queue = asyncio.Queue()
await queue.put(delay_request)
# 仅生产环境真实提交,其他环境不实际提交
if apps.get_active_env() not in ('dev', '', None):
delay_response_list = await requests.async_concurrency(queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT,
after_request=after_create_delay_request)
# 检查申请延期响应是否成功
if len(delay_response_list) != 1:
raise PushException("申请延期请求发生错误.", govs_delay.flow_token, 3)
return_response = delay_response_list[0]
return_response_data = return_response.body.decode()
return_response_data = json.loads(return_response_data)
if return_response_data.get('code') != 200:
raise PushException("申请延期请求发生错误.", govs_delay.flow_token, 3)
else:
echo_log(f"非生产环境,不实际提交.")
# 保存成功状态
govs_delay.status = 1
await govs_delay.async_save()
# 申请延期请求提交后,通知申请延期成功
await oa_result_notify.push_result_notify(
govs_delay.flow_token,
'申请延期成功',
1
)
except PushException as e:
# 任何异常都意味着失败,通知 OA
echo_log(f'申请延期发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
# 保存失败状态
govs_delay.status = 0
await govs_delay.async_save()
# 申请延期发生异常,通知申请延期失败
await oa_result_notify.push_result_notify(
e.flow_token, f"{e}", e.return_code
)
except Exception as e:
# 其他异常都意味着失败,通知 OA
echo_log(f'申请延期发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
+301
View File
@@ -0,0 +1,301 @@
import asyncio
import logging
import os
import io
import json
import time
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import apps
from dock.govs import govs_api, govs_upload_file
from dock.oa import oa_result_notify, oa_api_request, PushException
from models.govs_order_master import GovsOrderMaster
from models.govs_create_return import GovsWorkOrderReturnFormal
from paste.core.logging import echo_log
from paste.web import requests
from paste.util.ufile import inspect_type
async def get_create_return_request(govs_return: GovsWorkOrderReturnFormal, govs_order: GovsOrderMaster,
file_list: list = None):
"""
创建申请退回请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param govs_return: 申请退回对象
:param govs_order: 工单对象
:param file_list: 已上传到省12345的文件列表数据
:return: HTTPRequest 对象
"""
api_url = '/orderhandler/sendBackApply/saveSendBackApply'
body = {
"slaveDeptIsCompetent": [],
"duties": "",
"nextOrgIds": "",
"adviceMasterOrgName": "",
"adviceSlaveOrgNames": "",
"searchValueList": [],
"inforRetrieval": "",
"nextProcessing": "",
"assignedUnit": "",
"duration": "",
"dateChoose": "",
"plannedDuration": "",
"completionTime": "",
"returnAuditorName": govs_return.return_auditor_name,
'returnAuditorId': govs_return.return_auditor_id,
"handlingSuggestion": "",
"remark": govs_return.remark,
"fileList": [],
"isContact": "",
"contactName": "",
"contactTime": "",
"contactType": "",
"nextFeedbackTime": "",
"advice": "",
"answer": "",
"applyReason": "",
"applyBasis": "",
"platformOpinion": "",
"shortMessage": "",
"distributor": "",
"distributors": [],
"positionSelection": "",
"difficultReason": "",
"directCompletionType": "",
"applyType": "",
"returnReason": govs_return.return_reason_name,
"returnReasonName": govs_return.return_reason_name,
"replyResult": "",
"informPublic": "",
"approveAttachmentIds": "",
"formalReply": "",
"flowMap": {
"nextHandleName": "工单退回",
"nextHandle": "工单退回"
},
"adviceMasterOrgId": "",
"adviceSlaveOrgIdsList": [],
"id": "",
"key": "",
"nextHandler": "",
"nextOrgId": "",
"processInstanceId": govs_order.process_instance_id,
"reason": govs_return.reason,
"taskHandlerId": "",
"value": "",
"vote": "",
"assignedUnitList": [],
"assignedUnitLabel": "",
"nextOrgIdList": [],
"nextOrgIdStr": "",
"knowledgeQuote": "[]",
"defineAuditorId": "",
"defineAuditorName": "",
"visitTypes": " ",
"appeal1": "",
"appeal2": "",
"unreasonableDemands": "",
"complainant": "",
"pollutionType": "",
"pollutionType1": "",
"pollutionType2": "",
"involvedTargets": "",
"problemCategory": "生态环境类",
"defendantType": "",
"reportingPurpose": "投诉举报",
"industryType": "",
"industryType1": "",
"industryType2": "",
"complainants": [
{
"complainant": "",
"region": "",
"street": "",
"detailedAddress": "",
"show": True,
"disabled": False
}
],
"inforAddress": "",
"associatedDefendantType": "",
"adminLawEnf": "",
"approveResult": "",
"approveContent": "",
"nextOrgIdsName": [],
"noticeOrgId": "",
"completeType": "",
"caseAccordTypeOneName": govs_order.case_accord_type_one_name,
"caseAccordTypeTwoName": govs_order.case_accord_type_two_name,
"caseAccordTypeThreeName": govs_order.case_accord_type_three_name,
"caseAccordTypeFourName": "",
"caseAccordTypeFiveName": "",
"fileVos": [],
"dealOpinion": govs_return.deal_opinion,
"actionName": govs_return.action_name,
"orderId": govs_order.order_id,
"taskId": govs_order.next_task_id,
"submitType": "0",
"adviceSlaveOrgIds": "",
"masterId": str(govs_return.master_id),
"orderNo": govs_order.order_no,
}
if govs_return.return_auditor_name and file_list:
body['fileList'] = file_list
body['fileVos'] = file_list
return await govs_api.new_api_request(api_url, body)
async def after_create_return_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
提交省12345后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
echo_log(response.body.decode())
echo_log('申请退回请求成功.')
async def done_file_download(response_list: list[HTTPResponse]):
"""
所有附件下载完成执行的处理程序。
:param response_list: 附件下载响应列表
:return: 返回附件字典列表,每个元素包含文件名和io对象
"""
file_info_list = []
for response in response_list:
file_type = inspect_type(response.body)
basename = os.path.basename(response.request.url)
file_io = io.BytesIO(response.body)
file_info_list.append({
'file_name': f'{basename}.{file_type}',
'file_io': file_io
})
return file_info_list
async def done_file_upload(response_list: list[HTTPResponse]):
"""
文件上传完成后的处理程序
:param response_list: 附件上传响应列表
:return: 返回上次后的文件信息列表,包含文件名、文件路径
"""
uploaded_list = []
for response in response_list:
response_body = response.body.decode()
response_data = json.loads(response_body)
if response_data['msg'] == '附件上传成功!':
uploaded_list.append({
'file_name': getattr(response.request, 'file_name', 'file.bin'),
'path': response_data['data']
})
else:
echo_log(f'文件上传到省12345失败,{response_data}')
return uploaded_list
async def download_and_upload_files(file_id_str: str):
"""
从OA下载文件,上传到省12345,返回上传后的文件信息列表
:param file_id_str: 英文逗号分隔的OA文件id
"""
file_id_list = file_id_str.strip(',').split(',')
download_queue = asyncio.Queue()
for file_id in file_id_list:
download_request = await oa_api_request.get_download_request(file_id)
await download_queue.put(download_request)
file_info_list = await requests.async_concurrency(download_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_done=done_file_download)
upload_queue = asyncio.Queue()
for file_info in file_info_list:
upload_request = await govs_upload_file.get_upload_request(file_info['file_name'], file_info['file_io'])
setattr(upload_request, 'file_name', file_info['file_name'])
await upload_queue.put(upload_request)
uploaded_list = await requests.async_concurrency(upload_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_done=done_file_upload)
return uploaded_list
async def create_return(govs_return: GovsWorkOrderReturnFormal, govs_order: GovsOrderMaster):
"""
推送申请退回请求。
:param govs_return: 保存在数据库的申请退回对象
:param govs_order: 数据库中的工单对象
"""
try:
# 仅生产环境真实提交,其他环境不实际提交
if apps.get_active_env() not in ('dev', '', None):
if govs_return.file_id_str:
# 根据OA传过来的文件id,下载并上传到省12345
file_info_list = await download_and_upload_files(govs_return.file_id_str)
file_info_list = [{
"name": info['file_name'],
"filePath": info['path'],
"orderId": govs_order.order_id,
"uid": int(time.time() * 1000),
"status": "success"
} for info in file_info_list]
else:
file_info_list = None
return_request = await get_create_return_request(govs_return, govs_order, file_info_list)
queue = asyncio.Queue()
await queue.put(return_request)
return_response_list = await requests.async_concurrency(queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT,
after_request=after_create_return_request)
# 检查申请退回响应是否成功
if len(return_response_list) != 1:
raise PushException("申请退回请求发生错误.", govs_return.flow_token, 3)
return_response = return_response_list[0]
return_response_data = return_response.body.decode()
return_response_data = json.loads(return_response_data)
if return_response_data.get('code') != 200 or '退回申请提交成功' not in return_response_data.get('data'):
raise PushException("申请退回请求发生错误.", govs_return.flow_token, 3)
else:
echo_log(f"非生产环境,不实际提交.")
# 保存成功状态
govs_return.status = 1
await govs_return.async_save()
# 申请退回请求提交后,通知申请退回成功
await oa_result_notify.push_result_notify(
govs_return.flow_token,
'申请退回成功',
1
)
except PushException as e:
# 任何异常都意味着失败,通知 OA
echo_log(f'申请退回发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
# 保存失败状态
govs_return.status = 0
await govs_return.async_save()
# 申请退回发生异常,通知申请退回失败
await oa_result_notify.push_result_notify(
e.flow_token, f"{e}", e.return_code
)
except Exception as e:
# 其他异常都意味着失败,通知 OA
echo_log(f'申请退回发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
if __name__ == '__main__':
async def push():
task = await GovsOrderMaster.async_find_by_id(2060477579990339586)
return_request = await GovsWorkOrderReturnFormal.async_find_by_id(2061344445634318336)
await create_return(return_request, task)
from paste.core import aio_pool
_runner = aio_pool.get_aio_runner()
_runner(push())
+306
View File
@@ -0,0 +1,306 @@
import asyncio
import os
import io
import json
import time
import logging
from datetime import datetime
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import apps
from models.govs_create_reply import GovsReplyFormal
from models.govs_order_master import GovsOrderMaster
from dock.govs import govs_api, govs_upload_file
from dock.oa import oa_api_request, oa_result_notify, PushException
from paste.core.logging import echo_log
from paste.util.ufile import inspect_type
from paste.web import requests
async def get_reply_request(govs_reply: GovsReplyFormal, govs_order: GovsOrderMaster, file_list: list = None):
"""
创建答复办结请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param govs_reply: 答复办结对象
:param govs_order: 工单对象
:param file_list: 已上传到省12345的文件列表数据
:return: HTTPRequest 对象
"""
api_url = '/workflow/approveTask/orderApprove'
# 默认不加前缀
prefix = ""
# 只有 contact_name 和 contact_time 都存在时才拼接前缀
if govs_reply.contact_name and govs_reply.contact_time:
# 格式化时间
try:
dt = datetime.fromisoformat(govs_reply.contact_time.replace('Z', '+00:00'))
formatted_contact_time = dt.strftime('%Y-%m-%d %H:%M')
except Exception:
formatted_contact_time = govs_reply.contact_time
prefix = f"您好,{govs_reply.contact_name}{formatted_contact_time}通过{govs_reply.contact_type}方式联系您;"
body = {
"slaveDeptIsCompetent": [],
"duties": "",
"nextOrgIds": "",
"adviceMasterOrgName": "",
"adviceSlaveOrgNames": "",
"searchValueList": [],
"inforRetrieval": "",
"nextProcessing": "",
"assignedUnit": "",
"duration": "",
"dateChoose": "",
"plannedDuration": "",
"completionTime": "",
"returnAuditorName": "",
"handlingSuggestion": "",
"remark": govs_reply.remarks,
"fileList": file_list,
"isContact": "",
"contactName": govs_reply.contact_name,
"contactTime": govs_reply.contact_time,
"contactType": govs_reply.contact_type,
"nextFeedbackTime": "",
"advice": prefix + (govs_reply.advice or ""),
"answer": "",
"applyReason": "",
"applyBasis": "",
"platformOpinion": "",
"shortMessage": "",
"distributor": "",
"distributors": [],
"positionSelection": "",
"difficultReason": "",
"directCompletionType": "",
"applyType": "",
"returnReason": "",
"returnReasonName": "",
"replyResult": "",
"informPublic": govs_reply.is_contact, # 这个还要确认一遍
"approveAttachmentIds": "",
"formalReply": "",
"flowMap": {
"nextHandleName": "答复办结",
"nextHandle": "答复办结"
},
"adviceMasterOrgId": "",
"adviceSlaveOrgIdsList": [],
"id": govs_order.next_task_id,
"key": "",
"nextHandler": "",
"nextOrgId": "",
"processInstanceId": govs_order.process_instance_id,
"reason": prefix + (govs_reply.reason or ""),
"taskHandlerId": "",
"value": "",
"vote": "",
"assignedUnitList": [],
"assignedUnitLabel": "",
"nextOrgIdList": [],
"nextOrgIdStr": "",
"knowledgeQuote": "[]",
"defineAuditorId": "",
"defineAuditorName": "",
"visitTypes": " ",
"appeal1": "",
"appeal2": "",
"unreasonableDemands": "",
"complainant": "",
"pollutionType": "-",
"pollutionType1": "",
"pollutionType2": "",
"involvedTargets": "",
"problemCategory": "生态环境类",
"defendantType": "",
"reportingPurpose": "投诉举报",
"industryType": "-",
"industryType1": "",
"industryType2": "",
"complainants": [
{
"complainant": "",
"region": "",
"street": "",
"detailedAddress": "",
"show": True,
"disabled": False
}
],
"inforAddress": "",
"associatedDefendantType": "",
"adminLawEnf": "",
"approveResult": "",
"approveContent": "",
"nextOrgIdsName": [],
"noticeOrgId": "",
"completeType": "",
"caseAccordTypeOneName": govs_order.case_accord_type_one_name,
"caseAccordTypeTwoName": govs_order.case_accord_type_two_name,
"caseAccordTypeThreeName": govs_order.case_accord_type_three_name,
"caseAccordTypeFourName": "",
"caseAccordTypeFiveName": "",
"fileVos": file_list,
"reasonableLabels": "-",
"visitType": "",
"actionName": govs_reply.action_name,
"businessKey": govs_order.order_id,
"masterId": govs_reply.master_id,
"orderNo": govs_order.order_no
}
return await govs_api.new_api_request(api_url, body)
async def after_create_reply_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
提交省12345后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
echo_log(response.body.decode())
echo_log('答复办结请求成功.')
async def done_file_download(response_list: list[HTTPResponse]):
"""
所有附件下载完成执行的处理程序。
:param response_list: 附件下载响应列表
:return: 返回附件字典列表,每个元素包含文件名和io对象
"""
file_info_list = []
for response in response_list:
file_type = inspect_type(response.body)
basename = os.path.basename(response.request.url)
file_io = io.BytesIO(response.body)
file_info_list.append({
'file_name': f'{basename}.{file_type}',
'file_io': file_io
})
return file_info_list
async def done_file_upload(response_list: list[HTTPResponse]):
"""
文件上传完成后的处理程序
:param response_list: 附件上传响应列表
:return: 返回上次后的文件信息列表,包含文件名、文件路径
"""
uploaded_list = []
for response in response_list:
response_body = response.body.decode()
response_data = json.loads(response_body)
if response_data['msg'] == '附件上传成功!':
uploaded_list.append({
'file_name': getattr(response.request, 'file_name', 'file.bin'),
'path': response_data['data']
})
else:
echo_log(f'文件上传到省12345失败,{response_data}')
return uploaded_list
async def download_and_upload_files(file_id_str: str):
"""
从OA下载文件,上传到省12345,返回上传后的文件信息列表
:param file_id_str: 英文逗号分隔的OA文件id
"""
file_id_list = file_id_str.strip(',').split(',')
download_queue = asyncio.Queue()
for file_id in file_id_list:
download_request = await oa_api_request.get_download_request(file_id)
await download_queue.put(download_request)
file_info_list = await requests.async_concurrency(download_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_done=done_file_download)
upload_queue = asyncio.Queue()
for file_info in file_info_list:
upload_request = await govs_upload_file.get_upload_request(file_info['file_name'], file_info['file_io'])
setattr(upload_request, 'file_name', file_info['file_name'])
await upload_queue.put(upload_request)
uploaded_list = await requests.async_concurrency(upload_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_done=done_file_upload)
return uploaded_list
async def create_reply(govs_reply: GovsReplyFormal, govs_order: GovsOrderMaster):
"""
推送答复办结请求。
:param govs_reply: 保存在数据库的答复办结对象
:param govs_order: 数据库中的工单对象
"""
try:
# 仅生产环境真实提交,其他环境不实际提交
if apps.get_active_env() not in ('dev', '', None):
if govs_reply.file_id_str:
# 根据OA传过来的文件id,下载并上传到省12345
file_info_list = await download_and_upload_files(govs_reply.file_id_str)
file_info_list = [{
"name": info['file_name'],
"filePath": info['path'],
"orderId": govs_order.order_id,
"uid": int(time.time() * 1000),
"status": "success"
} for info in file_info_list]
else:
file_info_list = None
reply_request = await get_reply_request(govs_reply, govs_order, file_info_list)
queue = asyncio.Queue()
await queue.put(reply_request)
reply_response_list = await requests.async_concurrency(queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT,
after_request=after_create_reply_request)
# 检查答复办结响应是否成功
if len(reply_response_list) != 1:
raise PushException("答复办结请求发生错误.", govs_reply.flow_token, 3)
return_response = reply_response_list[0]
return_response_data = return_response.body.decode()
return_response_data = json.loads(return_response_data)
if return_response_data.get('code') != 200 or return_response_data.get('data') != 'ok':
raise PushException("答复办结请求发生错误.", govs_reply.flow_token, 3)
else:
echo_log(f"非生产环境,不实际提交.")
# 保存成功状态
govs_reply.status = 1
await govs_reply.async_save()
# 答复办结请求提交后,通知答复办结成功
await oa_result_notify.push_result_notify(
govs_reply.flow_token,
'答复办结成功',
1
)
except PushException as e:
# 任何异常都意味着失败,通知 OA
echo_log(f'答复办结发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
# 保存失败状态
govs_reply.status = 0
await govs_reply.async_save()
# 答复办结发生异常,通知答复办结失败
await oa_result_notify.push_result_notify(
e.flow_token, f"{e}", e.return_code
)
except Exception as e:
# 其他异常都意味着失败
echo_log(f'答复办结发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
if __name__ == '__main__':
async def push():
task = await GovsOrderMaster.async_find_by_id(2060173985047351297)
reply = await GovsReplyFormal.async_find_by_id(2061328980866371584)
await create_reply(reply, task)
from paste.core import aio_pool
_runner = aio_pool.get_aio_runner()
_runner(push())
+22
View File
@@ -0,0 +1,22 @@
from typing import Union
import base64
from urllib.parse import quote, urlencode
from dock.govs import govs_api
async def get_download_request(tenant_id: Union[int, str], file_url: str):
"""
创建从省12345下载文件的请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param tenant_id: 租户id
:param file_url: 文件url
"""
api_url = '/file/api/system/downloadPermission'
b64_file_url = base64.b64encode(file_url.encode()).decode()
b64_file_url = quote(b64_file_url, safe="~*'()!.-_")
body = {
'tenantId': tenant_id,
'fileUrl': b64_file_url
}
api_url += f'?{urlencode(body)}'
return await govs_api.new_api_request(api_url, {})
+281
View File
@@ -0,0 +1,281 @@
import asyncio
import os
import io
import json
import time
import logging
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import apps
from models.govs_phase_wise_completion import GovsPhaseWiseCompletion
from models.govs_order_master import GovsOrderMaster
from dock.govs import govs_api, govs_upload_file
from dock.oa import oa_api_request, oa_result_notify, PushException
from paste.core.logging import echo_log
from paste.util.ufile import inspect_type
from paste.web import requests
async def get_phase_request(phase_wise_completion: GovsPhaseWiseCompletion, govs_order: GovsOrderMaster,
file_list: list = None):
"""
创建阶段性办结请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param phase_wise_completion: 阶段性办结对象
:param govs_order: 工单对象
:param file_list: 已上传到省12345的文件列表数据
:return: HTTPRequest 对象
"""
api_url = '/orderhandler/remAndSup/savePeriodicCompletion'
body = {
"slaveDeptIsCompetent": [],
"duties": "",
"nextOrgIds": "",
"adviceMasterOrgName": "",
"adviceSlaveOrgNames": "",
"searchValueList": [],
"inforRetrieval": "",
"nextProcessing": "",
"assignedUnit": "",
"duration": "",
"dateChoose": "",
"plannedDuration": "",
"completionTime": "",
"returnAuditorName": "",
"handlingSuggestion": "",
"remark": phase_wise_completion.remark,
"fileList": file_list,
"isContact": phase_wise_completion.is_contact,
"contactName": phase_wise_completion.contact_name,
"contactTime": phase_wise_completion.contact_time,
"contactType": phase_wise_completion.contact_type,
"nextFeedbackTime": phase_wise_completion.next_feedback_time,
"advice": phase_wise_completion.advice,
"answer": "",
"applyReason": "",
"applyBasis": "",
"platformOpinion": "",
"shortMessage": "",
"distributor": "",
"distributors": [],
"positionSelection": "",
"difficultReason": "",
"directCompletionType": "",
"applyType": "",
"returnReason": "",
"returnReasonName": "",
"replyResult": "",
"informPublic": "",
"approveAttachmentIds": "",
"formalReply": "",
"flowMap": {
"nextHandleName": "阶段性办结",
"nextHandle": "阶段性办结"
},
"adviceMasterOrgId": "",
"adviceSlaveOrgIdsList": [],
"id": "",
"key": "",
"nextHandler": "",
"nextOrgId": "",
"processInstanceId": govs_order.process_instance_id,
"reason": phase_wise_completion.reason,
"taskHandlerId": "",
"value": "",
"vote": "",
"assignedUnitList": [],
"assignedUnitLabel": "",
"nextOrgIdList": [],
"nextOrgIdStr": "",
"knowledgeQuote": "[]",
"defineAuditorId": "",
"defineAuditorName": "",
"visitTypes": " ",
"appeal1": "",
"appeal2": "",
"unreasonableDemands": "",
"complainant": "",
"pollutionType": "",
"pollutionType1": "",
"pollutionType2": "",
"involvedTargets": "",
"problemCategory": "生态环境类",
"defendantType": "",
"reportingPurpose": "投诉举报",
"industryType": "",
"industryType1": "",
"industryType2": "",
"complainants": [
{
"complainant": "",
"region": "",
"street": "",
"detailedAddress": "",
"show": True,
"disabled": False
}
],
"inforAddress": "",
"associatedDefendantType": "",
"adminLawEnf": "",
"approveResult": "",
"approveContent": "",
"nextOrgIdsName": [],
"noticeOrgId": "",
"completeType": "",
"caseAccordTypeOneName": govs_order.case_accord_type_one_name,
"caseAccordTypeTwoName": govs_order.case_accord_type_two_name,
"caseAccordTypeThreeName": govs_order.case_accord_type_three_name,
"caseAccordTypeFourName": "",
"caseAccordTypeFiveName": "",
"fileVos": file_list,
"actionName": phase_wise_completion.action_name,
"orderId": govs_order.order_id,
"taskId": govs_order.next_task_id,
"submitType": "0",
"masterId": phase_wise_completion.master_id,
"orderNo": govs_order.order_no
}
return await govs_api.new_api_request(api_url, body)
async def after_phase_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
提交省12345后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
echo_log(response.body.decode())
echo_log('阶段性办结请求成功.')
async def done_file_download(response_list: list[HTTPResponse]):
"""
所有附件下载完成执行的处理程序。
:param response_list: 附件下载响应列表
:return: 返回附件字典列表,每个元素包含文件名和io对象
"""
file_info_list = []
for response in response_list:
file_type = inspect_type(response.body)
basename = os.path.basename(response.request.url)
file_io = io.BytesIO(response.body)
file_info_list.append({
'file_name': f'{basename}.{file_type}',
'file_io': file_io
})
return file_info_list
async def done_file_upload(response_list: list[HTTPResponse]):
"""
文件上传完成后的处理程序
:param response_list: 附件上传响应列表
:return: 返回上次后的文件信息列表,包含文件名、文件路径
"""
uploaded_list = []
for response in response_list:
response_body = response.body.decode()
response_data = json.loads(response_body)
if response_data['msg'] == '附件上传成功!':
uploaded_list.append({
'file_name': getattr(response.request, 'file_name', 'file.bin'),
'path': response_data['data']
})
else:
echo_log(f'文件上传到省12345失败,{response_data}')
return uploaded_list
async def download_and_upload_files(file_id_str: str):
"""
从OA下载文件,上传到省12345,返回上传后的文件信息列表
:param file_id_str: 英文逗号分隔的OA文件id
"""
file_id_list = file_id_str.strip(',').split(',')
download_queue = asyncio.Queue()
for file_id in file_id_list:
download_request = await oa_api_request.get_download_request(file_id)
await download_queue.put(download_request)
file_info_list = await requests.async_concurrency(download_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_done=done_file_download)
upload_queue = asyncio.Queue()
for file_info in file_info_list:
upload_request = await govs_upload_file.get_upload_request(file_info['file_name'], file_info['file_io'])
setattr(upload_request, 'file_name', file_info['file_name'])
await upload_queue.put(upload_request)
uploaded_list = await requests.async_concurrency(upload_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_done=done_file_upload)
return uploaded_list
async def create_phase_wise_completion(phase_wise_completion: GovsPhaseWiseCompletion, govs_order: GovsOrderMaster):
"""
推送阶段性办结请求。
:param phase_wise_completion: 保存在数据库的阶段性办结对象
:param govs_order: 数据库中的工单对象
"""
try:
# 仅生产环境真实提交,其他环境不实际提交
if apps.get_active_env() not in ('dev', '', None):
if phase_wise_completion.file_id_str:
# 根据OA传过来的文件id,下载并上传到省12345
file_info_list = await download_and_upload_files(phase_wise_completion.file_id_str)
file_info_list = [{
"name": info['file_name'],
"filePath": info['path'],
"orderId": govs_order.order_id,
"uid": int(time.time() * 1000),
"status": "success"
} for info in file_info_list]
else:
file_info_list = None
phase_request = await get_phase_request(phase_wise_completion, govs_order, file_info_list)
queue = asyncio.Queue()
await queue.put(phase_request)
phase_response_list = await requests.async_concurrency(queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT,
after_request=after_phase_request)
# 检查阶段性办结响应是否成功
if len(phase_response_list) != 1:
raise PushException("阶段性办结请求发生错误.", phase_wise_completion.flow_token, 3)
return_response = phase_response_list[0]
return_response_data = return_response.body.decode()
return_response_data = json.loads(return_response_data)
if return_response_data.get('code') != 200:
raise PushException("阶段性办结请求发生错误.", phase_wise_completion.flow_token, 3)
else:
echo_log(f"非生产环境,不实际提交.")
# 保存成功状态
phase_wise_completion.status = 1
await phase_wise_completion.async_save()
# 阶段性办结请求提交后,通知阶段性办结成功
await oa_result_notify.push_result_notify(
phase_wise_completion.flow_token,
'阶段性办结成功',
1
)
except PushException as e:
# 任何异常都意味着失败,通知 OA
echo_log(f'阶段性办结发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
# 保存失败状态
phase_wise_completion.status = 0
await phase_wise_completion.async_save()
# 阶段性办结发生异常,通知阶段性办结失败
await oa_result_notify.push_result_notify(
e.flow_token, f"{e}", e.return_code
)
except Exception as e:
# 其他异常都意味着失败
echo_log(f'阶段性办结发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
+123
View File
@@ -0,0 +1,123 @@
import asyncio
import logging
from tornado.httpclient import HTTPResponse, HTTPRequest
from sqlalchemy import select
import dock
import apps
from dock.govs import govs_api
from dock.oa import oa_result_notify, PushException
from models.govs_order_master import GovsOrderMaster
from models.govs_save_sign import GovsSaveSign
from paste.core.logging import echo_log
from paste.web import requests
async def get_sign_request(govs_order: GovsOrderMaster):
"""
创建省12345上工单确认签收的请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param govs_order: 工单对象
:return: HTTPRequest 对象
"""
api_url = '/orderhandler/claimTask/claimTask'
body = {
"orderId": govs_order.order_id,
"orderNo": govs_order.order_no,
"masterId": govs_order.master_id,
"orderProcessId": govs_order.id,
"taskId": govs_order.next_task_id,
"flag": "签收"
}
return await govs_api.new_api_request(api_url, body)
async def after_sign_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
提交省12345后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
echo_log(response.body.decode())
govs_order = getattr(response.request, 'govs_order', None)
if govs_order:
govs_order.govs_sign = 1
await govs_order.async_save()
echo_log('省12345确认签收请求成功.')
async def sign_order(govs_sign: GovsSaveSign, govs_order: GovsOrderMaster):
"""
推送工单确认签收请求。
:param govs_sign: 保存在数据库的工单签收对象
:param govs_order: 数据库中的工单对象
"""
try:
sign_request = await get_sign_request(govs_order)
queue = asyncio.Queue()
setattr(sign_request, 'govs_order', govs_order)
await queue.put(sign_request)
# 仅生产环境真实提交,其他环境不实际提交
if apps.get_active_env() not in ('dev', '', None):
sign_response_list = await requests.async_concurrency(queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT,
after_request=after_sign_request)
# 检查工单签收响应是否成功
if len(sign_response_list) != 1:
raise PushException("工单签收请求发生错误.", govs_sign.flow_token, 3)
else:
echo_log(f"非生产环境,不实际提交.")
# 保存成功状态
govs_sign.status = 1
await govs_sign.async_save()
# 工单签收请求提交后,通知工单签收成功
await oa_result_notify.push_result_notify(
govs_sign.flow_token,
'工单签收成功',
1
)
except PushException as e:
# 任何异常都意味着失败,通知 OA
echo_log(f'工单签收发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
# 保存失败状态
govs_sign.status = 0
await govs_sign.async_save()
# 工单签收发生异常,通知工单签收失败
await oa_result_notify.push_result_notify(
e.flow_token, f"{e}", e.return_code
)
except Exception as e:
# 其他异常都意味着失败
echo_log(f'工单签收发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
async def sign_order_bypass_api(task_id_list: list):
"""
不经过工单确认签收的api接口,签收指定的工单
:param task_id_list: 工单id列表
"""
try:
query = select(GovsOrderMaster).where(GovsOrderMaster.id.in_(task_id_list))
govs_orders = await GovsOrderMaster.orm_execute(query)
sign_queue = asyncio.Queue()
for row in govs_orders.all():
sign_request = await get_sign_request(row[0])
setattr(sign_request, 'govs_order', row[0])
await sign_queue.put(sign_request)
# 仅生产环境真实提交,其他环境不实际提交
if apps.get_active_env() in ('dev', '', None):
echo_log(f"非生产环境,不实际提交.")
return
await requests.async_concurrency(sign_queue, con_count=dock.CONCURRENCY_COUNT,
retry=dock.MAX_RETRY_COUNT, after_request=after_sign_request)
except Exception as e:
echo_log(f'签收工单发生错误.', logging.ERROR)
echo_log(e, logging.ERROR, is_log_exc=True)
+106
View File
@@ -0,0 +1,106 @@
"""
数据抓取模块。
"""
import asyncio
from typing import Optional, Union
from sqlalchemy import select, desc
import dock
from dock.govs import govs_scrape_order_master, govs_scrape_order_detail, govs_scrape_order_process
from models.govs_order_master import GovsOrderMaster
from paste.core.logging import echo_log
from paste.web import requests
async def fetch_govs_task(dept_page_tag: int = 1, num_per_page: int = 60, task_id: Optional[Union[str, int]] = None):
"""
抓取待办数据及其明细数据。
:param num_per_page: 读取多少任务进行明细抓取
:param dept_page_tag: 0代表全部工单,1代表待签收工单,2代表待交办工单
:param task_id: 可选的指定的工单id
"""
echo_log(f"开始抓取待办数据...")
task_request = await govs_scrape_order_master.get_task_request(
dept_page_tag=dept_page_tag, num_per_page=num_per_page
)
request_queue = asyncio.Queue()
await request_queue.put(task_request)
await requests.async_concurrency(
request_queue, retry=dock.MAX_RETRY_COUNT,
after_request=govs_scrape_order_master.after_task_request
)
echo_log(f"待办数据抓取完成...")
# 读取任务数据,以便能对最新数据抓取详细数据
query = select(
GovsOrderMaster.id, GovsOrderMaster.order_id, GovsOrderMaster.order_no, GovsOrderMaster.tenant_id,
GovsOrderMaster.master_id, GovsOrderMaster.area_code
).order_by(
desc(GovsOrderMaster.id)
)
# 如果dept_page_tag=1,只抓取待签收的,如果dept_page_tag不是0或者1,只抓取已签收的,针对性抓取特定状态的工单数据
if dept_page_tag == 1:
query = query.where(GovsOrderMaster.govs_sign == 0)
elif dept_page_tag != 0:
query = query.where(GovsOrderMaster.govs_sign == 1)
if task_id:
if isinstance(task_id, list):
query = query.where(GovsOrderMaster.id.in_(task_id))
echo_log(f"开始抓取待办列表:{task_id} 的详细数据...")
else:
query = query.where(GovsOrderMaster.id == task_id)
echo_log(f"开始抓取待办:{task_id} 的详细数据...")
else:
echo_log(f"开始抓取前 {num_per_page} 条待办的详细数据...")
query = query.limit(num_per_page)
task_df = await GovsOrderMaster.query_as_df(query)
# 构建请求队列
detail_queue = asyncio.Queue()
process_queue = asyncio.Queue()
# 向队列中填充请求对象
echo_log(f"正在准备请求队列...")
for _h, _row in task_df.iterrows():
order_id = _row.get(GovsOrderMaster.order_id.key)
order_no = _row.get(GovsOrderMaster.order_no.key)
tenant_id = int(_row.get(GovsOrderMaster.tenant_id.key))
master_id = int(_row.get(GovsOrderMaster.master_id.key))
area_code = _row.get(GovsOrderMaster.area_code.key)
_detail_request = await govs_scrape_order_detail.get_task_request(order_id, master_id, tenant_id)
setattr(_detail_request, 'order_id', order_id)
setattr(_detail_request, 'order_no', order_no)
setattr(_detail_request, 'master_id', master_id)
setattr(_detail_request, 'tenant_id', tenant_id)
await detail_queue.put(_detail_request)
_process_request = await govs_scrape_order_process.get_task_request(
order_id, order_no, master_id, tenant_id, '1700467981117980074', area_code
)
setattr(_process_request, 'order_id', order_id)
setattr(_process_request, 'order_no', order_no)
setattr(_process_request, 'master_id', master_id)
setattr(_process_request, 'tenant_id', tenant_id)
await process_queue.put(_process_request)
echo_log(f"抓取待办详细数据...")
tasks = [
requests.async_concurrency(
detail_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT,
after_request=govs_scrape_order_detail.after_task_request
),
requests.async_concurrency(
process_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT,
after_request=govs_scrape_order_process.after_task_request
)
]
await asyncio.gather(*tasks)
if __name__ == "__main__":
from paste.core import aio_pool
_runner = aio_pool.get_aio_runner()
_runner(fetch_govs_task(dept_page_tag=1, num_per_page=50))
+159
View File
@@ -0,0 +1,159 @@
import asyncio
import json
from typing import Union
import pandas as pd
from dateutil import parser
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import models
from dock.govs import govs_api
from models.govs_order_attachment import GovsOrderAttachment
from models.govs_order_detail import GovsOrderDetail
from models.govs_order_user import GovsOrderUser
from paste.core.logging import echo_log
from paste.util import udict
from paste.web import requests
async def get_task_request(order_id: str, master_id: Union[str, int], tenant_id: Union[str, int]):
"""
获取省12345任务详情数据。
通过 POST 请求向省12345的任务详情接口提交表单数据,获取任务详情数据。
自动注入有效的 Cookie(如 JSESSIONID)至请求头,并解析返回的 JSON 响应。
Args:
order_id (str): 待办任务ID
master_id (int): 关联订单主表ID
tenant_id (int): 租户ID
"""
api_url = f"/orderreceive/orderMaster/queryOrderDetail"
request_body = {
"orderId": order_id,
"masterId": master_id,
"tenantId": tenant_id
}
# 构造 API 请求
return await govs_api.new_api_request(api_url, request_body)
async def after_task_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
任务请求响应后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
order_id = getattr(response.request, 'order_id')
order_no = getattr(response.request, 'order_no')
master_id = getattr(response.request, 'master_id')
tenant_id = getattr(response.request, 'tenant_id')
response_body = response.body.decode()
response_data = json.loads(response_body)
order_detail_data = udict.get_by_path(response_data, 'result')
mapped_df = pd.DataFrame([order_detail_data])
# 更换映射方向,用于将源数据列名改为与数据库表对应
forward_mapping = {dict_f: table_f for table_f, dict_f in GovsOrderDetail.FieldMapping.items()}
mapped_df = mapped_df.rename(columns=forward_mapping)
# 把数组和字典转换为json字符串
mapped_df[GovsOrderDetail.order_custom_form_fields.key] = mapped_df[
GovsOrderDetail.order_custom_form_fields.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderDetail.order_phone_dto.key] = mapped_df[GovsOrderDetail.order_phone_dto.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderDetail.order_user.key] = mapped_df[GovsOrderDetail.order_user.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderDetail.order_attachment_list.key] = mapped_df[GovsOrderDetail.order_attachment_list.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderDetail.pre_process_list.key] = mapped_df[GovsOrderDetail.pre_process_list.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderDetail.tripartite_call_records_list.key] = mapped_df[
GovsOrderDetail.tripartite_call_records.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderDetail.plan_finish_time.key] = mapped_df[GovsOrderDetail.plan_finish_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderDetail.order_finish_time.key] = mapped_df[GovsOrderDetail.order_finish_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderDetail.plan_sign_time.key] = mapped_df[GovsOrderDetail.plan_sign_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
# 这里把空数据都换成 None,以便存入数据库时是 null
mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, None, inplace=True)
_created, _updated = await GovsOrderDetail.save_batch(mapped_df)
# 存储用户信息
user_data = udict.get_by_path(response_data, 'result.orderUser')
if user_data:
user_df = pd.DataFrame([user_data])
# 更换映射方向,用于将源数据列名改为与数据库表对应
forward_mapping = {dict_f: table_f for table_f, dict_f in GovsOrderUser.FieldMapping.items()}
user_mapped_df = user_df.rename(columns=forward_mapping)
# 比较字段转字符串
user_mapped_df[GovsOrderUser.id.key] = user_mapped_df[GovsOrderUser.id.key].astype(str)
user_mapped_df[GovsOrderUser.master_id.key] = user_mapped_df[GovsOrderUser.master_id.key].astype(str)
# 转换日期时间
user_mapped_df[GovsOrderUser.created_at.key] = user_mapped_df[GovsOrderUser.created_at.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
user_mapped_df[GovsOrderUser.updated_at.key] = user_mapped_df[GovsOrderUser.updated_at.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
# 这里把空数据都换成 None,以便存入数据库时是 null
user_mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, None, inplace=True)
await GovsOrderUser.save_batch(user_mapped_df)
# 存储附件信息
attachment_list = udict.get_by_path(response_data, 'result.orderAttachmentList')
if attachment_list:
attachment_df = pd.DataFrame(attachment_list)
# 更换映射方向,用于将源数据列名改为与数据库表对应
forward_mapping = {dict_f: table_f for table_f, dict_f in GovsOrderAttachment.FieldMapping.items()}
attachment_mapped_df = attachment_df.rename(columns=forward_mapping)
attachment_mapped_df[GovsOrderAttachment.master_id.key] = master_id
attachment_mapped_df[GovsOrderAttachment.order_id.key] = order_id
# 比较字段转字符串
attachment_mapped_df[GovsOrderAttachment.id.key] = attachment_mapped_df[GovsOrderAttachment.id.key].astype(str)
attachment_mapped_df[GovsOrderAttachment.master_id.key] = attachment_mapped_df[GovsOrderAttachment.master_id.key].astype(str)
# 这里把空数据都换成 None,以便存入数据库时是 null
attachment_mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, None, inplace=True)
await GovsOrderAttachment.save_batch(attachment_mapped_df)
# 输出数据创建状态
echo_log(f"成功创建租户:{tenant_id} 的待办工单:{master_id}({order_id}{order_no}) 详情.")
if retry_queue:
echo_log(f"待办工单详情重试队列中有:{retry_queue.qsize()} 个请求在等待.")
if __name__ == "__main__":
from paste.core import aio_pool
async def scrape(order_id: Union[str, int], master_id: Union[str, int], tenant_id: Union[str, int],
order_no: Union[str, int], ):
task_request = await get_task_request(order_id, master_id, tenant_id)
setattr(task_request, 'order_id', order_id)
setattr(task_request, 'master_id', master_id)
setattr(task_request, 'tenant_id', tenant_id)
setattr(task_request, 'order_no', order_no)
request_queue = asyncio.Queue()
await request_queue.put(task_request)
await requests.async_concurrency(
request_queue, retry=dock.MAX_RETRY_COUNT,
after_request=after_task_request
)
_runner = aio_pool.get_aio_runner()
_runner(scrape('DH050826052517663', '2058851271599333378', '1773611023340371969', 'DH050826052517663*3'))
+156
View File
@@ -0,0 +1,156 @@
import asyncio
import json
from typing import Union
import numpy as np
import pandas as pd
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import models
from dock.govs import govs_api
from models.govs_order_master import GovsOrderMaster
from paste.core.logging import echo_log
from paste.util import udict
from paste.web import requests
async def get_task_request(order_id: Union[str, int] = '', dept_page_tag: int = 1,
current_page: int = 1, num_per_page: int = 200):
"""
获取省12345任务列表数据。
通过 POST 请求向省12345的任务列表接口提交表单数据,获取任务分页数据。
Args:
order_id (int): 任务列表类型 ID,默认为企业待办:600058
dept_page_tag (int): 分页标志
current_page (int): 当前页码
num_per_page (int): 每页显示数据量,默认 200
"""
api_url = f"/orderhandler/taskQuery/getDeptAllToDoOrderProcess"
request_body = {
"data": {
"deptPageTag": dept_page_tag,
"orderId": f"{order_id}",
"keyWord": "",
"andOrFlag": "0",
"serviceObjectType": [],
"callNumber": "",
"orderSource": [],
"orderSourceDetailList": [],
"signedStatus": [],
"firstOrderStatus": [],
"secordOrderStatus": [],
"status": [],
"overDue": "",
"existQuotoInfo": [],
"isSupervise": [],
"planFinishTime": "",
"caseIsUrgent": [],
"areaCodeCity": "",
"areaCodeArea": "",
"areaCodeStreet": "",
"addressDetail": "",
"infoProtect": [],
"firstLevelAffiliations": [],
"secondLevelAffiliations": [],
"thirdLevelAffiliations": [],
"fourthLevelAffiliations": [],
"fifthLevelAffiliations": [],
"caseAccordTypeOneNames": [],
"caseAccordTypeTwoNames": [],
"caseAccordTypeThreeNames": [],
"caseAccordTypeFourNames": [],
"caseAccordTypeFiveNames": [],
"creatorId": "",
"assigneeUserId": "",
"callTimeEnd": "",
"callTimeFrom": "",
"caseLabels": [],
"contactNumber": "",
"createBy": "",
"deptName": "",
"deptType": "",
"fileExist": [],
"hotspot": [],
"claimStatus": "",
"orderSourceDetail": "",
"orderType": [],
"orgName": [],
"sortField": "",
"sortRule": "",
"actionName": "",
"returnReasonNameList": [],
"createDateFrom": "",
"createDateEnd": "",
"planBackTimeStart": "",
"planBackTimeEnd": "",
"planFinishTimeStart": "",
"planFinishTimeEnd": ""
},
"pageSize": num_per_page,
"pageNum": current_page
}
# 构造 API 请求
return await govs_api.new_api_request(api_url, request_body)
async def after_task_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
任务请求响应后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
response_body = response.body.decode()
response_data = json.loads(response_body)
list_data: list[dict] = udict.get_by_path(response_data, 'data.list')
order_master_list: list[dict] = []
for d in list_data:
order_master_dto = d.get('orderMasterDTO')
order_master_dto['nextTaskId'] = d.get('nextTaskId')
order_master_dto['claimStatus'] = d.get('claimStatus')
order_master_list.append(order_master_dto)
if order_master_list:
mapped_df = pd.DataFrame(order_master_list)
# 更换映射方向,用于将源数据列名改为与数据库表对应
forward_mapping = {dict_f: table_f for table_f, dict_f in GovsOrderMaster.FieldMapping.items()}
mapped_df = mapped_df.rename(columns=forward_mapping)
# 把数组转换为 JSON 字符串
mapped_df[GovsOrderMaster.attachment_list.key] = mapped_df[GovsOrderMaster.attachment_list.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderMaster.back_count.key] = mapped_df[GovsOrderMaster.back_count.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
# 根据claim_status字段,更新govs_sign字段
mapped_df[GovsOrderMaster.govs_sign.key] = np.where(
mapped_df[GovsOrderMaster.claim_status.key] == '已签收', 1, 0
)
# 这里把空数据都换成 None,以便存入数据库时是 null
mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, None, inplace=True)
# 筛选数据状态
_created, _updated = await GovsOrderMaster.save_batch(mapped_df)
echo_log(f"成功创建企业待办:{_created}条,更新:{_updated}条.")
else:
echo_log('未获取到企业待办数据')
if retry_queue:
echo_log(f"企业待办重试队列中有:{retry_queue.qsize()} 个请求在等待.")
if __name__ == "__main__":
from paste.core import aio_pool
async def scrape():
task_request = await get_task_request(dept_page_tag=0, order_id='DH058726052903006')
request_queue = asyncio.Queue()
await request_queue.put(task_request)
await requests.async_concurrency(
request_queue, retry=dock.MAX_RETRY_COUNT,
after_request=after_task_request
)
_runner = aio_pool.get_aio_runner()
_runner(scrape())
+154
View File
@@ -0,0 +1,154 @@
import asyncio
import json
from typing import Union
import pandas as pd
from dateutil import parser
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
import models
from dock.govs import govs_api
from models.govs_order_process import GovsOrderProcess
from paste.core.logging import echo_log
from paste.util import udict
from paste.web import requests
async def get_task_request(order_id: str, order_no: str, master_id: Union[str, int],
tenant_id: Union[str, int], dept_id: Union[str, int], area_code: str,
sort: str = ""):
"""
获取省12345任务处理过程数据。
通过 POST 请求向省12345的任务处理过程接口提交表单数据,获取任务处理过程数据。
自动注入有效的 Cookie(如 JSESSIONID)至请求头,并解析返回的 JSON 响应。
Args:
order_id (str): 待办任务ID
order_no (str): 待办任务号
master_id (int): 关联订单主表ID
tenant_id (str, int): 租户ID
dept_id (str, int): 部门ID
area_code (str): 邮编
sort (str): 排序
"""
api_url = f"/orderreceive/orderMaster/queryOrderProcess"
request_body = {
"orderId": order_id,
"orderNo": order_no,
"masterId": master_id,
"tenantId": tenant_id,
"deptId": dept_id,
"areaCode": area_code,
"sort": sort,
}
# 构造 API 请求
return await govs_api.new_api_request(api_url, request_body)
async def after_task_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
"""
任务请求响应后的处理程序。
:param response: 响应对象
:param retry_queue: 重试队列
"""
order_id = getattr(response.request, 'order_id')
order_no = getattr(response.request, 'order_no')
master_id = getattr(response.request, 'master_id')
tenant_id = getattr(response.request, 'tenant_id')
response_body = response.body.decode()
response_data = json.loads(response_body)
list_data = udict.get_by_path(response_data, 'result')
task_df = pd.DataFrame(list_data)
# 更换映射方向,用于将源数据列名改为与数据库表对应
forward_mapping = {dict_f: table_f for table_f, dict_f in GovsOrderProcess.FieldMapping.items()}
mapped_df = task_df.rename(columns=forward_mapping)
mapped_df[GovsOrderProcess.master_id.key] = master_id
mapped_df[GovsOrderProcess.tenant_id.key] = tenant_id
# 比较字段转字符串
mapped_df[GovsOrderProcess.id.key] = mapped_df[GovsOrderProcess.id.key].astype(str)
mapped_df[GovsOrderProcess.master_id.key] = mapped_df[GovsOrderProcess.master_id.key].astype(str)
# 过滤掉 id 和 order_id 为空的数据
mapped_df = mapped_df[
mapped_df[GovsOrderProcess.id.key].notna() & (mapped_df[GovsOrderProcess.id.key] != "")
]
mapped_df = mapped_df[
mapped_df[GovsOrderProcess.order_id.key].notna() & (mapped_df[GovsOrderProcess.order_id.key] != "")
]
# 字典转化为字符串
mapped_df[GovsOrderProcess.child_order_processes.key] = mapped_df[GovsOrderProcess.child_order_processes.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderProcess.handler_user_ids.key] = mapped_df[GovsOrderProcess.handler_user_ids.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderProcess.handler_org_ids.key] = mapped_df[GovsOrderProcess.handler_org_ids.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderProcess.next_handler_user_ids.key] = mapped_df[GovsOrderProcess.next_handler_user_ids.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
mapped_df[GovsOrderProcess.attachment_dto_list.key] = mapped_df[GovsOrderProcess.attachment_dto_list.key].apply(
lambda x: json.dumps(x, ensure_ascii=False) if x is not None else None
)
# 时间字段转化为日期对象
mapped_df[GovsOrderProcess.plan_sign_time.key] = mapped_df[GovsOrderProcess.plan_sign_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderProcess.plan_finish_time.key] = mapped_df[GovsOrderProcess.plan_finish_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderProcess.plan_back_time.key] = mapped_df[GovsOrderProcess.plan_back_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderProcess.contact_time.key] = mapped_df[GovsOrderProcess.contact_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderProcess.contact_time.key] = mapped_df[GovsOrderProcess.contact_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderProcess.origin_plan_finish_time.key] = mapped_df[
GovsOrderProcess.origin_plan_finish_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
mapped_df[GovsOrderProcess.origin_plan_sign_time.key] = mapped_df[GovsOrderProcess.origin_plan_sign_time.key].apply(
lambda x: parser.parse(x).strftime('%Y-%m-%d %H:%M:%S') if isinstance(x, str) and x.strip() else None
)
# 这里把空数据都换成 None,以便存入数据库时是 null
mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, None, inplace=True)
_created, _updated = await GovsOrderProcess.save_batch(mapped_df)
# 输出数据创建状态
echo_log(
f"成功创建租户:{tenant_id} 的待办工单:{master_id}({order_id}{order_no}) 处理流程:{_created}条,更新:{_updated}条.")
if retry_queue:
echo_log(f"待办工单处理流程重试队列中有:{retry_queue.qsize()} 个请求在等待.")
if __name__ == "__main__":
from paste.core import aio_pool
async def scrape(order_id: str, order_no: str, master_id: Union[str, int],
tenant_id: Union[str, int], dept_id: Union[str, int], area_code: str,
sort: str = ""):
task_request = await get_task_request(order_id, order_no, master_id, tenant_id, dept_id, area_code, sort)
setattr(task_request, 'order_id', order_id)
setattr(task_request, 'order_no', order_no)
setattr(task_request, 'master_id', master_id)
setattr(task_request, 'tenant_id', tenant_id)
request_queue = asyncio.Queue()
await request_queue.put(task_request)
await requests.async_concurrency(
request_queue, retry=dock.MAX_RETRY_COUNT,
after_request=after_task_request
)
_runner = aio_pool.get_aio_runner()
_runner(scrape(
'DH050826052517663', 'DH050826052517663*3', '2058851271599333378',
'1773611023340371969', '1700467981117980074', '320500',
))
+116
View File
@@ -0,0 +1,116 @@
"""
安全模块。
"""
import asyncio
import json
from tornado.httpclient import HTTPResponse, HTTPRequest
import dock
from dock.govs import govs_api
from models.token import TokenModel
from paste.core import config
from paste.core.logging import echo_log
from paste.security import cryp_rsa
from paste.util import udict
from paste.web import requests
public_key = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0Jr1NzVUQMburkZT6Rkt0eaPm
H8TN6E258l2tZMJgVCP/sL4oKjroKYmNPBkSSiLKFr9wwJqfesMeef6ChGRUXjG6
DX0oxQRe0f5/UnyEm/NicJwz9xwkU34gbuo1VB/EA2QZ5dl1rj9iSsiqKLK6/QFl
VuzslRdAXYZC79vprwIDAQAB
-----END PUBLIC KEY-----"""
"""
固定公钥,确保登录成功。
"""
async def login():
"""
登录政务服务 12345 系统并获取认证 Token。
流程:
1. 密码需要加密,密码明文使用PKCS1_v1_5进行RSA加密。
2. 从响应的['data']['access_token']内获取token。
Args:
无参数。
Returns:
tuple: 包含两个元素的元组:
- dict: DCM 接口返回的完整 JSON 响应数据
Raises:
AssertionError: 登录失败(`resultInfo.success` 为 False
ValueError: 响应体非合法 JSON
HTTPError: 网络请求失败(由 `async_request` 抛出)
"""
login_url = f"{govs_api.ApiUrl}/system/sysLogin"
# 构建扩展头
user_agent, browser_ver, os_name = dock.get_random_user_agent()
extra_headers = {
'Content-Type': 'application/json; charset=UTF-8',
'User-Agent': user_agent,
}
# 构造请求
request_body = {
"username": config.get_config("dock.govs.account.username"),
"password": cryp_rsa.rsa_encrypt_pkcs1_v1_5(public_key, config.get_config("dock.govs.account.password")),
"tenantAccount": "suzhou",
"rememberme": 1,
"code": "",
"uuid": "",
}
# 构造请求对象
request = dock.new_http_request(
url=login_url,
body=request_body,
method='POST',
timeout=dock.DEFAULT_TIMEOUT,
use_form=False,
extra_headers=extra_headers,
** govs_api.ProxyConfig
)
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)
success = udict.get_by_path(response_data, 'data.access_token', '')
if success:
await TokenModel.refresh(platform='GOVS', token=success)
echo_log(f"成功刷新省12345登录令牌.")
else:
echo_log(f"省12345登录失败,无法刷新令牌,响应:{response_body}")
if retry_queue:
echo_log(f"登录重试队列中有:{retry_queue.qsize()} 个请求在等待.")
return response_data
async def get_token(platform: str = 'GOVS'):
"""
取得可用 Token。
:param platform: 要查询的平台,默认是:GOVS,省12345
: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())
+17
View File
@@ -0,0 +1,17 @@
import io
from dock.govs import govs_api
async def get_upload_request(file_name: str, file_io: io.IOBase):
"""
创建上传文件到省12345的请求对象。方法仅创建请求对象,并未实际提交请求,具体由调度方法处理。
:param file_name: 文件名
:param file_io: 文件io对象
"""
api_url = '/file/api/system/uploadcircuit'
body = {
file_name: file_io
}
return await govs_api.new_api_request(api_url, body, use_form=True)