$O#>W
z&;d=yJC1amUU6aF`QXj=Oh;A-=Y+8FB67t^9w47lPID5pFM8s(HntBrDc(WtcsDkGSdaSqpIRv!%-
l&h<8BaZ?UA6`3YXPvXV=^x?-=%M*(~W)Ty7;TY@0{ts5X|0@6h
literal 0
HcmV?d00001
diff --git a/apps/api/dcm/__pycache__/rollback.cpython-311.pyc b/apps/api/dcm/__pycache__/rollback.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..db5744e6fc2482e5abe2bddfa57415c7596db6e1
GIT binary patch
literal 6457
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
U=Z4<;y`J5$B1`R^KGpR2fB70uMgRZ+
literal 0
HcmV?d00001
diff --git a/apps/api/dcm/__pycache__/stage_reply.cpython-311.pyc b/apps/api/dcm/__pycache__/stage_reply.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7a5d7ed472eef1da93d8d8156f57cb4d25bc5648
GIT binary patch
literal 5515
zcmb7IYitzP6~6P>r)SqMY#d^+!^333_VQ>^Wf>X+4$w$|01;)IYP#MTo3&?mHg{$X
zcI@I1Cs`*voF*x>AIcS~H%7xEdrNAkzYL*)kV
zl`3MDp-O`_IzWvw5={F|xAD+*@
zeRk&BUuOO^diTOd)7SoyfAi1zOJ|H8fA!ua1MUw{y8A#vjmq(`1Uw@|Vvu+{5{bRx
zxFCrba?7BQPb8INyrPDCL>`L?X#mQ7yCbo~q~y?CfeQ+bgq1$1bBCicpO9rTs9TI2
zlX*$*0p5Y-q$&=U1&yJMjuC5Tz_4?f+1{CfW7iRrTwcSoKhYa^?USV{x?
zaKKj%-|SH!Z&2q!+0>GHyYlo}HA7Ss4bhjVvlOT*8bUk`-Hl-6Eael>Ve~$=Kfvk^
zC7BSh?&kSuJgV}%?k0+%!h!4Mge%n!R%aDa>eksX?oo7S-*F@L@O(rHD+e
zGq_^W3AYv(`4H4AKLc`a1m)ayXB=a$(=N?jcVTVD9Y{9;G2_=8gDLj3FZYY~6sx)G
zGe(?+ehAkB(VbXSlQ?dZJej);zS(adfUOasq<^4j#+$lZ%0`E#eBq-d(7
zneOS9Rw_%`;)OQVp>sEj2#(Bh6&CJNAa8B`lO
zt`PleY{Ye>y`N8naX6;%-7@BduEEA7Ow87T%%Tu4HG+QdBHs#)TdQ%8PBdNL@WF=3
zU*BAv-O#CR=$zsn(ztaR7ns;~z3qdx$?h9n*^M3A#tx&R5jv%XOEImfZOQy2&^O)x
z-rzfflYtwDvg;qy)<3pzMCsv(?AHSO3+3(#
zZvnYIUQE!zX41qMUV(4604Dts^#r28uzSEn-=T)-6lMCKA$m8m!|E_I#Net@*cG7U
zO|;O*pQ6U7v<1O1j}-*)WOjucW>w1|L1iuI7-H@BhUWpkpJk4s^URAZz_w(x*
zgiKHk-)u7w;%|FE(7&k|riQ37x}Q?qWAqS36w3@B`iH1j>C=^m5hh;xeH37Hx^g2RE4NSX0Rk~wwL`1gk>Pd}oM`#V^S!_CP1jCTWow(Y+U8W*)T(vb
zs%?|cW>@XfR_)5vEsW%P10Ac@jW5$G*JdhPA*TDyH07LXX#V5Lizjb4Y`@j8{YH1T
zp;K$Rvd;Q?Qz0_IE5eX-7ldi
z`aP%ul#}}vzzJ_um&4nFyk3e0M*>I_V#
zyMXinAnA_ILx&C=3OEex(kUKqH|B;Jl%%BUl&Fwv0+Hg})s0QaiaJ=k7^O)06VO(f
z1!AI8p~~giNlkIf#*e)Iv{ut}yQbwgsRT1#i^_vvr%b
zy3J$zQXQ%OTz&I}-CBKfrhXg5iGDN9)NWf4Q+sk;#ci(s7FVBsWMX}WtIu**e{F
zEF>kub3Fe{GAzy2xOiTWBRp?BJVCLDgECwfaSiw|5ZyQT%8iJU#Pb0Plgo@kAm}l}
z%VTo=G6;D2*)|RX|;eLnPiP*A*p}Z;hi{AK#(jX
z?G+9p>Yt_t)Eesy)Z!<}n)d_Io$ziTj*?tl;C~)?o411fU;>Qb+s$l(KW6@Ygo$p#
z9|dxxuN59Drem%J*@}`v3QGSpFui|yNu}Y-jZ1?!5mr>OH6r6Vw_i11&^l)+rco|4
zD~^j@t;8O2Se3DY36SZYjzshz7Q3S-%p)Mz6myeAF@AKnn`gcQ3y>w}r8EI8Fcd`<
zXqo~^6XHZv`G}wVa_HfVJ${YqGWM85>ofM4LxGGv=Fp~$J?7Awj6LR1L&hGL8o4Qh
zY!MbqyGCkKYfiUhTx-pkrPgWGx&pnAqKF>cKe+&x5~KEeGx?4|bkqI18gl$E0Ns<^
literal 0
HcmV?d00001
diff --git a/apps/api/dcm/apply_postpone.py b/apps/api/dcm/apply_postpone.py
new file mode 100644
index 0000000..884bff7
--- /dev/null
+++ b/apps/api/dcm/apply_postpone.py
@@ -0,0 +1,102 @@
+"""
+接受OA请求,操作数字城管工单延期
+"""
+import logging
+from typing import Optional
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_push_apply_postpone
+from models.dcm_apply_delay import DcmApplyPostpone
+from models.dcm_task import DcmTask
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/applyDelay')
+class ApplyPostponeHandler(AppHandler):
+ """
+ 申请延期接口。
+
+ 对接数字城管系统的申请延期接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.dcm_task: Optional[DcmTask] = None
+ self.dcm_apply_postpone: Optional[DcmApplyPostpone] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ DcmApplyPostpone.flow_token.key: kwargs.get('flowToken', ''),
+ DcmApplyPostpone.dcm_task_id.key: kwargs.get('gdId', ''),
+ DcmApplyPostpone.task_number.key: kwargs.get('taskNumber', ''),
+ DcmApplyPostpone.apply_act_id.key: self.dcm_task.act_id,
+ DcmApplyPostpone.reply_part_id.key: kwargs.get('replyPartID', 39),
+ DcmApplyPostpone.ard_level.key: kwargs.get('ardLevel', 0),
+ DcmApplyPostpone.ard_type_id.key: kwargs.get('ardTypeId', 12),
+ DcmApplyPostpone.apply_memo.key: kwargs.get('opinion', ''),
+ DcmApplyPostpone.apply_type.key: kwargs.get('applyType', '延期'),
+ DcmApplyPostpone.attachments.key: kwargs.get('attachments', ''),
+ DcmApplyPostpone.delay_multiple.key: kwargs.get('delayMultiple', 2),
+ DcmApplyPostpone.time_num.key: kwargs.get('timeNum', 48),
+ DcmApplyPostpone.time_unit.key: kwargs.get('timeUnit', 4),
+ DcmApplyPostpone.postpone_date.key: kwargs.get('postponeDate', ''),
+ }
+
+ async def apply_postpone(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = [
+ 'gdId', 'taskNumber', 'applyType', 'opinion', 'delayMultiple', 'flowToken'
+ ]
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+ if kwargs.get('delayMultiple') not in (1, 2, '1', '2'):
+ raise ValueError('延期倍数只能为1或2')
+
+ # 读取待办任务对象
+ dcm_task_id = kwargs.get('gdId', '')
+ self.dcm_task = await DcmTask.async_find_by_id(dcm_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.dcm_apply_postpone = DcmApplyPostpone().copy_from_dict(params)
+ self.dcm_apply_postpone.status = 0
+ await self.dcm_apply_postpone.async_save()
+
+ # 后台执行提交申请延期请求到数字城管
+ await aio_pool.run_background_task(
+ dcm_push_apply_postpone.push_apply_postpone(self.dcm_apply_postpone, self.dcm_task)
+ )
+
+ return {
+ 'msg': '申请延期成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 申请延期接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.apply_postpone(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/apply_rollback.py b/apps/api/dcm/apply_rollback.py
new file mode 100644
index 0000000..054ecb3
--- /dev/null
+++ b/apps/api/dcm/apply_rollback.py
@@ -0,0 +1,97 @@
+"""
+接受OA请求,操作数字城管的申请回退接口
+"""
+import logging
+from typing import Optional
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_push_apply_rollback
+from models.dcm_apply_rollback import DcmApplyRollback
+from models.dcm_task import DcmTask
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/applyRollback')
+class ApplyRollbackHandler(AppHandler):
+ """
+ 申请回退接口。
+
+ 对接数字城管系统的申请回退接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.dcm_task: Optional[DcmTask] = None
+ self.dcm_apply_rollback: Optional[DcmApplyRollback] = None
+
+ def _extract_params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ DcmApplyRollback.flow_token.key: kwargs.get('flowToken', ''),
+ DcmApplyRollback.dcm_task_id.key: kwargs.get('gdId', ''),
+ DcmApplyRollback.act_id.key: self.dcm_task.act_id,
+ DcmApplyRollback.task_number.key: kwargs.get('taskNumber', ''),
+ DcmApplyRollback.reply_part_id.key: kwargs.get('replyPartID', 39),
+ DcmApplyRollback.ard_level.key: kwargs.get('ardLevel', 0),
+ DcmApplyRollback.ard_type_id.key: 18 if kwargs.get('applyType', '拒签') == '拒签' else 62,
+ DcmApplyRollback.opinion.key: kwargs.get('opinion', ''),
+ DcmApplyRollback.apply_type.key: kwargs.get('applyType', '拒签'),
+ DcmApplyRollback.trans_info.key: kwargs.get('transInfo', '52,254,0'),
+ DcmApplyRollback.attachments.key: kwargs.get('attachments', '')
+ }
+
+ async def apply_rollback(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId', 'taskNumber', 'opinion', 'applyType', 'flowToken']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+ if kwargs['applyType'] not in ('拒签', '处置阶段照片未公开'):
+ raise ValueError('申请类型只能为拒签或处置阶段照片未公开')
+
+ # 读取待办任务对象
+ dcm_task_id = kwargs.get('gdId', '')
+ self.dcm_task = await DcmTask.async_find_by_id(dcm_task_id)
+
+ # 保存请求数据
+ params = self._extract_params_for_db(**kwargs)
+ self.dcm_apply_rollback = DcmApplyRollback().copy_from_dict(params)
+ self.dcm_apply_rollback.status = 0
+ await self.dcm_apply_rollback.async_save()
+
+ # 后台执行提交申请回退请求到数字城管
+ await aio_pool.run_background_task(
+ dcm_push_apply_rollback.push_apply_rollback(self.dcm_apply_rollback, self.dcm_task)
+ )
+
+ return {
+ 'msg': '申请回退成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 申请回退接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.apply_rollback(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/dispose.py b/apps/api/dcm/dispose.py
new file mode 100644
index 0000000..00dbdbe
--- /dev/null
+++ b/apps/api/dcm/dispose.py
@@ -0,0 +1,96 @@
+"""
+接受OA请求,操作数字城管的工单批转接口
+"""
+import logging
+from typing import Optional
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_push_dispose
+from models.dcm_dispose import DcmDispose
+from models.dcm_task import DcmTask
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/transfer')
+class DisposeHandler(AppHandler):
+ """
+ 批转接口。
+
+ 对接数字城管系统的批转接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.dcm_task: Optional[DcmTask] = None
+ self.dcm_dispose: Optional[DcmDispose] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ DcmDispose.flow_token.key: kwargs.get('flowToken', ''),
+ DcmDispose.dcm_task_id.key: kwargs.get('gdId', ''),
+ DcmDispose.act_id.key: self.dcm_task.act_id,
+ DcmDispose.task_number.key: kwargs.get('taskNumber', ''),
+ DcmDispose.opinion.key: kwargs.get('opinion', ''),
+ DcmDispose.attachments.key: kwargs.get('attachments', ''),
+ DcmDispose.send_message.key: kwargs.get('sendMessage', '1'),
+ DcmDispose.trans_info.key: '52,254,0,0',
+ DcmDispose.add_num.key: kwargs.get('addNum', '0'),
+ DcmDispose.task_list_id.key: kwargs.get('taskListId', '600058'),
+ DcmDispose.undertake_user_name.key: kwargs.get('undertakeUserName', ''),
+ DcmDispose.undertake_phone.key: kwargs.get('undertakePhone', '')
+ }
+
+ async def dispose(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId', 'taskNumber', 'opinion', 'attachments', 'flowToken']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ dcm_task_id = kwargs.get('gdId', '')
+ self.dcm_task = await DcmTask.async_find_by_id(dcm_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.dcm_dispose = DcmDispose().copy_from_dict(params)
+ self.dcm_dispose.status = 0
+ await self.dcm_dispose.async_save()
+
+ # 后台执行提交批转请求到数字城管
+ await aio_pool.run_background_task(
+ dcm_push_dispose.push_dispose(self.dcm_dispose, self.dcm_task)
+ )
+
+ return {
+ 'msg': '批转成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 批转接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.dispose(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/fetch_allow_postpone.py b/apps/api/dcm/fetch_allow_postpone.py
new file mode 100644
index 0000000..3118263
--- /dev/null
+++ b/apps/api/dcm/fetch_allow_postpone.py
@@ -0,0 +1,58 @@
+"""
+接受OA请求,读取数字城管的是否允许申请延期。
+"""
+import logging
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_scrape_allow_postpone
+from models.dcm_task import DcmTask
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/fetchAllowPostpone')
+class AllowPostponeHandler(AppHandler):
+ """
+ 获取是否允许申请延期接口。
+
+ 对接数字城管系统的获取是否允许申请延期接口,用于判断工单有哪些是否允许申请延期。
+ """
+
+ async def fetch_allow_postpone(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ dcm_task_id = kwargs.get('gdId', '')
+ dcm_task = await DcmTask(id=dcm_task_id).async_find_first()
+ assert dcm_task, f"未找到待办工单,工单ID:{dcm_task_id}"
+ success, message = await dcm_scrape_allow_postpone.fetch_allow_postpone(dcm_task)
+ return {
+ 'success': success,
+ 'msg': message,
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 获取是否允许申请延期接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.fetch_allow_postpone(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/fetch_dispose_form.py b/apps/api/dcm/fetch_dispose_form.py
new file mode 100644
index 0000000..cfc0cf2
--- /dev/null
+++ b/apps/api/dcm/fetch_dispose_form.py
@@ -0,0 +1,59 @@
+"""
+接受OA请求,读取数字城管的便民表单。
+"""
+import logging
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_scrape_conv_dispose
+from models.dcm_task import DcmTask
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/fetchDisposeForm')
+class FetchConvenientFormHandler(AppHandler):
+ """
+ 获取便民表单接口。
+
+ 对接数字城管系统的获取便民表单接口,用于判断工单有哪些便民表单。
+ """
+
+ async def fetch_form(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId', 'formId']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ dcm_task_id = kwargs.get('gdId', '')
+ dcm_task = await DcmTask(id=dcm_task_id).async_find_first()
+ assert dcm_task, f"未找到待办工单,工单ID:{dcm_task_id}"
+
+ form = await dcm_scrape_conv_dispose.fetch_form(dcm_task)
+ return {
+ 'msg': '获取便民批转表单成功.',
+ 'form': form,
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 获取便民表单接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.fetch_form(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/fetch_operation.py b/apps/api/dcm/fetch_operation.py
new file mode 100644
index 0000000..9dc3087
--- /dev/null
+++ b/apps/api/dcm/fetch_operation.py
@@ -0,0 +1,58 @@
+"""
+接受OA请求,读取数字城管的可用操作。
+"""
+import logging
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_scrape_operation
+from models.dcm_task import DcmTask
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/fetchOperation')
+class FetchOperationHandler(AppHandler):
+ """
+ 获取可用操作接口。
+
+ 对接数字城管系统的获取可用操作接口,用于判断工单有哪些可用操作。
+ """
+
+ async def fetch_operations(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ dcm_task_id = kwargs.get('gdId', '')
+ dcm_task = await DcmTask(id=dcm_task_id).async_find_first()
+ assert dcm_task, f"未找到待办工单,工单ID:{dcm_task_id}"
+ operations = await dcm_scrape_operation.fetch_operation(dcm_task)
+ return {
+ 'msg': '获取可用操作成功.',
+ 'operations': operations,
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 获取可用操作接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.fetch_operations(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/fetch_rollback_form.py b/apps/api/dcm/fetch_rollback_form.py
new file mode 100644
index 0000000..ecdc669
--- /dev/null
+++ b/apps/api/dcm/fetch_rollback_form.py
@@ -0,0 +1,59 @@
+"""
+接受OA请求,读取数字城管的便民表单。
+"""
+import logging
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_scrape_conv_rollback
+from models.dcm_task import DcmTask
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/fetchRollbackForm')
+class FetchRollbackFormHandler(AppHandler):
+ """
+ 获取便民表单接口。
+
+ 对接数字城管系统的获取便民表单接口,用于判断工单有哪些便民表单。
+ """
+
+ async def fetch_form(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId', 'formId']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ dcm_task_id = kwargs.get('gdId', '')
+ dcm_task = await DcmTask(id=dcm_task_id).async_find_first()
+ assert dcm_task, f"未找到待办工单,工单ID:{dcm_task_id}"
+
+ form = await dcm_scrape_conv_rollback.fetch_form(dcm_task)
+ return {
+ 'msg': '获取便民回退表单成功.',
+ 'form': form,
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 获取便民表单接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.fetch_form(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/rollback.py b/apps/api/dcm/rollback.py
new file mode 100644
index 0000000..a3dd5a4
--- /dev/null
+++ b/apps/api/dcm/rollback.py
@@ -0,0 +1,98 @@
+"""
+接受OA请求,操作数字城管的回退接口
+"""
+import logging
+from typing import Optional
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_push_rollback
+from models.dcm_rollback import DcmRollback
+from models.dcm_task import DcmTask
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/rollback')
+class RollbackHandler(AppHandler):
+ """
+ 回退接口。
+
+ 对接数字城管系统的回退接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.dcm_task: Optional[DcmTask] = None
+ self.dcm_rollback: Optional[DcmRollback] = None
+
+ def _extract_params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ DcmRollback.flow_token.key: kwargs.get('flowToken', ''),
+ DcmRollback.dcm_task_id.key: kwargs.get('gdId', ''),
+ DcmRollback.act_id.key: self.dcm_task.act_id,
+ DcmRollback.task_number.key: kwargs.get('taskNumber', ''),
+ DcmRollback.opinion.key: kwargs.get('opinion', ''),
+ DcmRollback.attachments.key: kwargs.get('attachments', ''),
+ DcmRollback.send_message.key: kwargs.get('sendMessage', '1'),
+ DcmRollback.trans_info.key: kwargs.get('transInfo', '50,254,0'),
+ DcmRollback.save_old_act_flag.key: kwargs.get('saveOldActFlag', False),
+ DcmRollback.rollback_reason_id.key: kwargs.get('rollbackReasonId', -1),
+ DcmRollback.not_assigned.key: kwargs.get('notAssigned', '0'),
+ DcmRollback.not_assigned_reason.key: kwargs.get('notAssignedReason', ''),
+ DcmRollback.undertake_user_name.key: kwargs.get('undertakeUserName', ''),
+ DcmRollback.undertake_phone.key: kwargs.get('undertakePhone', '')
+ }
+
+ async def rollback(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId', 'taskNumber', 'opinion', 'flowToken']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ dcm_task_id = kwargs.get('gdId', '')
+ self.dcm_task = await DcmTask.async_find_by_id(dcm_task_id)
+
+ # 保存请求数据
+ params = self._extract_params_for_db(**kwargs)
+ self.dcm_rollback = DcmRollback().copy_from_dict(params)
+ self.dcm_rollback.status = 0
+ await self.dcm_rollback.async_save()
+
+ # 后台执行提交回退请求到数字城管
+ await aio_pool.run_background_task(
+ dcm_push_rollback.push_rollback(self.dcm_rollback, self.dcm_task)
+ )
+
+ return {
+ 'msg': '回退成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 回退接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.rollback(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/dcm/stage_reply.py b/apps/api/dcm/stage_reply.py
new file mode 100644
index 0000000..7ca4e9e
--- /dev/null
+++ b/apps/api/dcm/stage_reply.py
@@ -0,0 +1,91 @@
+"""
+接受OA请求,操作数字城管的阶段回复接口
+"""
+import logging
+from typing import Optional
+
+from apps.api import dcm
+from apps.app_handler import AppHandler
+from dock.dcm import dcm_push_stage_reply
+from models.dcm_stage_reply import DcmStageReply
+from models.dcm_task import DcmTask
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from paste.web.decorators import route
+
+
+@route(f'{dcm.ApiPrefix}/stageReply')
+class StageReplyHandler(AppHandler):
+ """
+ 阶段回复接口。
+
+ 对接数字城管系统的阶段回复接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.dcm_task: Optional[DcmTask] = None
+ self.dcm_stage_reply: Optional[DcmStageReply] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ DcmStageReply.flow_token.key: kwargs.get('flowToken', ''),
+ DcmStageReply.dcm_task_id.key: kwargs.get('gdId', ''),
+ DcmStageReply.rec_id.key: self.dcm_task.rec_id,
+ DcmStageReply.act_id.key: self.dcm_task.act_id,
+ DcmStageReply.content.key: kwargs.get('content', ''),
+ DcmStageReply.task_number.key: kwargs.get('taskNumber', ''),
+ DcmStageReply.item_type.key: kwargs.get('itemType', 'stage_reply'),
+ }
+
+ async def stage_reply(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = ['gdId', 'taskNumber', 'content', 'flowToken']
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ dcm_task_id = kwargs.get('gdId', '')
+ self.dcm_task = await DcmTask.async_find_by_id(dcm_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.dcm_stage_reply = DcmStageReply().copy_from_dict(params)
+ self.dcm_stage_reply.status = 1
+ await self.dcm_stage_reply.async_save()
+
+ # 后台执行提交阶段回复请求到数字城管
+ await aio_pool.run_background_task(
+ dcm_push_stage_reply.push_stage_reply(self.dcm_stage_reply, self.dcm_task)
+ )
+
+ return {
+ 'msg': '阶段回复成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 阶段回复接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.stage_reply(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/govc/__init__.py b/apps/api/govc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/api/govs/__init__.py b/apps/api/govs/__init__.py
new file mode 100644
index 0000000..0957e70
--- /dev/null
+++ b/apps/api/govs/__init__.py
@@ -0,0 +1,8 @@
+"""
+省12345接口。
+"""
+
+ApiPrefix = "/system"
+"""
+API 前缀。
+"""
diff --git a/apps/api/govs/__pycache__/__init__.cpython-311.pyc b/apps/api/govs/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b7a9510501c03190f209b225b47847bd71c1ecfd
GIT binary patch
literal 243
zcmZ3^%ge<81UXK;S?)mkF^B^Lj8MjBJ|JT{LkdF_LkeRQV+vC+gC=vSFi?=|`J6^W
zBV!ZOXMIbb_Ah?i(8N{6u3ubPT#}mWr^$4SJw84qKRG`B7N=uDW!>jzYpWaQ`RyBK@wCl(YG1Bp!i^!&17{rLFI
zyv&mLc)fzkUmP~M`6;D2sdh!|K(j#ZELH##AD9^#89y*FF|vGM01+GvykZTU2!SG2
GpdtX+LO~q>
literal 0
HcmV?d00001
diff --git a/apps/api/govs/create_order_delay.py b/apps/api/govs/create_order_delay.py
new file mode 100644
index 0000000..acd6bda
--- /dev/null
+++ b/apps/api/govs/create_order_delay.py
@@ -0,0 +1,101 @@
+from typing import Optional
+import logging
+
+from apps.api import govs
+from apps.app_handler import AppHandler
+from paste.web.decorators import route
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from dock.govs import govs_create_order_delay
+from models.govs_order_master import GovsOrderMaster
+from models.govs_create_delay import GovsApplicationForDelay
+
+
+@route(f'{govs.ApiPrefix}/application-for-delay-formal/create')
+class CreateDelayHandler(AppHandler):
+ """
+ 申请延期接口。
+
+ 对接省12345的申请延期接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.govs_order: Optional[GovsOrderMaster] = None
+ self.govs_delay: Optional[GovsApplicationForDelay] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ GovsApplicationForDelay.master_id.key: kwargs.get('gdId', ''),
+ GovsApplicationForDelay.gd_id.key: kwargs.get('gdId', ''),
+ GovsApplicationForDelay.finally_time_after_approve.key: kwargs.get('finallyTimeAfterApprove', ''),
+ GovsApplicationForDelay.finally_time_before_approve.key: kwargs.get('finallyTimeBeforeApprove', ''),
+ GovsApplicationForDelay.request_delay.key: kwargs.get('requestDelay', ''),
+ GovsApplicationForDelay.is_nature_day.key: kwargs.get('isNatureDay', ''),
+ GovsApplicationForDelay.already_notify_order_user.key: kwargs.get('alreadyNotifyOrderUser', ''),
+ GovsApplicationForDelay.request_reason.key: kwargs.get('requestReason', ''),
+ GovsApplicationForDelay.remarks.key: kwargs.get('remarks', ''),
+ GovsApplicationForDelay.contact_name.key: kwargs.get('contactName', ''),
+ GovsApplicationForDelay.contact_time.key: kwargs.get('contactTime', ''),
+ GovsApplicationForDelay.contact_type.key: kwargs.get('contactType', ''),
+ GovsApplicationForDelay.contact_type_name.key: kwargs.get('contactTypeName', ''),
+ GovsApplicationForDelay.reply_script.key: kwargs.get('replyScript', ''),
+ GovsApplicationForDelay.file_id_str.key: kwargs.get('fileIdStr', ''),
+ GovsApplicationForDelay.request_delay_time.key: kwargs.get('requestDelayTime', ''),
+ GovsApplicationForDelay.flow_token.key: kwargs.get('flowToken', '')
+ }
+
+ async def create_delay(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = [
+ 'gdId', 'flowToken', 'finallyTimeAfterApprove', 'requestDelay', 'isNatureDay',
+ 'alreadyNotifyOrderUser', 'requestReason'
+ ]
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ govs_task_id = kwargs.get('gdId', '')
+ self.govs_order = await GovsOrderMaster.async_find_by_id(govs_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.govs_delay = GovsApplicationForDelay().copy_from_dict(params)
+ self.govs_delay.status = 0
+ await self.govs_delay.async_save()
+
+ # 后台执行提交申请延期请求到省12345
+ await aio_pool.run_background_task(
+ govs_create_order_delay.create_delay(self.govs_delay, self.govs_order)
+ )
+
+ return {
+ 'msg': '申请延期成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 申请延期接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.create_delay(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/govs/create_order_return.py b/apps/api/govs/create_order_return.py
new file mode 100644
index 0000000..c00a5d2
--- /dev/null
+++ b/apps/api/govs/create_order_return.py
@@ -0,0 +1,102 @@
+from typing import Optional
+import logging
+
+from apps.api import govs
+from apps.app_handler import AppHandler
+from paste.web.decorators import route
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from dock.govs import govs_create_order_return
+from models.govs_order_master import GovsOrderMaster
+from models.govs_create_return import GovsWorkOrderReturnFormal
+
+
+@route(f'{govs.ApiPrefix}/work-order-return-formal/create')
+class CreateDelayHandler(AppHandler):
+ """
+ 申请工单退回接口。
+
+ 对接省12345的申请退回接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.govs_order: Optional[GovsOrderMaster] = None
+ self.govs_return: Optional[GovsWorkOrderReturnFormal] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ GovsWorkOrderReturnFormal.master_id.key: kwargs.get('gdId', ''),
+ GovsWorkOrderReturnFormal.flow_token.key: kwargs.get('flowToken', ''),
+ GovsWorkOrderReturnFormal.gd_id.key: kwargs.get('gdId', ''),
+ GovsWorkOrderReturnFormal.return_reason.key: kwargs.get('returnReason', ''),
+ GovsWorkOrderReturnFormal.return_reason_name.key: kwargs.get('returnReasonName', ''),
+ GovsWorkOrderReturnFormal.return_auditor_name.key: kwargs.get('returnAuditorName', ''),
+ GovsWorkOrderReturnFormal.return_auditor_id.key: kwargs.get('returnAuditorId', ''),
+ GovsWorkOrderReturnFormal.deal_opinion.key: kwargs.get('dealOpinion', ''),
+ GovsWorkOrderReturnFormal.reason.key: kwargs.get('reason', ''),
+ GovsWorkOrderReturnFormal.remark.key: kwargs.get('remark', ''),
+ GovsWorkOrderReturnFormal.file_id_str.key: kwargs.get('fileIdStr', ''),
+ GovsWorkOrderReturnFormal.process_instance_id.key: kwargs.get('processInstanceId', ''),
+ GovsWorkOrderReturnFormal.action_name.key: kwargs.get('actionName', ''),
+ GovsWorkOrderReturnFormal.order_id.key: kwargs.get('orderId', ''),
+ GovsWorkOrderReturnFormal.task_id.key: kwargs.get('taskId', ''),
+ GovsWorkOrderReturnFormal.order_no.key: kwargs.get('orderNo', ''),
+ GovsWorkOrderReturnFormal.case_accord_type_one_name.key: kwargs.get('caseAccordTypeOneName', ''),
+ GovsWorkOrderReturnFormal.case_accord_type_two_name.key: kwargs.get('caseAccordTypeTwoName', ''),
+ GovsWorkOrderReturnFormal.case_accord_type_three_name.key: kwargs.get('caseAccordTypeThreeName', '')
+ }
+
+ async def create_return(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = [
+ 'gdId', 'flowToken', 'returnReason', 'returnReasonName', 'dealOpinion', 'reason'
+ ]
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ govs_task_id = kwargs.get('gdId', '')
+ self.govs_order = await GovsOrderMaster.async_find_by_id(govs_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.govs_return = GovsWorkOrderReturnFormal().copy_from_dict(params)
+ self.govs_return.status = 0
+ await self.govs_return.async_save()
+
+ # 后台执行提交申请延期请求到省12345
+ await aio_pool.run_background_task(
+ govs_create_order_return.create_return(self.govs_return, self.govs_order)
+ )
+
+ return {
+ 'msg': '申请退回成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 申请退回接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.create_return(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/govs/create_reply.py b/apps/api/govs/create_reply.py
new file mode 100644
index 0000000..3abf3e6
--- /dev/null
+++ b/apps/api/govs/create_reply.py
@@ -0,0 +1,102 @@
+from typing import Optional
+import logging
+
+from apps.api import govs
+from apps.app_handler import AppHandler
+from paste.web.decorators import route
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from dock.govs import govs_create_reply
+from models.govs_order_master import GovsOrderMaster
+from models.govs_create_reply import GovsReplyFormal
+
+
+@route(f'{govs.ApiPrefix}/reply-formal/create')
+class CreateDelayHandler(AppHandler):
+ """
+ 答复办结接口。
+
+ 对接省12345的答复办结接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.govs_order: Optional[GovsOrderMaster] = None
+ self.govs_reply: Optional[GovsReplyFormal] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ GovsReplyFormal.master_id.key: kwargs.get('gdId', ''),
+ GovsReplyFormal.flow_token.key: kwargs.get('flowToken', ''),
+ GovsReplyFormal.gd_id.key: kwargs.get('gdId', ''),
+ GovsReplyFormal.is_contact.key: kwargs.get('isContact', ''),
+ GovsReplyFormal.contact_name.key: kwargs.get('contactName', ''),
+ GovsReplyFormal.contact_time.key: kwargs.get('contactTime', ''),
+ GovsReplyFormal.contact_type.key: kwargs.get('contactType', ''),
+ GovsReplyFormal.advice.key: kwargs.get('advice', ''),
+ GovsReplyFormal.reason.key: kwargs.get('reason', ''),
+ GovsReplyFormal.remarks.key: kwargs.get('remarks', ''),
+ GovsReplyFormal.file_id_str.key: kwargs.get('fileIdStr', ''),
+ GovsReplyFormal.save_id.key: kwargs.get('saveId', ''),
+ GovsReplyFormal.process_instance_id.key: kwargs.get('processInstanceId', ''),
+ GovsReplyFormal.business_key.key: kwargs.get('businessKey', ''),
+ GovsReplyFormal.order_no.key: kwargs.get('orderNo', ''),
+ GovsReplyFormal.action_name.key: kwargs.get('actionName', ''),
+ GovsReplyFormal.case_accord_type_one_name.key: kwargs.get('caseAccordTypeOneName', ''),
+ GovsReplyFormal.case_accord_type_two_name.key: kwargs.get('caseAccordTypeTwoName', ''),
+ GovsReplyFormal.case_accord_type_three_name.key: kwargs.get('caseAccordTypeThreeName', ''),
+ }
+
+ async def create_delay(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = [
+ 'gdId', 'flowToken', 'isContact', 'contactType', 'advice', 'reason'
+ ]
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ govs_task_id = kwargs.get('gdId', '')
+ self.govs_order = await GovsOrderMaster.async_find_by_id(govs_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.govs_reply = GovsReplyFormal().copy_from_dict(params)
+ self.govs_reply.status = 0
+ await self.govs_reply.async_save()
+
+ # 后台执行提交答复办结请求到省12345
+ await aio_pool.run_background_task(
+ govs_create_reply.create_reply(self.govs_reply, self.govs_order)
+ )
+
+ return {
+ 'msg': '答复办结成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 答复办结接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.create_delay(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/govs/phase_wise_completion.py b/apps/api/govs/phase_wise_completion.py
new file mode 100644
index 0000000..e364683
--- /dev/null
+++ b/apps/api/govs/phase_wise_completion.py
@@ -0,0 +1,101 @@
+from typing import Optional
+import logging
+
+from apps.api import govs
+from apps.app_handler import AppHandler
+from paste.web.decorators import route
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from dock.govs import govs_phase_wise_completion
+from models.govs_order_master import GovsOrderMaster
+from models.govs_phase_wise_completion import GovsPhaseWiseCompletion
+
+
+@route(f'{govs.ApiPrefix}/phase-wise-completion/create')
+class CreateDelayHandler(AppHandler):
+ """
+ 阶段性办结接口。
+
+ 对接省12345的阶段性办结接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.govs_order: Optional[GovsOrderMaster] = None
+ self.phase_wise_completion: Optional[GovsPhaseWiseCompletion] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ GovsPhaseWiseCompletion.master_id.key: kwargs.get('gdId', ''),
+ GovsPhaseWiseCompletion.flow_token.key: kwargs.get('flowToken', ''),
+ GovsPhaseWiseCompletion.gd_id.key: kwargs.get('gdId', ''),
+ GovsPhaseWiseCompletion.is_contact.key: kwargs.get('isContact', ''),
+ GovsPhaseWiseCompletion.contact_name.key: kwargs.get('contactName', ''),
+ GovsPhaseWiseCompletion.contact_time.key: kwargs.get('contactTime', ''),
+ GovsPhaseWiseCompletion.contact_type.key: kwargs.get('contactType', ''),
+ GovsPhaseWiseCompletion.next_feedback_time.key: kwargs.get('nextFeedbackTime', ''),
+ GovsPhaseWiseCompletion.advice.key: kwargs.get('advice', ''),
+ GovsPhaseWiseCompletion.reason.key: kwargs.get('reason', ''),
+ GovsPhaseWiseCompletion.remark.key: kwargs.get('remark', ''),
+ GovsPhaseWiseCompletion.action_name.key: kwargs.get('actionName', ''),
+ GovsPhaseWiseCompletion.case_accord_type_one_name.key: kwargs.get('caseAccordTypeOneName', ''),
+ GovsPhaseWiseCompletion.case_accord_type_two_name.key: kwargs.get('caseAccordTypeTwoName', ''),
+ GovsPhaseWiseCompletion.case_accord_type_three_name.key: kwargs.get('caseAccordTypeThreeName', ''),
+ GovsPhaseWiseCompletion.order_id.key: kwargs.get('orderId', ''),
+ GovsPhaseWiseCompletion.task_id.key: kwargs.get('taskId', '')
+ }
+
+ async def create_delay(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = [
+ 'gdId', 'flowToken', 'isContact', 'contactName', 'contactTime', 'contactType', 'nextFeedbackTime', 'advice',
+ 'reason'
+ ]
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ govs_task_id = kwargs.get('gdId', '')
+ self.govs_order = await GovsOrderMaster.async_find_by_id(govs_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.phase_wise_completion = GovsPhaseWiseCompletion().copy_from_dict(params)
+ self.phase_wise_completion.status = 0
+ await self.phase_wise_completion.async_save()
+
+ # 后台执行提交阶段性办结请求到省12345
+ await aio_pool.run_background_task(
+ govs_phase_wise_completion.create_phase_wise_completion(self.phase_wise_completion, self.govs_order)
+ )
+
+ return {
+ 'msg': '阶段性办结成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 阶段性办结接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.create_delay(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/api/govs/save_sign.py b/apps/api/govs/save_sign.py
new file mode 100644
index 0000000..f515d82
--- /dev/null
+++ b/apps/api/govs/save_sign.py
@@ -0,0 +1,91 @@
+from typing import Optional
+import logging
+
+from apps.api import govs
+from apps.app_handler import AppHandler
+from paste.web.decorators import route
+from paste.core import aio_pool
+from paste.core.logging import echo_log
+from dock.govs import govs_save_sign
+from models.govs_order_master import GovsOrderMaster
+from models.govs_save_sign import GovsSaveSign
+
+
+@route(f'{govs.ApiPrefix}/save-sign-for/create')
+class CreateDelayHandler(AppHandler):
+ """
+ 申请延期接口。
+
+ 对接省12345的申请延期接口,请求后本接口先将数据保存本地,然后响应客户端,然后开始后台启动推送。
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.govs_order: Optional[GovsOrderMaster] = None
+ self.govs_sign: Optional[GovsSaveSign] = None
+
+ def _params_for_db(self, **kwargs: dict) -> dict:
+ """
+ 提取数据库所需参数。
+ """
+ return {
+ GovsSaveSign.gd_id.key: kwargs.get('gdId', ''),
+ GovsSaveSign.flow_token.key: kwargs.get('flowToken', ''),
+ GovsSaveSign.order_id.key: kwargs.get('orderId', ''),
+ GovsSaveSign.order_no.key: kwargs.get('orderNo', ''),
+ GovsSaveSign.master_id.key: kwargs.get('gdId', ''),
+ GovsSaveSign.order_process_id.key: kwargs.get('orderProcessId', ''),
+ GovsSaveSign.task_id.key: kwargs.get('taskId', ''),
+ GovsSaveSign.flag.key: kwargs.get('flag', '')
+ }
+
+ async def create_delay(self, **kwargs) -> dict:
+ # 必填参数校验
+ required_keys = [
+ 'gdId', 'flowToken'
+ ]
+ missing = [
+ k for k in required_keys
+ if k not in kwargs or kwargs[k] is None
+ ]
+ if missing:
+ raise ValueError(f"缺少必要参数: {missing}")
+
+ # 读取待办任务对象
+ govs_task_id = kwargs.get('gdId', '')
+ self.govs_order = await GovsOrderMaster.async_find_by_id(govs_task_id)
+
+ # 保存请求数据
+ params = self._params_for_db(**kwargs)
+ self.govs_sign = GovsSaveSign().copy_from_dict(params)
+ self.govs_sign.status = 0
+ await self.govs_sign.async_save()
+
+ # 后台执行提交申请延期请求到省12345
+ await aio_pool.run_background_task(
+ govs_save_sign.sign_order(self.govs_sign, self.govs_order)
+ )
+
+ return {
+ 'msg': '确认签收成功.'
+ }
+
+ # @auth_token
+ async def post(self):
+ """
+ 处理 POST 请求。
+
+ ---
+ tags:
+ - D3I API
+ summary: 申请延期接口
+ """
+ try:
+ echo_log(self.request.body.decode())
+ _, params = self.get_request_params()
+ _result = await self.create_delay(**params)
+ self.response_ok(code=0, data=_result)
+ except Exception as e:
+ self.response_error(e, status_code=200, api_status_code=500)
+ self.log(msg=e, level=logging.ERROR, is_log_exc=True)
diff --git a/apps/app_handler.py b/apps/app_handler.py
new file mode 100644
index 0000000..7643cf4
--- /dev/null
+++ b/apps/app_handler.py
@@ -0,0 +1,183 @@
+import datetime
+import json
+import os
+from abc import ABC
+from typing import Optional, Callable, Awaitable
+
+from paste.rbac.rbac_user import RbacUser
+from paste.util.encoder import JsonDumpsEncoder
+from paste.web.handler import RequestHandler
+from paste.web.param_aware_loader import ParamAwareLoader
+
+
+class AppHandler(RequestHandler, ABC):
+ """
+ 控制器基类。
+ """
+
+ commands: dict[str, Callable] = {}
+ """
+ API 接口命令字典,其结构为命令名称指向对应的方法。
+
+ 其结构如下::
+
+ {
+ command_name: method
+ }
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.user: Optional[RbacUser] = None
+ """
+ 当前登录用户对象。
+ """
+
+ self.start_at = datetime.datetime.now()
+ """
+ 实例初始化时间。
+ """
+
+ self.command = ""
+ """
+ 命令。
+ """
+
+ self.request_params = {}
+ """
+ 命令参数。
+ """
+
+ async def after_auth_token(self, token_payload: dict):
+ """
+ 初始化登录用户信息。
+ """
+ from paste.security import token
+ if token_payload != token.PRIVATE_ISS:
+ from jwt import InvalidTokenError
+ raise InvalidTokenError()
+
+ async def run_command(self):
+ """
+ 根据请求参数运行命令方法,返回命令执行结果。
+ """
+ self.get_request_params()
+ assert self.command in self.commands, '请提供正确的命令参数.'
+
+ # 读取命令方法对象,并执行
+ _cmd_func = self.commands[self.command]
+ _result = _cmd_func(self, **self.request_params)
+ # 处理异步方法执行
+ if isinstance(_result, Awaitable):
+ _result = await _result
+ return _result
+
+ async def gen_html(self, template_file: str, **kwargs):
+ """
+ 生成 HTML 内容。
+
+ :param template_file: 模板文件
+ :param kwargs: 参数数据字典
+ :return: 返回生成的 HTML 文件内容
+ """
+ # 将参数字典转换为 namedtuple,名称固定
+ template_data_obj = self.dict_to_namedtuple('TemplateData', {**kwargs})
+ # 手动构建完整命名空间,加入自定义参数,传给生成器
+ namespace = self.get_template_namespace()
+ namespace.update({'td': template_data_obj})
+
+ # 获取模板文件
+ template_file = f"{self.application.settings.get('template_path')}/{template_file}"
+ # 用参数感知模板加载器,加载模板文件,并传入 namespace 以便在加载完成后,执行数据准备
+ loader = ParamAwareLoader(os.path.dirname(template_file), namespace=namespace)
+ # 从文件中加载模板,同步完成数据准备
+ template = await loader.load_with_prepare(os.path.basename(template_file))
+
+ # 渲染模板,传入需要的数据
+ output = template.generate(**namespace)
+ return output
+
+ def response_ok(self, **kwargs):
+ self.log_request_end()
+ super().response_ok(**kwargs)
+
+ def response_error(self, e: Exception, status_code: int = 200, api_status_code: int = None, **kwargs):
+ self.log_request_end()
+ if api_status_code is None:
+ api_status_code = status_code
+
+ self.set_status(status_code=status_code)
+ chunk = {'code': api_status_code, 'status': 'error'}
+ chunk.update(kwargs)
+ if len(e.args) > 0 and isinstance(e.args[0], str):
+ chunk['msg'] = e.args[0]
+ if len(e.args) > 1:
+ if isinstance(e.args[1], dict):
+ chunk.update(e.args[1])
+ elif isinstance(e.args[1], list):
+ chunk['errors'] = e.args[1]
+ self.write(json.dumps(chunk, cls=JsonDumpsEncoder, ensure_ascii=False))
+ self.set_header('Content-Type', 'application/json')
+
+ def get_request_params(self):
+ """
+ 读取命令名称及请求参数。注意,参数命名应当避开 cmd 和 params。
+
+ 该方法自动合并参数,并输出请求开始日志。
+
+ 支持通过 Form 或 Json 两种方式提交请求并读取相应参数。
+
+ 如使用 Form 方式,则应当在 form-data 中包含名为 cmd 的输入项,其值为对应的命令,其他输入项为命令参数。
+ 注意:不会自动读取上传的文件数据,可通过::
+
+ self.request.files
+
+ 方法读取上传的文件。
+
+ 如使用 Json 方式,则应当遵循以下结构::
+
+ {
+ cmd: command_name,
+ params:
+ {
+ key: value
+ }
+ }
+
+ :return: 命令,命令对应的参数
+ """
+ _arguments = self.request_arguments()
+ _cmd = _arguments.get('cmd', None)
+ _params = _arguments.get('params', None)
+ if _params is None:
+ _arguments.pop('cmd', None)
+ _params = _arguments
+
+ self.command = _cmd
+ self.request_params = _params
+ # 合成规则参数到参数字典,规则参数可在相应规则中修改
+ self.request_params.update(self.rule_kwargs)
+ # 取得命令和参数之后,记录请求开始日志
+ self.log_request_start()
+
+ return self.command, self.request_params
+
+ def log_request_end(self):
+ end_at = datetime.datetime.now()
+ total_delta = (end_at - self.start_at).total_seconds()
+ _spend = f"耗时:{total_delta:f} 秒."
+
+ _user_name = self.user.username if self.user else 'Unknown'
+ _log = f"O 用户:{_user_name} 完成 {self.request.uri}"
+ _log = f"{_log} 接口命令 {self.command},{_spend}" if self.command else f"{_log} 请求,{_spend}"
+ self.log(_log)
+
+ def log_request_start(self):
+ """
+ 收到请求时记录的日志
+ """
+ _user_name = self.user.username if self.user else 'Unknown'
+ _log = f"I 用户:{_user_name} 请求 {self.request.uri}"
+ _log = f"{_log} 接口命令 {self.command}." if self.command else f"{_log}."
+ self.log(_log)
diff --git a/base/__init__.py b/base/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/base/__pycache__/__init__.cpython-311.pyc b/base/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..03d3c90decf3633725ff734c8546a663d1769823
GIT binary patch
literal 159
zcmZ3^%ge<81RDaGvq1D?5CH>>P{wCAAY(d13PUi1CZpd&ryk0@&FAkgB{FKt1
dRJ$Tppb;QTiur-W2WCb_#t#fIqKFwN1^_*OBdY)a
literal 0
HcmV?d00001
diff --git a/base/__pycache__/conn_pool.cpython-311.pyc b/base/__pycache__/conn_pool.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..94bd50570dbd16333f2f184d8e2d5cee176ab07a
GIT binary patch
literal 1991
zcmbtUZ%7+w7=JIholoRr-_3RB{@5{T1P3!CSx75j=~1nhj8YOHhQ^pcbBa~
z3ZWFn@Xv(SZ5^~Yn6|^MwUm{yS_*@Gzl4LxK|rX9`e^~#m%?DrdAyb5OQ7&pr7)
z&-1>|pZCvm%j0n&7~MbqmH63#&=V|J!&U|R4*-0PIK&YGisFBpKqPHZ8-cN1uqVkV
znRG-Qrc4UXBo(DFX6XjRqe;sjT5hPp17H>KT1{qM4S1#dKdd|gD;-x*)WbP}H*&-|
z6r%R9=dloBIE93w@?ywlkmHgh7!4n!_!N&JHuB>6gviJ3wLB0a0b_apdjglQ0=LFw
z4c8|>X>A$0WUrfF^ZT#!N%g?yq)F90mP));@VZv>thmm=LDc)XOr-70PAj{E|Ml}O
zIn>5y*0f{F6y%CY!Y_7YY4U}lJ5{gc7ooJX8aE818FF);kd!-^47Z?86nA=&KEzaLBeQ|5|
zyE!mz|2n(<@WGSJ<=w~E_dcJ4wWS*?yLZ-$kCrN~iuc!xnT^uYdTIU+Of%!8UV@0l
zjiu7ng`Im3i#KQYu3g`~dp9x`^2qo=j8;~e5#zDAB*OJlW0iYmkkj}IHa@3yC6t7y
zsH_<0Wq6FpaK$h1@hP5@QmWB9f*s-e`aXN{Ev!EaTAE{3UQHxDGspEb$Es$tM$jk}l#I^~#?wLOLrF*_d4>5%lBbw)S(=3HQ<(7?^@1ca
zZx6o9Ot1>i-~`M7k?9%34VhWqMqRyn=TNqDSjTiabEeR8OmBHL
z+Y-_-MKbRd+--|fE5liLSa*jtaugFYnk7ed0Pb+P5%~gnZ-36)pY?`yZ#Z+dKy`d0
z<*2Ymg*Uq+8r@k4g!I5``M`->;6yfXQV*Qe=$E&A!Mrb=^M$iMM)xrq*;=4FwQeRy
zF&f2e_6%rrS0UJ^2M6=PpevVXgZ+A
zcczkn`4H%77Mgd2n=l`h>;W%w6K1$+$wx73!3-a|nUdU%Q7!AI^r@u8r3C)8{0bOw
z`&NDfR<;uav5A^8=5Gu2YW1{<`nAJo3w3DqwB tuple[str, str, str]:
+ """
+ 从 user_agents 列表中随机返回一个 User-Agent 字符串及其浏览器版本和操作系统名称。
+
+ Returns:
+ tuple: (user_agent: str, browser_version: str, os_name: str)
+ """
+ ua: str = random.choice(USER_AGENTS)
+
+ # 提取浏览器版本
+ browser_version: str = "Unknown"
+ if "Chrome/" in ua:
+ match = re.search(r"Chrome/(\d+\.\d+\.\d+\.\d+)", ua)
+ if match:
+ browser_version = match.group(1)
+ elif "Firefox/" in ua:
+ match = re.search(r"Firefox/(\d+\.\d+)", ua)
+ if match:
+ browser_version = match.group(1)
+ elif "Version/" in ua and "Safari/" in ua: # Safari
+ match = re.search(r"Version/(\d+\.\d+)", ua)
+ if match:
+ browser_version = match.group(1)
+ elif "Edg/" in ua:
+ match = re.search(r"Edg/(\d+\.\d+\.\d+\.\d+)", ua)
+ if match:
+ browser_version = match.group(1)
+ elif "OPR/" in ua:
+ match = re.search(r"OPR/(\d+\.\d+\.\d+\.\d+)", ua)
+ if match:
+ browser_version = match.group(1)
+
+ # 提取操作系统名称
+ os_name: str = "Unknown"
+ if "Mac" in ua:
+ os_name = "Mac"
+ elif "Windows" in ua:
+ os_name = "Windows"
+ elif "Linux" in ua:
+ os_name = "Linux"
+
+ return ua, browser_version, os_name
+
+
+def get_cookies(response: HTTPResponse) -> Dict[str, str]:
+ """
+ 从响应对象读取 Cookies。
+
+ :param response: 请求响应对象
+ :return: 提取到的 Cookies
+ """
+ cookies = SimpleCookie()
+ for set_cookie in response.headers.get_list('Set-Cookie'):
+ cookies.load(set_cookie)
+ return {k: v.value for k, v in cookies.items()}
+
+
+def get_cookie_value(cookies_string, cookie_name):
+ """
+ 从 cookies 字符串中按名称提取对应的值
+
+ 参数:
+ cookies_string: str, 格式为 "key1=value1; key2=value2; ..."
+ cookie_name: str, 要查找的 cookie 名称
+
+ 返回:
+ str 或 None: 如果找到返回对应的值,否则返回 None
+ """
+ # 按分号分割 cookie 字符串
+ cookies_list = cookies_string.split(';')
+
+ for cookie in cookies_list:
+ # 去除前后空格,然后按等号分割键值对
+ parts = cookie.strip().split('=', 1)
+ if len(parts) == 2:
+ key, value = parts
+ if key == cookie_name:
+ return value
+
+ # 如果没有找到,返回 None
+ return None
+
+
+def new_http_request(
+ url: str,
+ body: Optional[Dict[str, Any]] = None,
+ method: str = 'POST',
+ timeout: Optional[float] = None,
+ follow_redirects: bool = True,
+ use_form: bool = False,
+ extra_headers: Optional[Dict[str, str]] = None,
+ **kwargs
+) -> HTTPRequest:
+ """
+ 新建 HTTPRequest 对象。
+
+ 支持 GET 和 POST 方法:
+ - GET: 参数通过 URL 查询字符串传递
+ - POST: 参数通过 JSON body 或 form 表单传递(由 use_form 控制)
+
+ :param url: 请求的完整 URL
+ :param body: 请求体(字典),GET 时为查询参数,POST 时为 JSON 或 form 数据
+ :param method: HTTP 方法,仅支持 'GET' 或 'POST'
+ :param timeout: 请求超时时间(秒)
+ :param follow_redirects: 是否跟随重定向
+ :param use_form: 如果为 True,POST 时使用 application/x-www-form-urlencoded 格式;否则使用 JSON
+ :param extra_headers: 可选的额外请求头,用于传入 Cookie、Authorization 等
+ :param kwargs: 其他参数,符合 tornado.httpclient.HTTPRequest 参数要求
+ :return: tornado.httpclient.HTTPRequest 对象
+ :raises ValueError: 当 method 不合法时抛出
+ """
+ return requests.build_http_request(
+ url, body, method, timeout, follow_redirects, use_form, extra_headers,
+ **kwargs
+ )
+
+
+async def scrape_cookies(url: str, timeout: Optional[float] = 10,
+ extra_headers: Optional[Dict[str, str]] = None,
+ **kwargs) -> Dict[str, str]:
+ """
+ 发送 GET 请求到 url 以获取服务端下发的 Cookies(如 JSESSIONID)。
+ 不关心响应体,只提取响应头中的 Set-Cookie。
+ 返回解析后的 Cookie 字典 { 'name': 'value', ... }
+
+ :param url: 获取 Cookies 的路径
+ :param timeout: 超时时间
+ :param extra_headers: 扩展头
+ :return: Cookies 读取到的 Cookies
+ """
+ cookies: Optional[Dict[str, str]] = None
+
+ request = new_http_request(
+ url=url,
+ method='GET',
+ timeout=timeout,
+ follow_redirects=True,
+ extra_headers=extra_headers,
+ **kwargs
+ )
+
+ def after_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]):
+ nonlocal cookies
+ cookies = get_cookies(response)
+
+ request_queue = asyncio.Queue()
+ await request_queue.put(request)
+ await requests.async_concurrency(
+ request_queue, con_count=1, retry=MAX_RETRY_COUNT,
+ after_request=after_request
+ )
+
+ if cookies is None:
+ # 无正确响应,抛出异常
+ raise Exception(f"未能读取到 Cookies.")
+
+ return cookies
diff --git a/dock/__pycache__/__init__.cpython-311.pyc b/dock/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..65683087fbb6416e659bc3c7191b11af3e493c3b
GIT binary patch
literal 8149
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
literal 0
HcmV?d00001
diff --git a/dock/dcm/__init__.py b/dock/dcm/__init__.py
new file mode 100644
index 0000000..e25783d
--- /dev/null
+++ b/dock/dcm/__init__.py
@@ -0,0 +1,3 @@
+"""
+数字城管对接模块。
+"""
\ No newline at end of file
diff --git a/dock/dcm/__pycache__/__init__.cpython-311.pyc b/dock/dcm/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f9b247815de56c1dff87835be9038e46a1d21450
GIT binary patch
literal 217
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