From 646a4d02c0298b0a66205b8b5a3534366ebacb1e Mon Sep 17 00:00:00 2001 From: zwf <2466627138@qq.com> Date: Tue, 2 Jun 2026 17:46:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/vcs.xml | 4 + README.md | 130 + apps/__init__.py | 32 + apps/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1031 bytes apps/__pycache__/app_handler.cpython-311.pyc | Bin 0 -> 8859 bytes apps/api/__init__.py | 0 apps/api/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 163 bytes apps/api/dcm/__init__.py | 8 + .../dcm/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 270 bytes .../apply_postpone.cpython-311.pyc | Bin 0 -> 6707 bytes .../apply_rollback.cpython-311.pyc | Bin 0 -> 6372 bytes .../dcm/__pycache__/dispose.cpython-311.pyc | Bin 0 -> 6077 bytes .../fetch_allow_postpone.cpython-311.pyc | Bin 0 -> 3619 bytes .../fetch_dispose_form.cpython-311.pyc | Bin 0 -> 3570 bytes .../fetch_operation.cpython-311.pyc | Bin 0 -> 3560 bytes .../fetch_rollback_form.cpython-311.pyc | Bin 0 -> 3565 bytes .../dcm/__pycache__/rollback.cpython-311.pyc | Bin 0 -> 6457 bytes .../__pycache__/stage_reply.cpython-311.pyc | Bin 0 -> 5515 bytes apps/api/dcm/apply_postpone.py | 102 + apps/api/dcm/apply_rollback.py | 97 + apps/api/dcm/dispose.py | 96 + apps/api/dcm/fetch_allow_postpone.py | 58 + apps/api/dcm/fetch_dispose_form.py | 59 + apps/api/dcm/fetch_operation.py | 58 + apps/api/dcm/fetch_rollback_form.py | 59 + apps/api/dcm/rollback.py | 98 + apps/api/dcm/stage_reply.py | 91 + apps/api/govc/__init__.py | 0 apps/api/govs/__init__.py | 8 + .../govs/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 243 bytes apps/api/govs/create_order_delay.py | 101 + apps/api/govs/create_order_return.py | 102 + apps/api/govs/create_reply.py | 102 + apps/api/govs/phase_wise_completion.py | 101 + apps/api/govs/save_sign.py | 91 + apps/app_handler.py | 183 ++ base/__init__.py | 0 base/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 159 bytes base/__pycache__/conn_pool.cpython-311.pyc | Bin 0 -> 1991 bytes base/conn_pool.py | 54 + cli.py | 360 +++ dock/__init__.py | 215 ++ dock/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 8149 bytes dock/dcm/__init__.py | 3 + dock/dcm/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 217 bytes dock/dcm/__pycache__/dcm_api.cpython-311.pyc | Bin 0 -> 1791 bytes .../dcm_push_apply_postpone.cpython-311.pyc | Bin 0 -> 11265 bytes .../dcm_push_apply_rollback.cpython-311.pyc | Bin 0 -> 10510 bytes .../dcm_push_conv_dispose.cpython-311.pyc | Bin 0 -> 3457 bytes .../dcm_push_conv_rollback.cpython-311.pyc | Bin 0 -> 3631 bytes .../dcm_push_dispose.cpython-311.pyc | Bin 0 -> 11379 bytes .../dcm_push_rollback.cpython-311.pyc | Bin 0 -> 11457 bytes .../dcm_push_stage_reply.cpython-311.pyc | Bin 0 -> 4603 bytes .../dcm_push_upload.cpython-311.pyc | Bin 0 -> 9075 bytes .../__pycache__/dcm_scrape.cpython-311.pyc | Bin 0 -> 10394 bytes .../dcm_scrape_allow_postpone.cpython-311.pyc | Bin 0 -> 4480 bytes .../dcm_scrape_attachment.cpython-311.pyc | Bin 0 -> 3657 bytes .../dcm_scrape_conv_dispose.cpython-311.pyc | Bin 0 -> 4038 bytes .../dcm_scrape_conv_rollback.cpython-311.pyc | Bin 0 -> 4018 bytes ...dcm_scrape_convenient_form.cpython-311.pyc | Bin 0 -> 4737 bytes .../dcm_scrape_extend_info.cpython-311.pyc | Bin 0 -> 3495 bytes .../dcm_scrape_form_data.cpython-311.pyc | Bin 0 -> 4090 bytes .../dcm_scrape_more_info.cpython-311.pyc | Bin 0 -> 3482 bytes .../dcm_scrape_operation.cpython-311.pyc | Bin 0 -> 6862 bytes .../dcm_scrape_process_info.cpython-311.pyc | Bin 0 -> 3859 bytes .../dcm_scrape_task.cpython-311.pyc | Bin 0 -> 4082 bytes .../__pycache__/dcm_security.cpython-311.pyc | Bin 0 -> 5567 bytes .../__pycache__/dcm_send_sms.cpython-311.pyc | Bin 0 -> 2617 bytes dock/dcm/dcm_api.py | 53 + dock/dcm/dcm_push_apply_postpone.py | 216 ++ dock/dcm/dcm_push_apply_rollback.py | 210 ++ dock/dcm/dcm_push_conv_dispose.py | 74 + dock/dcm/dcm_push_conv_rollback.py | 75 + dock/dcm/dcm_push_dispose.py | 233 ++ dock/dcm/dcm_push_rollback.py | 235 ++ dock/dcm/dcm_push_stage_reply.py | 109 + dock/dcm/dcm_push_upload.py | 197 ++ dock/dcm/dcm_scrape.py | 160 ++ dock/dcm/dcm_scrape_allow_postpone.py | 86 + dock/dcm/dcm_scrape_attachment.py | 52 + dock/dcm/dcm_scrape_conv_dispose.py | 73 + dock/dcm/dcm_scrape_conv_rollback.py | 73 + dock/dcm/dcm_scrape_convenient_form.py | 85 + dock/dcm/dcm_scrape_extend_info.py | 50 + dock/dcm/dcm_scrape_form_data.py | 61 + dock/dcm/dcm_scrape_more_info.py | 50 + dock/dcm/dcm_scrape_operation.py | 119 + dock/dcm/dcm_scrape_process_info.py | 54 + dock/dcm/dcm_scrape_task.py | 67 + dock/dcm/dcm_security.py | 115 + dock/dcm/dcm_send_sms.py | 51 + dock/govc/__init__.py | 3 + .../govc/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 214 bytes .../govc/__pycache__/govc_api.cpython-311.pyc | Bin 0 -> 2507 bytes dock/govc/govc_api.py | 65 + dock/govc/govc_scrape.py | 94 + dock/govc/govc_scrape_contact_info.py | 59 + dock/govc/govc_scrape_delay_info.py | 59 + dock/govc/govc_scrape_dept_feedback.py | 66 + dock/govc/govc_scrape_detail.py | 67 + dock/govc/govc_scrape_finish_info.py | 59 + dock/govc/govc_scrape_order.py | 238 ++ dock/govc/govc_scrape_order_status.py | 59 + dock/govc/govc_scrape_process.py | 58 + dock/govc/govc_scrape_requester.py | 56 + dock/govc/govc_scrape_return_visit.py | 59 + dock/govc/govc_scrapy_history_order.py | 56 + dock/govc/govc_security.py | 382 +++ dock/govs/__init__.py | 3 + .../govs/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 214 bytes .../govs/__pycache__/govs_api.cpython-311.pyc | Bin 0 -> 2335 bytes .../govs_scrape_order_detail.cpython-311.pyc | Bin 0 -> 12724 bytes .../govs_scrape_order_master.cpython-311.pyc | Bin 0 -> 7559 bytes .../govs_scrape_order_process.cpython-311.pyc | Bin 0 -> 12159 bytes .../__pycache__/govs_security.cpython-311.pyc | Bin 0 -> 4978 bytes dock/govs/govs_api.py | 61 + dock/govs/govs_create_order_delay.py | 113 + dock/govs/govs_create_order_return.py | 301 ++ dock/govs/govs_create_reply.py | 306 +++ dock/govs/govs_download_file.py | 22 + dock/govs/govs_phase_wise_completion.py | 281 ++ dock/govs/govs_save_sign.py | 123 + dock/govs/govs_scrape.py | 106 + dock/govs/govs_scrape_order_detail.py | 159 ++ dock/govs/govs_scrape_order_master.py | 156 ++ dock/govs/govs_scrape_order_process.py | 154 ++ dock/govs/govs_security.py | 116 + dock/govs/govs_upload_file.py | 17 + dock/oa/__init__.py | 12 + dock/oa/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 936 bytes dock/oa/__pycache__/oa_api.cpython-311.pyc | Bin 0 -> 3452 bytes .../oa_api_request.cpython-311.pyc | Bin 0 -> 18880 bytes .../oa_result_notify.cpython-311.pyc | Bin 0 -> 3109 bytes .../__pycache__/oa_security.cpython-311.pyc | Bin 0 -> 4608 bytes dock/oa/oa_api.py | 87 + dock/oa/oa_api_request.py | 469 ++++ dock/oa/oa_result_notify.py | 55 + dock/oa/oa_security.py | 96 + dock/oa_dcm/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 166 bytes .../oa_push_attachment.cpython-311.pyc | Bin 0 -> 8652 bytes .../oa_push_extend_info.cpython-311.pyc | Bin 0 -> 8477 bytes .../oa_push_more_info.cpython-311.pyc | Bin 0 -> 8693 bytes .../__pycache__/oa_push_order.cpython-311.pyc | Bin 0 -> 12805 bytes .../oa_push_order_detail.cpython-311.pyc | Bin 0 -> 10190 bytes .../oa_push_process_info.cpython-311.pyc | Bin 0 -> 10690 bytes .../__pycache__/oa_sign_task.cpython-311.pyc | Bin 0 -> 5858 bytes .../oa_update_post_delay.cpython-311.pyc | Bin 0 -> 4808 bytes .../__pycache__/oa_upload.cpython-311.pyc | Bin 0 -> 12227 bytes dock/oa_dcm/oa_push_attachment.py | 140 + dock/oa_dcm/oa_push_extend_info.py | 135 + dock/oa_dcm/oa_push_more_info.py | 139 + dock/oa_dcm/oa_push_order.py | 241 ++ dock/oa_dcm/oa_push_order_detail.py | 165 ++ dock/oa_dcm/oa_push_process_info.py | 164 ++ dock/oa_dcm/oa_sign_task.py | 98 + dock/oa_dcm/oa_update_post_delay.py | 88 + dock/oa_dcm/oa_upload.py | 204 ++ dock/oa_govc/__init__.py | 0 dock/oa_govs/__init__.py | 0 dock/oa_govs/govs_push_detail.py | 193 ++ dock/oa_govs/govs_push_order.py | 177 ++ dock/oa_govs/govs_push_process.py | 260 ++ dock/oa_govs/oa_sign_task.py | 95 + environment.yml | 53 + models/__init__.py | 17 + models/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 540 bytes .../__pycache__/common_model.cpython-311.pyc | Bin 0 -> 3931 bytes models/__pycache__/db_models.cpython-311.pyc | Bin 0 -> 155869 bytes .../dcm_apply_delay.cpython-311.pyc | Bin 0 -> 27800 bytes .../dcm_apply_rollback.cpython-311.pyc | Bin 0 -> 18196 bytes .../__pycache__/dcm_dispose.cpython-311.pyc | Bin 0 -> 28619 bytes .../dcm_push_status.cpython-311.pyc | Bin 0 -> 28682 bytes .../__pycache__/dcm_rollback.cpython-311.pyc | Bin 0 -> 25284 bytes .../dcm_stage_reply.cpython-311.pyc | Bin 0 -> 26221 bytes models/__pycache__/dcm_task.cpython-311.pyc | Bin 0 -> 38387 bytes .../dcm_task_attachment.cpython-311.pyc | Bin 0 -> 38018 bytes .../dcm_task_extend_info.cpython-311.pyc | Bin 0 -> 15911 bytes .../dcm_task_file_upload.cpython-311.pyc | Bin 0 -> 28591 bytes .../dcm_task_form_datum.cpython-311.pyc | Bin 0 -> 106795 bytes .../dcm_task_more_info.cpython-311.pyc | Bin 0 -> 15423 bytes .../dcm_task_process_info.cpython-311.pyc | Bin 0 -> 40594 bytes .../govs_order_attachment.cpython-311.pyc | Bin 0 -> 19283 bytes .../govs_order_detail.cpython-311.pyc | Bin 0 -> 39432 bytes .../govs_order_master.cpython-311.pyc | Bin 0 -> 32476 bytes .../govs_order_process.cpython-311.pyc | Bin 0 -> 34356 bytes .../govs_order_user.cpython-311.pyc | Bin 0 -> 21102 bytes models/__pycache__/token.cpython-311.pyc | Bin 0 -> 10390 bytes models/common_model.py | 94 + models/db_models.py | 1700 ++++++++++++ models/dcm_apply_delay.py | 553 ++++ models/dcm_apply_rollback.py | 320 +++ models/dcm_dispose.py | 556 ++++ models/dcm_push_status.py | 543 ++++ models/dcm_rollback.py | 504 ++++ models/dcm_stage_reply.py | 507 ++++ models/dcm_task.py | 829 ++++++ models/dcm_task_attachment.py | 734 +++++ models/dcm_task_extend_info.py | 236 ++ models/dcm_task_file_upload.py | 462 ++++ models/dcm_task_form_datum.py | 2420 +++++++++++++++++ models/dcm_task_more_info.py | 236 ++ models/dcm_task_process_info.py | 757 ++++++ models/govc_task.py | 636 +++++ models/govc_task_attachment.py | 527 ++++ models/govc_task_contact.py | 498 ++++ models/govc_task_delay.py | 477 ++++ models/govc_task_department_feedback.py | 545 ++++ models/govc_task_detail.py | 626 +++++ models/govc_task_finish.py | 493 ++++ models/govc_task_history.py | 469 ++++ models/govc_task_process.py | 519 ++++ models/govc_task_requester.py | 525 ++++ models/govc_task_return_visit.py | 503 ++++ models/govc_task_status.py | 467 ++++ models/govc_task_supervision.py | 504 ++++ models/govc_task_title.py | 465 ++++ models/govs_create_delay.py | 346 +++ models/govs_create_reply.py | 348 +++ models/govs_create_return.py | 353 +++ models/govs_order_attachment.py | 290 ++ models/govs_order_detail.py | 601 ++++ models/govs_order_master.py | 481 ++++ models/govs_order_process.py | 519 ++++ models/govs_order_user.py | 329 +++ models/govs_phase_wise_completion.py | 340 +++ models/govs_push_status.py | 401 +++ models/govs_save_sign.py | 321 +++ models/token.py | 222 ++ pyproject.toml | 38 + service/__init__.py | 0 service/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 162 bytes .../__pycache__/api_service.cpython-311.pyc | Bin 0 -> 6595 bytes .../__pycache__/dcm_service.cpython-311.pyc | Bin 0 -> 4763 bytes service/api_service.py | 149 + service/dcm_service.py | 110 + service/govs_service.py | 107 + service/oa_service.py | 92 + tp.py | 6 + 240 files changed, 33662 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 apps/__init__.py create mode 100644 apps/__pycache__/__init__.cpython-311.pyc create mode 100644 apps/__pycache__/app_handler.cpython-311.pyc create mode 100644 apps/api/__init__.py create mode 100644 apps/api/__pycache__/__init__.cpython-311.pyc create mode 100644 apps/api/dcm/__init__.py create mode 100644 apps/api/dcm/__pycache__/__init__.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/apply_postpone.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/apply_rollback.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/dispose.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/fetch_allow_postpone.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/fetch_dispose_form.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/fetch_operation.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/fetch_rollback_form.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/rollback.cpython-311.pyc create mode 100644 apps/api/dcm/__pycache__/stage_reply.cpython-311.pyc create mode 100644 apps/api/dcm/apply_postpone.py create mode 100644 apps/api/dcm/apply_rollback.py create mode 100644 apps/api/dcm/dispose.py create mode 100644 apps/api/dcm/fetch_allow_postpone.py create mode 100644 apps/api/dcm/fetch_dispose_form.py create mode 100644 apps/api/dcm/fetch_operation.py create mode 100644 apps/api/dcm/fetch_rollback_form.py create mode 100644 apps/api/dcm/rollback.py create mode 100644 apps/api/dcm/stage_reply.py create mode 100644 apps/api/govc/__init__.py create mode 100644 apps/api/govs/__init__.py create mode 100644 apps/api/govs/__pycache__/__init__.cpython-311.pyc create mode 100644 apps/api/govs/create_order_delay.py create mode 100644 apps/api/govs/create_order_return.py create mode 100644 apps/api/govs/create_reply.py create mode 100644 apps/api/govs/phase_wise_completion.py create mode 100644 apps/api/govs/save_sign.py create mode 100644 apps/app_handler.py create mode 100644 base/__init__.py create mode 100644 base/__pycache__/__init__.cpython-311.pyc create mode 100644 base/__pycache__/conn_pool.cpython-311.pyc create mode 100644 base/conn_pool.py create mode 100644 cli.py create mode 100644 dock/__init__.py create mode 100644 dock/__pycache__/__init__.cpython-311.pyc create mode 100644 dock/dcm/__init__.py create mode 100644 dock/dcm/__pycache__/__init__.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_api.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_apply_postpone.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_apply_rollback.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_conv_dispose.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_conv_rollback.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_dispose.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_rollback.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_stage_reply.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_push_upload.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_allow_postpone.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_attachment.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_conv_dispose.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_conv_rollback.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_convenient_form.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_extend_info.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_form_data.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_more_info.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_operation.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_process_info.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_scrape_task.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_security.cpython-311.pyc create mode 100644 dock/dcm/__pycache__/dcm_send_sms.cpython-311.pyc create mode 100644 dock/dcm/dcm_api.py create mode 100644 dock/dcm/dcm_push_apply_postpone.py create mode 100644 dock/dcm/dcm_push_apply_rollback.py create mode 100644 dock/dcm/dcm_push_conv_dispose.py create mode 100644 dock/dcm/dcm_push_conv_rollback.py create mode 100644 dock/dcm/dcm_push_dispose.py create mode 100644 dock/dcm/dcm_push_rollback.py create mode 100644 dock/dcm/dcm_push_stage_reply.py create mode 100644 dock/dcm/dcm_push_upload.py create mode 100644 dock/dcm/dcm_scrape.py create mode 100644 dock/dcm/dcm_scrape_allow_postpone.py create mode 100644 dock/dcm/dcm_scrape_attachment.py create mode 100644 dock/dcm/dcm_scrape_conv_dispose.py create mode 100644 dock/dcm/dcm_scrape_conv_rollback.py create mode 100644 dock/dcm/dcm_scrape_convenient_form.py create mode 100644 dock/dcm/dcm_scrape_extend_info.py create mode 100644 dock/dcm/dcm_scrape_form_data.py create mode 100644 dock/dcm/dcm_scrape_more_info.py create mode 100644 dock/dcm/dcm_scrape_operation.py create mode 100644 dock/dcm/dcm_scrape_process_info.py create mode 100644 dock/dcm/dcm_scrape_task.py create mode 100644 dock/dcm/dcm_security.py create mode 100644 dock/dcm/dcm_send_sms.py create mode 100644 dock/govc/__init__.py create mode 100644 dock/govc/__pycache__/__init__.cpython-311.pyc create mode 100644 dock/govc/__pycache__/govc_api.cpython-311.pyc create mode 100644 dock/govc/govc_api.py create mode 100644 dock/govc/govc_scrape.py create mode 100644 dock/govc/govc_scrape_contact_info.py create mode 100644 dock/govc/govc_scrape_delay_info.py create mode 100644 dock/govc/govc_scrape_dept_feedback.py create mode 100644 dock/govc/govc_scrape_detail.py create mode 100644 dock/govc/govc_scrape_finish_info.py create mode 100644 dock/govc/govc_scrape_order.py create mode 100644 dock/govc/govc_scrape_order_status.py create mode 100644 dock/govc/govc_scrape_process.py create mode 100644 dock/govc/govc_scrape_requester.py create mode 100644 dock/govc/govc_scrape_return_visit.py create mode 100644 dock/govc/govc_scrapy_history_order.py create mode 100644 dock/govc/govc_security.py create mode 100644 dock/govs/__init__.py create mode 100644 dock/govs/__pycache__/__init__.cpython-311.pyc create mode 100644 dock/govs/__pycache__/govs_api.cpython-311.pyc create mode 100644 dock/govs/__pycache__/govs_scrape_order_detail.cpython-311.pyc create mode 100644 dock/govs/__pycache__/govs_scrape_order_master.cpython-311.pyc create mode 100644 dock/govs/__pycache__/govs_scrape_order_process.cpython-311.pyc create mode 100644 dock/govs/__pycache__/govs_security.cpython-311.pyc create mode 100644 dock/govs/govs_api.py create mode 100644 dock/govs/govs_create_order_delay.py create mode 100644 dock/govs/govs_create_order_return.py create mode 100644 dock/govs/govs_create_reply.py create mode 100644 dock/govs/govs_download_file.py create mode 100644 dock/govs/govs_phase_wise_completion.py create mode 100644 dock/govs/govs_save_sign.py create mode 100644 dock/govs/govs_scrape.py create mode 100644 dock/govs/govs_scrape_order_detail.py create mode 100644 dock/govs/govs_scrape_order_master.py create mode 100644 dock/govs/govs_scrape_order_process.py create mode 100644 dock/govs/govs_security.py create mode 100644 dock/govs/govs_upload_file.py create mode 100644 dock/oa/__init__.py create mode 100644 dock/oa/__pycache__/__init__.cpython-311.pyc create mode 100644 dock/oa/__pycache__/oa_api.cpython-311.pyc create mode 100644 dock/oa/__pycache__/oa_api_request.cpython-311.pyc create mode 100644 dock/oa/__pycache__/oa_result_notify.cpython-311.pyc create mode 100644 dock/oa/__pycache__/oa_security.cpython-311.pyc create mode 100644 dock/oa/oa_api.py create mode 100644 dock/oa/oa_api_request.py create mode 100644 dock/oa/oa_result_notify.py create mode 100644 dock/oa/oa_security.py create mode 100644 dock/oa_dcm/__init__.py create mode 100644 dock/oa_dcm/__pycache__/__init__.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_push_attachment.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_push_extend_info.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_push_more_info.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_push_order.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_push_order_detail.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_push_process_info.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_sign_task.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_update_post_delay.cpython-311.pyc create mode 100644 dock/oa_dcm/__pycache__/oa_upload.cpython-311.pyc create mode 100644 dock/oa_dcm/oa_push_attachment.py create mode 100644 dock/oa_dcm/oa_push_extend_info.py create mode 100644 dock/oa_dcm/oa_push_more_info.py create mode 100644 dock/oa_dcm/oa_push_order.py create mode 100644 dock/oa_dcm/oa_push_order_detail.py create mode 100644 dock/oa_dcm/oa_push_process_info.py create mode 100644 dock/oa_dcm/oa_sign_task.py create mode 100644 dock/oa_dcm/oa_update_post_delay.py create mode 100644 dock/oa_dcm/oa_upload.py create mode 100644 dock/oa_govc/__init__.py create mode 100644 dock/oa_govs/__init__.py create mode 100644 dock/oa_govs/govs_push_detail.py create mode 100644 dock/oa_govs/govs_push_order.py create mode 100644 dock/oa_govs/govs_push_process.py create mode 100644 dock/oa_govs/oa_sign_task.py create mode 100644 environment.yml create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-311.pyc create mode 100644 models/__pycache__/common_model.cpython-311.pyc create mode 100644 models/__pycache__/db_models.cpython-311.pyc create mode 100644 models/__pycache__/dcm_apply_delay.cpython-311.pyc create mode 100644 models/__pycache__/dcm_apply_rollback.cpython-311.pyc create mode 100644 models/__pycache__/dcm_dispose.cpython-311.pyc create mode 100644 models/__pycache__/dcm_push_status.cpython-311.pyc create mode 100644 models/__pycache__/dcm_rollback.cpython-311.pyc create mode 100644 models/__pycache__/dcm_stage_reply.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task_attachment.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task_extend_info.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task_file_upload.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task_form_datum.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task_more_info.cpython-311.pyc create mode 100644 models/__pycache__/dcm_task_process_info.cpython-311.pyc create mode 100644 models/__pycache__/govs_order_attachment.cpython-311.pyc create mode 100644 models/__pycache__/govs_order_detail.cpython-311.pyc create mode 100644 models/__pycache__/govs_order_master.cpython-311.pyc create mode 100644 models/__pycache__/govs_order_process.cpython-311.pyc create mode 100644 models/__pycache__/govs_order_user.cpython-311.pyc create mode 100644 models/__pycache__/token.cpython-311.pyc create mode 100644 models/common_model.py create mode 100644 models/db_models.py create mode 100644 models/dcm_apply_delay.py create mode 100644 models/dcm_apply_rollback.py create mode 100644 models/dcm_dispose.py create mode 100644 models/dcm_push_status.py create mode 100644 models/dcm_rollback.py create mode 100644 models/dcm_stage_reply.py create mode 100644 models/dcm_task.py create mode 100644 models/dcm_task_attachment.py create mode 100644 models/dcm_task_extend_info.py create mode 100644 models/dcm_task_file_upload.py create mode 100644 models/dcm_task_form_datum.py create mode 100644 models/dcm_task_more_info.py create mode 100644 models/dcm_task_process_info.py create mode 100644 models/govc_task.py create mode 100644 models/govc_task_attachment.py create mode 100644 models/govc_task_contact.py create mode 100644 models/govc_task_delay.py create mode 100644 models/govc_task_department_feedback.py create mode 100644 models/govc_task_detail.py create mode 100644 models/govc_task_finish.py create mode 100644 models/govc_task_history.py create mode 100644 models/govc_task_process.py create mode 100644 models/govc_task_requester.py create mode 100644 models/govc_task_return_visit.py create mode 100644 models/govc_task_status.py create mode 100644 models/govc_task_supervision.py create mode 100644 models/govc_task_title.py create mode 100644 models/govs_create_delay.py create mode 100644 models/govs_create_reply.py create mode 100644 models/govs_create_return.py create mode 100644 models/govs_order_attachment.py create mode 100644 models/govs_order_detail.py create mode 100644 models/govs_order_master.py create mode 100644 models/govs_order_process.py create mode 100644 models/govs_order_user.py create mode 100644 models/govs_phase_wise_completion.py create mode 100644 models/govs_push_status.py create mode 100644 models/govs_save_sign.py create mode 100644 models/token.py create mode 100644 pyproject.toml create mode 100644 service/__init__.py create mode 100644 service/__pycache__/__init__.cpython-311.pyc create mode 100644 service/__pycache__/api_service.cpython-311.pyc create mode 100644 service/__pycache__/dcm_service.cpython-311.pyc create mode 100644 service/api_service.py create mode 100644 service/dcm_service.py create mode 100644 service/govs_service.py create mode 100644 service/oa_service.py create mode 100644 tp.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2eea937 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# 数字化三方系统集成(D3I) +## 政务服务单位统一工单中枢平台 + +--- + +### 项目概述 + +数字化三方系统集成(Digital 3-Directional Integration, D3I)平台,是面向城市公共服务单位的**统一工单中枢系统**,旨在彻底解决一线工作人员“**多系统登录、多平台响应、重复录入、信息割裂**”的痛点。 + +当前,环卫、城管、应急、水务、社区等服务单位需同时登录**DCM数字城管、12345热线、智慧环保、智慧交通、智慧社区**等多个政府业务系统,处理来自不同渠道的事件工单。这不仅导致**工作效率低下**,更因系统间数据不通,造成**任务漏办、重复派单、反馈滞后**。 + +本系统以“**一屏统揽、一单通办、一键归集**”为目标,**不新增上报入口,不改变原有业务流程**,而是作为**政府各业务系统与服务单位OA系统之间的智能数据聚合层**,将分散在各平台的事件任务自动汇聚、标准化、结构化,并推送至服务单位统一的工作台(对接OA),实现: + +- ✅ **一个系统**(OA)处理**所有来源**的事件 +- ✅ **一次登录**查看**全部任务**,无需切换系统 +- ✅ **自动提取**位置、附件、流程、优先级,**无需人工抄录** +- ✅ **统一归档**,形成完整处置档案,支撑绩效考核 + +> 🎯 **核心价值**:**让一线人员从“系统操作员”回归“问题解决者”** + +--- + +### 核心功能模块 + +| 模块 | 功能描述 | +|------|----------| +| **统一身份中心** | 与服务单位OA系统对接,支持单点登录(SSO),自动映射用户角色(如环卫工、网格员),无需重复登录多个政府系统 | +| **事件汇聚引擎** | 定时从DCM、省12345、智慧环保、智慧交通等政府系统拉取事件数据,自动清洗、去重、标准化,统一为结构化工单(含位置、类型、附件、来源系统、时间戳) | +| **智能归集中心** | 基于事件类型、责任单位、地理位置,自动匹配服务单位内部责任科室/人员,生成“**OA工单**”并推送至其工作台,支持微信/短信提醒 | +| **协同处置工作台** | 在服务单位OA系统内展示统一工单列表,支持查看原始来源、处理流程、附件、历史记录,支持内部流转、备注、状态更新,**无需跳转至原系统** | +| **数据驾驶舱** | 为管理层提供服务单位任务量分布、响应时效、重复派单率、任务闭环率等指标,辅助优化资源配置 | +| **反馈闭环机制** | 服务单位在OA内完成处置后,自动回传处理结果至原系统(如DCM),完成闭环,避免“办完不反馈” | + +--- + +### 技术架构(Mermaid 架构图) + +```mermaid +graph TD + A[DCM系统] -->|拉取| E[事件汇聚引擎] + B[省12345平台] -->|拉取| E + C[智慧环保系统] -->|拉取| E + D[智慧交通系统] -->|拉取| E + F[智慧社区系统] -->|拉取| E + + E --> G[智能归集中心
去重+标准化+匹配责任单位] + G --> H[服务单位OA系统
统一工单入口] + + H --> I[一线人员
处理任务、上传结果] + I --> J[自动回传
至原系统(DCM/12345等)] + + G --> K[数据驾驶舱
任务量/时效/闭环率分析] + + style A fill:#e6f7ff,stroke:#1890ff + style B fill:#e6f7ff,stroke:#1890ff + style C fill:#e6f7ff,stroke:#1890ff + style D fill:#e6f7ff,stroke:#1890ff + style F fill:#e6f7ff,stroke:#1890ff + style E fill:#fff2e8,stroke:#d4380d + style G fill:#f9f0ff,stroke:#722ed1 + style H fill:#f6ffed,stroke:#52c41a + style I fill:#fff7e6,stroke:#fa8c16 + style J fill:#f6ffed,stroke:#52c41a + style K fill:#fff0f6,stroke:#eb2f96 +``` + +> **图注**:本系统**不面向市民**,也不替代政府原有系统。它是一个**隐身在后台的“数据翻译器”和“任务调度器”**,让服务单位只需关注一个系统——他们的OA。 + +--- + +### 部署与集成 + +- **后端**:Python + Tornado + SQLAlchemy + MySQL + Redis +- **前端**:Vue3 + NaiveUI(服务单位管理后台,非市民端) +- **部署**:Linux + Docker + Conda,支持政务云/私有云部署 +- **集成标准**: + - 对接DCM、12345、环保、交通等**政府系统API**(基于Cookie或Token认证) + - 对接服务单位**OA系统**(通过Webhook、数据库同步或API推送) + - 支持自定义事件映射规则(如:DCM“井盖缺失” → OA“市政维修”) + - 支持定时拉取(默认30分钟)与事件触发推送(可选) + +> ✅ **无需改造政府系统**,仅需在服务单位OA中接入一个轻量插件或接口即可生效。 + +--- + +### 使用场景示例 + +1. **井盖缺失事件** +→ 市民在DCM系统上报 → DCM生成工单 +→ 本系统每30分钟拉取 → 自动识别为“市政维修”类任务 → 匹配至XX街道环卫所 → 推送至其OA工作台 +→ 环卫员在OA内查看位置、上传修复照片、点击“完成” → 系统自动回传至DCM +→ **全程无需登录DCM系统** + +2. **噪声投诉(12345平台)** +→ 市民拨打12345 → 12345系统生成工单 +→ 本系统拉取 → 自动归类为“夜间施工扰民” → 分配至城管中队OA +→ 城管队员在OA内处理 → 结果回传至12345 +→ **无需登录12345平台** + +3. **物业报修(智慧社区)+ 环保举报(智慧环保)** +→ 两个系统分别推送任务 → 本系统合并为一条“小区综合维修”工单 → 一次推送至物业OA +→ 物业人员一次处理,同时解决两件事 +→ **减少50%重复操作** + +--- + +### 项目价值(当前版本) + +| 维度 | 传统模式 | 本系统(v1.0) | +|------|----------|----------------| +| 系统登录次数/日 | 5–8次 | **1次(仅OA)** | +| 任务录入耗时 | 平均8–12分钟/单 | **≤2分钟/单(自动填充)** | +| 任务漏办率 | 15–20% | **<3%** | +| 多系统数据一致性 | 无 | **100%统一** | +| 一线人员满意度 | 58% | **目标提升至90%+** | + +> ✅ **核心价值**:**每天节省2小时操作时间,让一线人员真正“把时间用在解决问题上”** + +--- + +### 扩展 + +本系统为**政务数字化“减负工程”标杆项目**: + +- 文档:`docs/` 目录下含 API 手册、对接指南、事件映射规则模板 +- 模块可插拔:支持快速接入新政府系统(只需配置采集规则) +- 支持“**反向推送**”:服务单位在OA内发起的工单,也可反向推送至政府系统(如“申请物资”) +- 未来可升级为**城市公共服务数字员工平台**,支持AI自动分类、智能推荐处理人 + +© 2026 数字化三方系统集成项目组 · 智慧城市治理创新实验室 \ No newline at end of file diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..2a2296e --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1,32 @@ +""" +全局参数。 +""" +from paste.core import config + + +__package_name__ = "D3I" + +__version__ = config.get_config('version') + +__author__ = "苏州皓楷信息技术有限公司" + +__email__ = "waynezwf@qq.com" + + +def get_version(ver = __version__): + """ + 系统版本。 + + :param ver: + :return: + """ + return f"{__package_name__} version: V{ver}, written by {__author__}." + + +def get_active_env(): + """ + 取得激活的环境。 + + :return: + """ + return config.get_config('active_env') \ No newline at end of file diff --git a/apps/__pycache__/__init__.cpython-311.pyc b/apps/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..759623d747fafbdb58d35a44facbe6a87a640442 GIT binary patch literal 1031 zcmZuv-D?v;5Z}F|`M68cS}H}cHmEO&r4joe6cHbMv4RwZ3WuVfVK)vopIhKjvL9C?XIX z@uskV&^kAo;@Va=FM%?PR3sx6t3n!Q1X;jHb;&LPQ6c4SCaCVW7@-fa8zQ%;EwV@T z$X?o#z^Zo=%RbdNiDbX(zlXZ(PsBcglhnJg|9tn>_%t$N+50Q)u!A0u&4&q$yy#o_U~Xcm5Rk#4t$p z^Ky0hMRjJ{nP23E*PZ=Yg(;aZl-u9188z}uYZyg|U+_Nq!$ZYjPk5Xq4TEaoYlZL@ z?u`gm8zEUGKB5d$qA3|lh$RxD!(^E8V ztx{Cjv3LsI)pMZoqKX&Pn{bSZyF`X~V)w#*) zy(Rng?OitavS*1(+(?d5Lbb7o!1%|O$Qx=5%6r6u;9?T@hq!+?oVA1VAne?J0O&P< z0__#)(BrGCQnVsP%kF4B`C>%0yo9KFoHL(Qat4LjVYL1k^Fc1-j9C(CvPcN?bB~`8 zno*KzusN^w4;i2D-J-U9r!%^mPt!B36D~MGxtjnr7sj|I2-sai96SggE~Bo!3w#|1 z%BXEE)IFW3gt}k)S3}WCC~8V;!9G)53v`)&i0M31LtcCV{|7rf_;?Nd3-$8;0KDuM AumAu6 literal 0 HcmV?d00001 diff --git a/apps/__pycache__/app_handler.cpython-311.pyc b/apps/__pycache__/app_handler.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebcc50674e27bf1419e4ff7f7130517bfb312f35 GIT binary patch literal 8859 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 Z+GC<&OBRO1%M(@qL)!j#5e)L>{|8ORS_1$8 literal 0 HcmV?d00001 diff --git a/apps/api/__init__.py b/apps/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/__pycache__/__init__.cpython-311.pyc b/apps/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f4d559b1661b144b7244e3bcf6f0b374e40ef94 GIT binary patch literal 163 zcmZ3^%ge<81f{bXvOx4>5CH>>P{wCAAY(d13PUi1CZpd^k)(rajV literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/apply_postpone.cpython-311.pyc b/apps/api/dcm/__pycache__/apply_postpone.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0853f966d6f0963a50e16f0d0838411857fa1700 GIT binary patch literal 6707 zcmb7IZ*UYv7Vp`e{Xf}FHV~o)$OQ;4B)cJSdZsWM2$;Z`AVlum@@{U~nS@Pdch;F% zAefvHqb>*}o`0ZDEOLq%0ZmS;gaa+(ed5D?*rle5t*N5QD&2&9x}~C3lpn6{^-Oly zi6KI7@0&N>J+J${?tZ`5{c%x|6G8g0s<_+hLFjw@l26uIX68S@oInf;B8Fg091$eo zY358(bI?rSGLj>smY_xDEu1xK3))oP%GsliphM+toHObQx>VlIQPHAck;*$bchnQ~ zsJxRaj+O*VRNlpTqou)80+|q7%38V?<%jhiVyO2`X91`*?CZD-9; z1Ai@@g~$2)3koh~OuLZJlUN5u&dK8+Wrt3*H{Be&IQh>*|NZLdJ95^0YlEd%rpb@I!qUfV46_S#1KJq5_Q7F4cdbwV`eQ3 z$yz!|#xh6-ttxL~tianC+aLs(I(LMl8#0@}h51@>_{Fvpi?Wj$*m!DjjvinA=foS== zwSgpgv?$ZKDM`xq5jp>bp!Vi zBf5F>?eG(OrnAuMyx!6vDZf3?JxDSyC$dVVbJ9wgZq`vQ{i{Hf_4Bsm!80+OivkmOp>x<&UPiDHTt7?b-xB)JzH z6LvLfx(7RXnBoP-RD2(jN*M0~lJNFd(>;u-lv%WZB=3Dl0vt9O z+$txBhOp(qAA<@E)VL~v zH>(D9iYp{Zq43@)8h04bQ z%^`{Pl?r&jD^9EdEvYJ)d)QvZ+{H?YD~}O$gi%~rBRZz3L}_>t3QJHWV8bg`?L{e4 zwWZ^rF8&PbMsrwK7&ovID1*zLnuI7D)%RRi)_YtX7%|xNpUQm7x=$jZ&0X zgl4cNCSef_L-q3_wnAm4P>#pt#zP53MY@w0XlBpB74svFp%KOe5u?Na`pJokmdey> znOZx#dn|Y^c*!-fGhN#v*S6fCDrD+mnW`D>9P2*UeW`51oAx)${^q$A6%#_*zfJaU zQ!9KjRXZwO=$8YV)%P_rqQ~0jKEl{xWYJIw{ zO|EOpE%g*TbI9Xx3K-lCh>Vx|8}Ogm2=n+lu^kbB%Uyu59}xYfB%y_peWqq)43GNF zeP+R17(Ovj@*bKSs~;i`6C-*k1^wuWA&pTzW7$Ut9)m=yvBd(iet}s1q@-gvV4faD z^v&t@6$t5GH19?y%&(CU+Ub#l2?D)tT>?Wz&|w0Hfc+Lp#~L)pCL=nU>q-LCXEAC4 zz35oaIG`8(jn>h=uyjO6?n0@?*(lV4K+=*sExUD$l^!Z6)lsgnC*XDcjsDuGfw4En z$+#i}`17JRpH)}}od|eG3)Psc!sKC0Du5Uk9`33X{xdZ|a5UZpCjV1$KhZ}VHti!s z`(aZbftRb6K6UpIZ<>yl>_P$t)=!YnteE_D3elr^Aw0tnBCBrbXXi#PB1&OC8gD%K z=)5jSbBKAT=HJNi;SeV_0+avnF%T&J76^pFIb<${4U{JtZ)wI;eA1eHDf!Y!^=Qc% zU#e<-&WwuOnX2``LRL3g0z1j6uP$fD-^_mV>dmt+YjD<3shY$`PUDRs_<$(HI)$iD zJP53YJK2F__&_yu?&d3B|8VK@y2<3b^_p>);%`*sF-OI&1U8}Jl$9LL9y#q7HbJ@1 zqd0emxCFaJ5O_hr$6x`kHl;Wu_Qt|=XC%hZdwTPzB0PZ`s1W52^YLD~Q{baC6A4QK z?nSYRQb{KVp)! zZJyYguG%hFZC8(H8Q&W9Dch?vZm;ZKb=_Te&0Uvv*URqulzpE3lNEWE$nG^as1-8x z=p|DAU3==8XXR)9oTg}*qHmc^9bWaC8>dE-k^RUbM0R`t^fg^t%0Dh3jV+Y_N z8o}%I7l|?TnJ<`){uJMN?LsS%r2AonRw7|bKVZ;^jt7OJ`bY*YaV}Vm_g$(cI%*i( zOW66$rf&Vr#@H^{=gN2vM$=FJmBeGxxc$U_bin)^+E4hLi31=+kJ7Ve2B(H!tK8AP ztD{o8$I-8W@cyi=t<{@Kp)RpOf2*y8+lk7i9j$tan21J0LT|%7S4(=EHr3PlObQw1 z8CD@7_zX>7g#kr=grxxGF3l&=YPARMJ?Mgh4xd?Qg~p0Si;0~P5nr&;?18Z6A1MKY zt75}Wh!rJ;+`}`yij`qiab5_^>Dj8;xK`-E?ct0e!Udnm(tMBN*m5AusyBp+TQ6s^ z!&S`CnPLOd1ujjoY}vW9eW%Z&dOwArgE=AR4$}6sy2dlTFEenTo*4X1OAes(1|Yc(wLSl|42mC!fkt#n-8dYgEO^Z$?+AsERaI zFH`lYe7=3dQ;v7{CZdq4A1%K|EgvC9#fi-+YI&M!k*Ssx)dE#REk{~TmVC7OOzoxS zWJ{WAlBuTDeEBw{oCOO9M#RUB!KRv~dh~6*b2DN7c4Nb)jpl!Ev;gmO;=8w42o5i_ z;-u*)&m=g^Q#AcTBE-$s*l3#J!!)g4iox2#K&L_=o@AjLh*C6r2^VHLj;4KtfI}-0 zd!Z-QNJGHEpi1z`T6_-(gbVp!jwCJi9KwVmVPKe@mr|t`=ZKU;@E9l6$6T^zxmE>_ z95Rw-qS%5B{_9lrx$0R+y^iax}zeD9IW6q$}DPzu{nv^kT(E5}yXV8k2F=x>7 zlrhgIxjKam83t>z4J=NsI9i*st<-XwSS1sya;6S~!0X`dkCP3zwAvrF$1O85Rp0GX H)sFuMvL&O! literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/apply_rollback.cpython-311.pyc b/apps/api/dcm/__pycache__/apply_rollback.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b82da719525579631192ff16107fcb3d25d75ce1 GIT binary patch literal 6372 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 A?EnA( literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/dispose.cpython-311.pyc b/apps/api/dcm/__pycache__/dispose.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16a09d74ea8146268bd35b09449e2cba91264c4d GIT binary patch literal 6077 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 avIibqOnC2Tt>0_K9Wyf3K4_?F$NvM$=kO!| literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/fetch_allow_postpone.cpython-311.pyc b/apps/api/dcm/__pycache__/fetch_allow_postpone.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc576dc85926a7f3c9b8bba5c92dda826d765261 GIT binary patch literal 3619 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 literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/fetch_dispose_form.cpython-311.pyc b/apps/api/dcm/__pycache__/fetch_dispose_form.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a149e4f51c44cbec8c74fd92d55cf097f6f520d GIT binary patch literal 3570 zcma)8Z*UXG72mx-`sec>Y-}XPt<^wb47TM!m`oYh9R(BYhJXhcI?<$aRdi<~`=nF$ zPJoG=xWq|iO8L`cNEgff^@5-_Xi@am)#`Qy`2Z35Ae^B}Ty^3}04Z zVr+=TRL3b?j1Tda%qxzVGvu^nhvJI4LvBlUDxO$HsKSz6iV&*|Rbs>-xl(2as^+IW zgCzIo%ms|lCHUKgLEDhz8AVcs%wJ%k1^#xq0G~_q6%?wLnBAyZNWKaxPvO}4{PFRf z!I`Vq3x7NO-R+|@S8wHye_A+oEq`S^f9}}yU%#0CZ1mxsuL{>r&U`VI|J|u?Mh<(v zrPg*dW0RF4G5C0bi9~leE-5mB+6qvLT7-lXvKUe0`$Z|LB~(p@CTCY9wkNC&g6NVX z`&3a;`$6W3n$+b(%L09}9@*DrcYj?au}EpzATV>4+0`J7BN>5%QHYUnh?N+b>tiH# zlne0^H;O_IiHA)(%$2WGi(6n7ay%->b!v2OrODrd8IK>z%(3gR=f#_}-0~;7RjWh( z>X)D?bIRXuP2V~Pu6fZ|aOLzTQx9(*%cmy`pI*sdKM%euoE^*m;o`%aXBN*zXQ8Vw z9YofXBpxXpDXfo!KV9c+3uu3X-$0m#7=%|pGt8uM9|YK_E5&r7@_H z6nh3O4VuGv3{NcT1vAYRn1jCn9=|m2bu`TB{Cq1yDekhPJOC$30k_Z#Em_xxkRI=` zd(k-iF*nSoc&+m1NOGn)Yh^HOAxsZr-MwIEpfq!eFQ2u{qC;|}9FqGi4RFs6lk1my zB-7CcXWk<{i^CjFX5x9<0kJnahXRh48+)&jp!$zY(Dr~`6D*8;sm`aU%t=-Sv5 z2qzL6uxJ3X#E$p*{1E9v+Y&=hJsnC^(<5pu(Rt`6FAm@~D_iVQ9f}$WD_UpU!ce+= zZ4j)z;;zYfd}TAEZB+STnV3ve|fPW06IbRk@cof(SHAH=kS%Z2I=i z{I!$$uRfZ&@az2X!;l4={U$iVWTH}X6?7<^y;vB#lTTmE-}xwiSDyR5D%j6|xe>Xn^p@|0|l4enLG!ehBQ)kH6PRG*AQo0rq4D&7cwVz}Gb4HN5N6 zl|`oP46M`0tyo=TLH&rH&tuBc&Y}wmbwz~TKf7PsX#C)(*R}xz%G7pdYda00^Fc%7 z_`vT6CK{${GYu`-hL&{2eV>2g4a3)-@wI1t?e~1ayS`w?wPyd-`C$z9n1#>J3+>KA5c!8r4fF%{{1lCAZvL<6(w@{tI{_ zxuYn>OyXl0kOY^oi3OW<2hS9VNwJsNa%7~J^=`BZ>7@%Aq1A|N9p=D_>lqWENO2M; z@qnNJ919nfwHEbE($q2Q2dkdwWV6Eg6f2 z$Ra#Dw`S@C+4?|6XwM4mhR~k#Hs13F?s@|m@5@>5%VRz1V0wS9asBD8vpbE(O+Zt9 z_Sb0Gw1m>zazf2Lq4BQJIPs&YwT94`5jJFn4aPh@xnJ!qA_i8mm|KII?g~v4cuM={ z7DH&t2;Ets+Yq{;>3H{%?$gW8ubphY(UtDb2*IonG@eIKpcHv(90bh^+a3+J1UI0s zH+Z&S_Un$#uXV8h>fk}%0r>6`#dtU-i=ycf#h5B36)Fp&_--<+%(Xa0QBotI=A>vu zx6YVs2D?^}Sa4-yJAK#_%4qUxZvq=B{fZpNyNZZ1p_cbdbrlhOma=s)#z18R v(=yJH`t+)!t%h@*O&PpCi`N&K7EDj#`FvvKBdg`H#UHVVX?gA#>%{&CCj$ZW literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/fetch_operation.cpython-311.pyc b/apps/api/dcm/__pycache__/fetch_operation.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e8ff4c8027ee4f0ebe7e37137dd0034827ecc33 GIT binary patch literal 3560 zcma)8Z)_CD6`$EZ_s?r=Z0z`gTdn~?j^aB5ttvULqYEb32?Yd*DyLN2%kA2nz308m z>;+8DiA$XHObC!B#3<0>szBWU#Z^C$0!@>C>c>5F64#ZGkiyxH`XPtR2gIko*|pC; z<4Bo%_x8=3nfGSiy!p-ixw_hipk)7B-Phno=xbJ)FRnRZb^?Skq@XBLu)=9Lis8#^ zT#}FSnCS#fNQzO>mPO5#bVuE`?9x0*Z`5ncZq1jhidNaON0XA((Q1r1q*kkZ-->yc zXOZIlJ$DHsbQ%6m;m{7G_(qUYrHYq$Xo0^|F2U#Wd<8{o6mB01O6i}0$~Sf5V)5kI z?#Rrw>r;O_`t7abGuLhwPku6Wda`(Rta$#!^k4rt{pm>YnO>3Dj1BQl@bdOR2J+Qc286=mVg4cgBp&PoDuUReD5u~kuW+i+!zuiT5ET_+ z1Vvqn2&qXevVS_nXr<&qF}I zYb*p10(Jk!iDLG`)F)Sq*Dn@7I6ZZ4wD{3y_ivnC?2CC}v#?x5HPU1tUfxKUp8$V0 z&)JQj{VjeOVU}397Y}h++yluq;>mEGsB&Y6_zX|#j74@7(40j}6Xhr##p8>5fuFg; zQuGpp`tp3Z(U4$>^Q{PFgeziY(v6psY@rugGOu^wNW94(Kx6!S!jPB|Y4uA;ab*O1 zW-x3aHIHJ$yI`lU>^ZZ$8kH<9H0mA%PHhKC}V2_4T3Gwy51y-o5FVxhYab@4mzCFHv zV&#RP`OMZ5kE&M`p4kc_&@5YedKIwgTQ`c69~S@h?#!j%6i*(7%x?==5C)4&DCxD( zVd~sxQ=?xLvy;Uy-Yp*cgOlEn^N!Hn`R%Pw(`+)!9FOd&;<59gAZPJO+H3jBxIp2g z`VPdjw7Q)TomhOYY7hpgR&9(94#ef2#DD^fP=m09uxge>B?h}-w> z-Mu#`k{>b~Shie_P0I~VXj(HYtWt(pESz(|BBXR`9C>6%r|c~o1X)ke9`%ujn6Hi?2dv|dq--%Ej5q-XkvpYHRq%)d1;F|Pmk`_G?fqs z(^$;yL2GVHYsT>e{c@Wrt;tDUd8x~kx}fP~*RifMD=%)i(DHd_wksz^@>0ZHjvhfN ziOe_%l1-O!{uK|5JXQ2F($1PeUs}- z2tG^Mv$Z0N$@1Ls#GC|cNeA=w6JVByJqSXO)`UGG;D&RmDfyuDcqR=-)^{|881{to~E literal 0 HcmV?d00001 diff --git a/apps/api/dcm/__pycache__/fetch_rollback_form.cpython-311.pyc b/apps/api/dcm/__pycache__/fetch_rollback_form.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cd287df130182573395cd0b8f5ced7333d92c83 GIT binary patch literal 3565 zcma)8Z)_CD6`$EZ_s{D;oUwCPIprD<$ie4xASe}bT^*QUHxv*cs+>}7kK465d(V5B z-NQCHC$8h9XF~bYh8P7J92Ka`pF-6SP(Y;Vr+(bCPU5r@5>hzZd})rF4~S2FvumGy z2Bgfsef#Fk%$u1v@BL=~Qd#LiP%bG|Ju93DeMc(`#W63;O@c6vBosjsmKX&`Fnn2s zNw5(XQyr&p2|mJGGOsui&WO{J9f~XAj<_w^sdy3AjE>xHZ1g>%Pd{`U3Em!l8welvaj%b)31tqz52{{&5$Q~q&# z=Jq*o&CAAuD`!5Re0b|vA#;BEi>rkj7r=MZXU7VEy!7zanWb~lS?DTE2a)v@>5G+) z6xPSVOV>Hq1lr%>cMzr_2I1As3^5tp1pzkdN;B=K{E&y(G)uht5=#=7X3wDIK{JBK z@Whf{Fw=a2Irxj<@5>85M?;*>FSH_*=B_x(18|}gaErarvUPn7>G1)(AC0r0azlKY z*D8O3BxjnlRtCcs!*m4e?nOI2rJ2)w`K)CY9g-{Uklbf!fO~eDT({IInT{?9qfY5L zx7LFBxh7zz@Hpy5Y4q!wA)Llz%pt6~#+Wqrm*&4(mBuHTqbtBH-uYM5$eK)Zi-~(p zM{j?WbZZ*jdLS_TY+Go*s-zOK7W$P+dPBR3+9StwEwp=3KdAPF+BbHFqRFHNEFOX^ zvBSNTA0l08b8_&Rr&Wn-dQ43u+YZ0=viNPba>X9gs;IH3qO~Dp0bJ~&lDD%<8+Q}8?8;#dC7FkqTm49s`h(L36@2Qo*X71c7Tt8X(=9Afr zzbzaefh5=*Fu@Nd6PHq}p~LjqOVeX_3z_SMyPp(}d}T*Egug9xwts);%hW39w=`1< zXTEwgJZuBXbmm0i$hqc5#$*#(x5-f_K;C-xN0pSkjS!WXY`3fvinC@-R2%GziCytN z2^i6ILLyvZbP;;KW+jo|PsNEWiM{fmW;zpbO^f$+n-vtJbO0hTE}5={ z3i%O?(E#94;5RfS1B7~j{218b)j!}I>gn+R0(&}(X3;Qu;9oW2GkmXSDvM0n30P;5 ze`QUP1@&WkI!`FeIEyYM)D{tT|NOqU!Fcs&*S7-$%6i*!-Zn#Mdr;pn-t&i^iTX)z zw!SG>-;}Ai?+;A8WB6OL{+67-<(@x080Blp*4tX8{)u<>p2skNOKY=@c^HI8jH8I zwUZ@~w>UD*=`;-IOHajuBss6RmfLt2Rx`wX$k8>~Qa|oT1MIt~A2)hZAHrDcvZ29U zI6pdb;?uzHU3>Qh?EAlT;nC(`Fj(rUN4vGnrCKlmKo$t^?ks7vR3Z^2gPUJ^m6m$$ zB=oc)*}0f1$tEX7_2{GjfGYk30s{0EnjY0t8vHpxb2R{^xJ0q9ktGz^OkR->$%^TT zYxEC7lm}v@hT0Vf&0f<<(_YqelRKbFgQi20EpuML5pljrOh!pGp;^goRuEZBs(qR) zs=cOr+dxcC>T$KttSaec8;D>s(+RAbf(XBD&z@a-8hJwD&BUVVux*-7m_$n{x`}0t z;uCeJjU^_RR5ktZt1kr;+PofyX#WFdqe#)~bZ^1;g_S4veg0mqZtcCg;8a~OTNldJ zg|b3RPG~WNmb|awo-Z`z3uS$8$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%?Drd&#Ayb5OQ7&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 zcmZ3^%ge<81T(JjXDI{e#~=<2FhUuhK}x1Gq%cG=q%a0EXfl<`0tL99P2KQx?ewSf z`<|~`_;mfwXMIbbtyuVU?)1kEO>rWI{6u+~NnPnd?m133JbQ7$)v1N6ojotJSf=a0(6c$WKlgYYsk;UB|dS|v2 zJDW@uTWoxgvn@ia0rgZ=_NA-{J)$oqauqj^+hH??1k{ZLMleO0ZbmS7n<|iC={5tmVy4eh zQW99f)MF7iY!z&n6)Ir0V@_~jo8ZK3k5h0BS-UF)QwQ>HNbCeHyD>Va|2U>!xv+F| zTEBJQxG>{sXlwT9SLTtTw789~&OGudS5lKwrdT~g=L<+N|v|+WuJPq@42G}9Q(2~(|p%{~78KkgN zCb;+yU4xSwYp17?VE)wHg~rUETauj2tzDp}C4N@pLo-52Te2c)S2)EcSSCzKhhqJV zjUdG_!zk7nCh2&_4!~R>*;(2l^es|gPMQ=(F)3!nqBwrAUW4pztYw&VDoy~6O?<%s zXdWQr^3aMpmA$8C?|q+#Z28vD#_dyp;qvWidtM~aA0nZcr&!=X0oaK-eNtUboy-0* zV_cdrCa3h2s$adc^v#f`vmJmh-LFhu)^DCOM(@D!;c|23FaLaU?r`)wpzDPhBjRDk2VIch4AOcVVoIJ}4I`bwa>njNj5n zu4NY^lR!xscC@zRG9?C)Z5cQ%}#_<48zfr!3lEyalM>mu|gL1_D}b5APhS>BPW z;G`IdW-qhsI7Y+)>gZ!kG)|QGcqoR6n}){~eM$6}MGLb=PhR(RiP1z1OTMEbIpJ#~ zVjm96lCNzLKJK`0|K4UFO~(hR(#b=s*7Xl+Zkh&d+oEN%6)bOrDa`|0hDSxYt;o7P zZF!}9WNNnmUgQ6*#{aOUezB%L{rvi&w*14Qov$wLXn44zadAiEL(4Yx{Sn9b(LXo7 zoY~~bSw)WjBTe!28?d|9g^8u6Db(RncXuzGd=g{J^*=y1kxBC2#Leuu!SUpFp>aq#1p$FhN4 z%A53(IIr{S@LTWI!&?`k!UnG)Z1fsQh0f$P;d-9*y^?7dMacK+r9R% z!|Mn;y-r+a2)V)~-V%%(L#5#|ZyCl-q4IErw*upIs4`sTt-`oDR2{DI)?nNcstqsm zE(6@kmN7QAg0Zv983+8F?25w{#&weN)-fxX64uC+vX3xjEHUcNPxr8qDL-lO)-x6C zO4d-5FD4G_m`Z5Zz*MmYZ41_@qN12;plZzbe3)WtAis)T&D0*#A!b}FtiZBRQ@GjN z3?p2AlJI(T1lv6NXuhi}fP9$G5Z*OVi+&z$`4!lW58I+4W0B(pM4u()Yo$EIymnwsV=9nvLFVm=uhvu?5 z%rZg0FUZ7XtAg_5V{A+|`5DF+iH8G*-0Gkg!e4^lH~$EW_7p*)qf90Zj4nZpqI&#X zgu|)MGt>*v4oVG|3`t{+nkI)G(NIlzhp zIciqXRWemjTFuk|tYww~T%LellP10*Pel-;);yjtBurOer9LdI)=LDl;_vkbiSznb zs3e`BM+i?{T!4+SxZ%m9E@nQvaqsLe?qB;f^VzTF-gys@xldlqT>JI?Pu~C2#EWx3 z`Ssi{&fWd&Wrb2*`_;|UnO{SbpJzUPcmA!H=gv&ty>VF~Kvge)diV1;@16bR{>)36 z8&?!Qnad~dO}+wbEx7mYF+b-IyBD9lZnt_!%3v0@KfYNY=ifSc_p@KQ2hmX;=yPX2I)Cr#mAlvf1&#Ld+jp=3{NCB=`#<_*?kBI# zPn^%3`81PyW&Z86>V8#6zuUc#G4z00?}wZ5H}KBgq($SMJ2#^Yfn)hN7wLAZgo|4> z@fVi2AB~3D_Ty3R*x^w0c)K6T0wf{OU5=x}2s=D~?zirEDZ(dS&&-^;_sM5-=YKW- z*5p?=Q}fkO97l%_DxmbMo2O(8X3`Vj2l`|y$0Fh4=TL_BbIfk`c{U`Q0Yg)ULNlfw zWW&)1q(wII!7w`%56d({4n%^yY|9U)52TlDgejwX3trX(G5--ZZiDXCdoRym)0~1uP ztfMasO9Q>ba`@tCJ?#1Q@F8(78#y)r)Ep8+vNOUSSE~k(nR7uoSAqcAL9PseasVKP zL50M*keVSI3&`f@qRhCD2?lt%a=}FNf*TDdtc{3I};5YYlka~eu^^Fa&8+Nmuru(Jd9nfO!69z+X@|`LC4O35V{Y4 z6GXO}aJEkD$m)oSew|RaR;qhEOCVf zQf;SL*(FtWW%W?{C0%hYCM;`}mTeZRwn$Z5vIZ#48VPfmP`+9!Uz;_dJWZI(&y5H* zYowZuV#Ow@VpG}d1tMt)<&31g|byrS=S6FmOYlWqe4fK zwG-uCgt-c{*pw|n`BK7MGTD?ZLwGr1u9%EuD-d1@?Is7ZRS2&p%rzHQW@`{$i)fCX zjb@jj+;XL61;XnHbM=MN>>~(wqk&d(i;~J@<`})Q&2E>LT zsbNT1u_#fsX=}Ye)qex~0;ef&jOf`D0d(zoZrlgvNt_K7R4dRSiGtPq4+?6Y2Q2?2 zKw4w7?xyuP$le3+fsS1cO#( zlZ2+0p5g?fQ{%s0k<3u%RE}bq4;9}m9eTNdSTp| zGe5p}_B|N){cG1i1uK-9cV;q|Cce7)_WqxdvLzTp2CEN7h-DwxuqOh(P%y^Nqa)>^ zWfKS*KhJZr9;6?KR#-McizC6v5!ukcZ{P4fkkn&iY=q&Eppz{~Gy3@GF*f3%xyPZ1 zdjf$T1bPwZ10b6~0wQ&!NZg%J6hp>aPD;ADEr@7&G~$E4{eh!lHUegN^mqgr^*)y4 zq8zsuNYDbrJ_n$Pxo=Aed->#!3x}rsqODo7HBa=1yrYxW9M<*c((sPasAi_Q&_bHl{WG-Z2r_{HIKgHuhy znyu2Boww@6^4(JTZjl<4s6ip0xNT^*py(9u2CsiWbxDeX;5SgJ*!w&*$v;J#e@fjd zUrlJ8t*L_LS|9*~A+?txNd}3ejxSi)=&EWd`iN)GKLfmy`Xt2{ENZy?h0B?siWa@L zEr=FpvG;_5cNKHm>&{hxS3_PT5_(4e0vacEkzx!91EX?OL0ExD6X{sM6RGgrvw&ww z!oWF~=9}YXU0i!f529&lpWxy11YwoXC5-4epT3aLftWGH-+&Ch+;gWU=H5A_T-V&? z{P^@-YI5$>2lwAUjie5?=I>4aD0Ab?LYrQ5TDsjDE^oe`xp6j=I)DHDX^lv6-~a8| z%ti1ekeLn^bmryjxc&v>s;5_lS241#cR$w;!;|%EyO;-L8Fd= z&=cw{W`paJlN>I>RV_K)cbtv4osFV%wd7oV$Ju_{**-&y&c`I@V{o0DJQKTStK3s7 z#Hx0wsy$1X4GkEavZlz?rdd~oGD?5)gie$QifS;#Uy*_fM{)ytUzfnx28)s zPM#1-H%^sI?R~!j5G73?nCZCsJq1torg~@XRTm7GVxql4vO`0?xpmrrU+K~=)V?d< zz6)?j(+89{YWrnr3nFRBle7R137D2uobA4F^qsI+)*_X)r08_*%9LfY^OQ5?Ok0tt z!-9xoVQB(tA%V2{OYr*!37wqyQ3)Eg-LSN9(F!1~DA3vi3&jw`g3zVW$H$>-<*EZzQW5vil15~n+VFOWy_2}?AV!anbi|NiOwQB!ixGZYBK4(90A)rD> zGlsNU?pQhqgFW@V)6d?q*Wb3+E9XI^8YQYxpc+9l3HD}@@<^0N$S1sqn(Zr?kv71T z?K4A1Nlmn9lH^*{v6{sjp)vPhTaFM~qY5@sr7emkpmy_2)bj@xD)!RLgWaRzVIZB+Ht7)lEmo&9{Gp4XB0Wh z_(|yb$~+F*5c%8q(UcbE!7tC(g8^Gt#IG8!fFAv@pi9+#gI=yLQi2J+XfKkWcewS8 zX$twWiynqPNhC~t3+w{zT$41vs@O1r;(CSIg2@inVbSS$k<^?VizEXW+n3Dhg#4xS|;VYc|1TnRu*h3N1 zo)Pt!UMq=9w} z+BJC^ByZCB4^Ff;3U-|2cNbRxR;cIycZHT3(Q)F0{%PVk>8UEZ{Bv(i&rketv5sG8 zb}to5zx&%unU5}H-kAogWbXBkGdJD@dqmA@vDc5-MNPcujZl=*c@qVYvYi&{D5p3!oIVIlFb3|lLOIsgvSmxghH`}fy`f-hAQkX zKOi@2TuOA{kO=t(MGDUoM?mO3Ye5w$f4D+FKt^bGsF3=tA^ljkqJ3t^DEH$J%c#v4?fTO*vRvo5mi}LUu0!hkNOMh{_q%%S7YN> z)`!@LYy?-4kH_E`qlx-r{^wZ^nP+k-W~2Fb%OTAtm*+p$a?t>Um6Y$bvNeClP{1F_ zAZ`#3226eU$^(nQu(G+o@Y&a;3jPNWVNG!liff94wD3)%7?ldb=Lk69a9Gr3vTNZs z;A_k6LF3SVTi`Y;y=jB_c&`>51-}VN*JwdiR;O|1?1>{ z$36v>pZiGQ7$yx}>58h9C2d_HSv_A9<>o!)tPKL+ouaKvvULfzu2~xj>`z-}wu`on zl5L}4+n8x?ziR)P^Qu#@txDH-O7)N5sqepC-~UBGtluTo?-FcvPz?U7{Yg@RllEE9 zM#;1L*0ZAL`;zDTLc^luazp4&sBRV>-6lM-Pjrn)t`WgClCEsOQ`vdDvQy~l6D#|r z%6`GMbjqyXDy<(*yF9<#FRg#-k0Xbqkt4IqI{&!Mn>FY+IKCzT{22il(%OfE)UIzW zL~EDO_2ey!u>YXga!6`9l-ikgR%HpDeV9zwu6VEc?Pf)SU8mtKHuOjUYqv|a+b2!4 zl?_ungx1HU)?K%H#L6M5a!7Cu;YA%1ZNrjnSg;L4Fwy3ctaYN*Em_?{<9fl05~k`F zZJQpCodCIG;&);t`DbW_Sa zxp~S2vufT>rfqdo^t5N@`(HExhi1f~88u${a3IiiFb1vw)7Ff-6QlKka~Y0 zJQom48L5;JYz%yiTD4lBYb7*Fw=zo6(JDDw1$v2;anwnU4p=D3=1JS!X`B6P6Xk@F z0mu@R`LBR)9fY$%a%>bf?UFVf6di{oh%(a;O@EoSR%Z#D0l4NthIR=htsfn~niT1+ z61`QRx6ab8$&zzB&JIp90^KRnof6#%2z++?pUe!l(fJ6ToBj0e3wtlDo~pmx3daCV z4geejG`YVW`~@C#vMXQ%4P8a>-6sTdT96RWtO?k`JBEC|ll`^3nuvdO_Z#5#yC%z! zUjMs}o^89=>wmxA4w*mbHVkgl|6!W}aGCb`!tm+Z=R^OQ;}1sRvvf3sqxg5yePoeOo};4lynO{0cFIIiHL z+WxJ?$&duBhAznD3kUy$-GmA$4*8F=OVPuJgAvAeILJY47X32}Rv-}MLIDQ344JYE zK1ZvqMVW#iFV9T@tq8vuihwIRXSN2U-A18ilUTD!s@Zg>X5en4@q|D>G-12b zLk@XbxC9K2JA}Xw5I~lLT*^neh@XkJ9p(A4Kqv^Gqc~(ibLhBmuORRuqN9;D)dq$K z{tpAi`Z3|Ms2`hX*pgAclv#7-K17b~n94BNB%v5;pIc1)j}dsiWv8kVEw{2crDOgW z&$b1k9NU)rET`Cu7NsK22SZ9^-mDZHXP?8iAm>AaZbJb1(cF&_NFuNofJ`BqMltz9 zs1TV8==MbX@ShbvImaD!%UwkEQ>b1s{@_+|@koT_WK#~oj)Y>_Zvnao^cI*)Jj8C} zu0a+>l4IK;M4Hu;B$*~06ZkJpI4AI5nlMk`ze3fQ#3o^>^d(`Pz<=o?(kc*3q%_eh zJUpd|7D1cRM5mxlX`(^UrZmwcXj7V4E@)Gls1vj)Yg|dL6doJM66k&48RZ=e;dgr7 z`m7$x(@*r`H!f6xU1TLW1SS=NGgjsOg-3Y{Uccl$qToxwb>(E`Oyf*KTGy8)0AC2& zX!6l%7!AB`nSO5<4*I2?&*AJsP>KBc~JTVEsSYtmGM zpiNn$-q3^W!y>d1&ef@-l4F(VXp$UF6FbtBbz(=VFV%PI$Q9kaBgc8LXzG5xhiTedpDwH7u8g`imF8h|5~TAsoLVt-RmAn zp0UkdZm;gztv7G_(!8jdshDtr97&itNgvvZ+7&isW zLkm3%0XMTnjD;;>tn4Dj20uHyxX;8mPEwuX|}z2~)%p{jS_}`}9olNu8&X zDPfngy0Tm`(WhaY(65RqWp(NvtX|QIVirKF>fFeE6jKK8%h=^i`4J5w#?`|LEDY9! zYCW~U;i8j-$E_jQ+WyCKLtO#nYVL*btbkhdbF0g*z;0ZHpPYaa*@J)XiYZjo<<>}+ z?Y+Ib_p-5QBphQUv;6kcI2+@k(!lsbUSBjI*%1WAd z2fTA^EFR>&;Rqk-8<4EKp+m=UKO5x(k+7ST9PR$l=4doHusagtVL(<{wl@+C9`yN- zK$!vB_4;B6rCI;sh&LEH1h_8V7YL$0bau>`R46kDB)wd=zeJ-l9=c1$5X%I7-T)Jm z%reRkMA?{R@G*=x91r<**_A;L2mD@y-}j#Yc#0sA7?Xp#1kn%gasoWRn1JK0LN$PE zvrQ0ADe(LV35wAqG>kSu_Um)?48`aeeL~9^F4zb4yggS=5dCxxPv{c*D=^a4{Ae!{ z4E;~qr-^ggm#INR!q7vwjqyJLVD@4Phm#_+wHm*obi>ZQ-` zef`$`kxw6tznHmsMW&OveDePA%RttIM{kY#IA6#$cUM|nG94vjGE1}jp`B}WWj_Ds z%xh=ow#e^jtLxs4SMGf^djH*%Q}3L-_vL4jA38xfX^&e#P?U>0g;eoW2?GBKNh-w6 z^QXhhhoWw~WWuGJ{d{M;Wae0OBz+uup?w^)lRe4?B_m*H%1~&;t#`4Zh-BnBUpUqo z?u$rzm|0gmB$@E)?Th&iu@a5(&i0sO7oRyU`Z(D6m5 z5v7M6jZa5{@emtp+z)%$xSNagvwl9-xO;#<90@nJH+MEN5&w}!I5p@e>sdA7hUkD) zeu(9PSEa7YDPL|ijE9(vodWOx{)UL@1%$nRXxm+9t>|n_m%7E$wdvyJV)4p!NsU<2 zjA0{&tI~yKX=io1cyYRXQQBEC5<$gV2#S08-+wX8xX&wU2UE_3SU)W|#YEB5SW=Crv& zO=UGdY%(bCuU^R9xG}dI;|2+I^&qOSPk0A*fP+77FJ z;5+lioA*aP0KOkwzYcm|Zkf3-p1C;mm#^OK{fLxIff%wQyucwo<6v4G_j`kZ7(azX z%1uiKIAA`W=Oithat^JqWPl!r0^viFu4C`s?!BOZqER-?a7I*VLh9JdM~<*z&>L%^ zh+BsMVwGEuz!Lx@1Dt%M9A(Y235sG$^}w~DMvEe0FO2Q;9}cl$-pfReg^``*WjQXw zah=d)4eIzc0Qq?TP)Jydhqs;IKk7?aYDG)!P{)L$^t@qo;g~PwSSdPI4t1stmgKy|LdT;ECY#j9ohtzok$jH;gHU?{S}@dmU?3*IM4a&*Kw0Sq_2H&jG|^R+O|xhg zBy^m8e!5v&=DD>OwV;}i4UtX#<{b8S<_XFwp-JeG%%3@*(14mT#9xOOe7Gl14^3V; zEvuRA;r!&xWO8`&^oI{Fok3a$8=8dIqh z*Mr}UAo~|92r%0+FWtcPC-ln>EDJ4TBu!f{w*lCbv@2VfhhziRJ8lcO4?dpd10hz@ zh9k!$I>vK-_|?dQ%fNC_%h^yrOCTJJ!bOhF4T(ZV0a7wh%KO58R?_j{D*+WCAZY>- z$*^UBXJeh+peuHu{&*lSsx=|MCg#Ny$N+M0vhJ8prmgk%g(Vs_@IT4Vi~4nNX` zEvSD>u73;QkjM7PPt^BrVI68xmupf7IAkDNR5H?f{_urRs;EvZs!P)8@})`BaPw(< z(w;UWRfiQ3_dG0(U#TR}ZI0~_9+3I$kQonfe&wQ8Kx$Q>wg+abA#l&pq0z<%U}*W! zU zwCaOj4BeGkv9u97Yx0>h&)>0D-nLfC@{ppcMXFk$s^Ml5thFi1EmCeFm+>B|wl8l+ z{yUgx^7a`Cq}{>bgOTJkOmcO6-i(}kAHX){^*S~^HHQebQ3Wj&x5bPJsNVcRt#Z`) z4LL@u7Lh?JLBVaa9c~SUi8o{3%{EOT7(nHN0eThRdbm60@SV_(=0gj)^*~PuGJt}( zn!i((bV=e&Cy)G~Z0ZMdT22Wq@66#~3L{?>Csk>F9{i$QJwfmlGx#*-Xa-4ZD_~En`&g{0pQ? za!hJUAQ$c#O8xm8R4o^b`6zgpDms?gbQr3K;OiB%N^^LEq`*T1b2GSu9Ctc ze$z~SfBxL5ElS`3%&C_% zmrrHm3D}@$XlURzLH$Frt_yp=Zb~xx!PE6e;$a@U$PkIdHUctWB^s)*!~2k2uW|~J zz#%2@eKCM@#34{y&+7(B0;4ffaj3$Bo(80WlwpYiVP=-rzwzi-g_w-$gBaHi6Tw1}1#!O}8eDH4j8kD11|q%3Pi%UZ#*HdEVp&H9o3nq9CgOIJ3F zl~3NO?6_Uo@r^%KxkIenAy_J)7(7esR#Jw;)(Q7o(Y^E5^C|bUqWf8)YEE{zAgCrR zs1+XDC~VrBa`cFf9>LL*b~fH|Hs5wO3oY#_XNTzQ5FGR8%<2u|>h83|{hMBK^;3WB z*)R4SnpoKU$Bmw8opz1wI|9I85dfA}J{lx<{9q#LTZERaw@gCs)2X`sV%`4a_O!ip zn$TFg$#nVR4{G17l{M218lI`D%_6|^En@kWVZ(&8YIK`W|D;&IxGe zmg2J!(YfY^>6H#I<^ z7E!20otHiu2y_MTU~~wMu@WYG>NA>^H??)3!hGVz+;yd=+d^vk5k+8TbJJ8&v;jAkc1@ zi^VS3SEkHOqPb~&&u#M>!MtXoaEajBEVS(t+kPS(^rs3Lv5*lg%rs3bTQ1P$BI42| zbD6T$i?(`!o+oE)6{4*P7D}|Z(-v3SV*SoQ*?}_v(*$Mw(co{0&9Z62qJu6ukf9wy zLH)4I;fkpf^m=j^To{+eUVcF#_G3qMJp!8IWoH|A=kaBl{A}xb@7=^Lx%O zAFaGx4||L_2LOAFIRD4N-yk8eErH$9wakDYKOvaYtc++*8-N@oRoFW*+J*|kLbhb20|CEDu^!BBY!M(uU{m`FERwnRP-po(8G+W$2@Qt91+zeuP(0_E6XvX`LEbY<4&R=s>*Y3L#uy-Ukur8@>McX21;zLP_`~rwoWWtcc-lLc3Eet z>?yJADWPa~PSO(<4Z^App<-*QVyjrO^-jh9+ZFp$6$iwM1HyvYIcc4+RgTt=FG$&1 zMO&*tKQdz@(M@)_>o^osCkNSfF`1->kbC*M6!u<>Z z^eq6Q#bEk0fF+JUT+6140hdL5*q*{>hx}p683MB+7(=bIAB^GEH^3p^St^@TnfqS? zyy}u&QI8f~-lD>&FUGSC{s_l5WWR~Yrk+Wzi1UG<92Pgq1;^Nf*o@=$qd~O+m=^j- z&7DFZjsW_oB~i$1k*zfb6(WlOouRNV#Cp9_mO2`idlS{8ZO+`Aa$r?n(WrU}~+{!0_~A^evnj6?V@Uv-yQC(M`c z66PWNm!3iD1!A6@CfbBY=QPnQsB@ZFBdBwls1npUP1FeLoF*0t>YOGj1a+R)FCmu- zk9STJ=(+DX`56e}XL{A@X)TneH?`p>E>wVa(n)rK)r8=m*m4dLwVSQkaUKfNii&1b1qSrbcJ0O&L`YSPuOGOLc*K$g?$$8Ow=bg zg*O2%%08)1b{*m**9k7{m)x>b^2lhUp{$%N2FfS`E-!F3R_*e^e6z&7iGua3ly4x& z5T_JNMxvQ`&_O(}3=QoYl$A_6rO3o>&wi}Qic0zVV#%SXas*IOjvY=%66s;UT|n-O z!xlxRD_FMp72>91C3}I0bfUuMn4_W|LL%VD;eGTE0DBNaZ<85!WSNl)Qx=Ud<%#5& zag4JP3$%(9iJNgqJW$vQ7f^;B7U zp5e!N)mxT}(1^2)XB}C-R_<{mdERFCqf6{5Zd}L;aQ9x#1b$Z=G;`wT=En<5pMF-H z|5@>!2P>c4T)A_vc=woj=fTR|55LGAE57@n_`8csAD^^2t5^T|$Ft^xKNM$vW8S{9 z`u@q{`KhIaYc>O2I(cvDlMBn!cUR_*n+tQc9P`?V<*8F3)@7aEor&UTvaQNe-EB*s zT{dsMXU<$NUYsvZ-ZmF5EPrsqR?Svw{Z4Au?l$w&ljfN#=DlB;lNYP_2m-g_u^ijo zNBC$=-QBlJEdoS$ES&^DlT$z={Ae_x$*Ue%ijRT83A1zPd*DtUIi+Y=jzpDMJRanU za43zFz%Ry?-Qc2FC9Z=wE+5)Qzk}4#RU7qIq`wwd)PbN#1W+Opmxusb(6LcVNgyD4 zM2=`6DUymNWm5k?V&*U?N4OCsog%!XB{K@9K1S+O@=+Qtku^^bIx)=^*i9FmbV0)j z;-acVtm`GdiY}1^C_}h|X=#jjV(HXqS%^X%i24JxFMB)orxRLIRyw|)#z#8#;q-_c zQ(f5tr9a1`Wqyx-G-$+JN4o56$YvC5kjFIMHS*;FY#=HP(lE5ua;aW&@#KnXz$gx@RgaC*@1^mdZ0fa=r;oW zCAZ)K`2cc#|M8)gZAM_{{MP&J`9O~m=+PV3_uOj*_g0>n+hkDg;TjmsK1p1Ps=OZvkRE6d08EP8>MAx97QbgKO8h z_C?O4d!Ng5L4ynG<&J}}yT*V55G(_R;o3JEaO0(`l_CHsxnj~cL& zdlRXZlulhG%hV?IamVA!MeSMRY?hs+U$LQO9kpjdk>JWj0$huR=UN+~Mbc_nR;`qI z_&%C+tX4y%S;(d~mobEH@^cUjH)udut@<=@Q$afhWZO;8U0==pq`DBTcgp``tt3#YL9vPlzHv69W)y~I1~y6S;9*4D5gbtKzkbC>g6lT(^r=kK3YC^$Na@{sDMlN zC#x$X^>|VT9t>WrrBqs#AaM|{?b+pQ?zMt~aM4UgAwHT!pC?y2F*Ey&1fp7MUD8a6&fTl4P!YK8zUUe)WqMrccAyJ!NFYvy+4Tb?tN=uh}6IS z@`1?UuA#v-0;W+Eo8mFRCI&@f7 zGqFS*elRe-I!yomNPQBrTtcDj*8eD`;Yk`YQAL$Qu{4%L@K-w=PYv5{&``y`R$MU+ zNK$9-I4U2sOmBGxrk8Hn5mOWV8d+Lf=K50nHb~ME@^^4QEYUKeGy*KK48s(VFK0ak zB|@R5OfFbZ1+(zSz5S z#$eBg=CdJA%)}vuI*ALxHKa+LVIYnRZU503_;F%1gJx$ulc7G@`h}RlpZ?t@wggsB@G0bp|(@dy=bMki1#iOxp`r&xir=tkC+`!eyd5m3Nm^X2zSCGHHNcjf* zCSvEJiEtzx^P7q5;Ly;qL0(9wk^)bh+U$oCFNl<{H<}oV2;+dVd~`Gwj;BTdcLI4Z z6_1~YMAaqCs~ZHu;xd)6RD%dxqh{l92(f^lg7-085*}LVsKH44Jb?%#SRqRZiwz0VWt_5l9~G8=ro3~bn8|G3dr-LlrgHD$=KXg+&AeIo`NP8RudaV^M&qnp z{nwu^Di8lynE#z}_eSv-X9|~R*H>?840P$tgY`dO*_gY(xqM1lUDV_#w@z=&o&~WE zReN_jf+LA`-IU$!>mOZL?!2wcFBPsX7iR7%t5-IDd0IP{cB0B{y4}0ml@HG-7j7sI zexuA>t>F97Q^CxS~0JkBgXNSH64(j5Qf`s{SM2N;>ek-w2Ww4Wt35USV zu}B;zaEu>5MsG^$=&FXg4LwIWJ(0NLTn_-b79O!gjABa zDv6{~N^+nhGR}u3P!>)`5FaCAmKQ1(9?!h8xfCj+;AMuOPCWWZj!j7spNz%M4W_F20Al0# zXo`cF$Inn&0DwThNqal~I$eJ%GAu&)RQaFbY5RcVX}aS%0Gg+PL9Ood$1i^WAMTxh zckj%)cjer>vP?&g>5!R@HO3{ocW0SBIcATncT7bP7xi^L-&L8@2Ft2BhrtN7=Bzb= zXp|)=0EU69jmnuP5N@b4rKQG{%qvJVN)vUQDN{`eVT=-D7L8&9dX2JIt#{foWm%v% zs%cZ^YW+~24B6mxG@{FGj3#8YO<6?a7X)DHFFu6@bMQxSZ zz=3TN{o@I_EuLzfI}ix?EyTj{ z69f%NUxi)8>o+#$ZmzGsw{hv7@~cx&dDmBFDl2jTLV?tQYlC}BNs(4PNGQauxx8Kg z+-kHCCX!AI#6#0ARH4`eALf%2YOaN{AWSBsu@td>kLDs`NlPN8zCaq(6&gKJ31dDP zog@rR?xf+s(SZZ64i5GW9C#yq;OMIZL&W>?{^Q}nzM;V~0n_O6Gx!@Yl*>=~NW`k< zJb@aImJ}^gTWbr25vaYu*#)nV2T&2Hvb!_Obmf>Xndw@yJ9GBdti3H~ZBnShxfAo zf`7I@>uAe4+GOSny3*jwHSEo^?(0oCU+@oNuJiEP_I+xF`Zujm|DgbC6)hT123DJz zNBqtBdAK8{4uEM|!aV@sR~Wu}0321T^4r7|PDZ#?U{n;-(Rd90X)yhom_{w}CLklm z1NJk7x9<|BUJ&(HN zFY1rbF1fnr(QdiA=h0rdy64fCk6%}Zt)QDV`IlWK1j8qte|u4W dgUcP})#+By(rB_RKVL#He6m{|o(R;B_CKQjNO=GN literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_push_dispose.cpython-311.pyc b/dock/dcm/__pycache__/dcm_push_dispose.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..869f3a2e4192da790a65a408dab71d4bd9961f69 GIT binary patch literal 11379 zcmcgSTW}LsmfccIYQ1IImhlrA8;rnDYzATo8Dbmb5Nu*hNJO$)p>BhW9^O`q*T`F* zgd`$E5~l)#lfXooWr;J8#M#HhVUh_STeb5syVIh27FAcdDpi9F{u*a9sh!H#o_kx5 zZewP$v$cD5``p*L=bpZO@44rktM{x{69M7glVxL}H3acHRLBQQF7x$=Btcvx7=j@~ zgjY^UFNw<s#W^OoVfK2#odd0jYf2vvkvc~{{)9jXjhd8@+J z-fCPnhHApK-df0;*fPezPuP7=sy* z!>FDqJEQY9Fy(9`tFO#g6Qdf&1^w1C6|7#}gEc6yC}tI4t;>%-N->pCZerInRmU`l z7}o-`P#yAwH+VMymo;Yy??w&5ZWwFM4|NqX*YX9z+XAiV*{H6+3SZ_LJaPa^K0MephqKFc8`qg8)jqU(MXJy%yQ{(;%tofP?CWOgnj<;pj3`BJ$BsdkW&=l~zEJcCaIK4v216_)OfbNsw%h|FNKpD$ecS@y%9EWY`|!fR7^Zd{QOP}2*a-1+>CyXQZ? zH~W0{##Nb4_R5*NQ!fHp6CSsF+|T*LZpF9lcFSDI<1a@--@MzM{p9bnuUxnvqp%jb z-FN0*zVlh??ptRT-#l~Y7e9CJMayKM*PZ>~;@#_4@67!JV(`jacjkU}_x#MgAAP*= z_Zw)<3MjrGU42TnEwbX**G@jMjuVLHzkbz#x>kI`kqY%$+ZY+2hy%YC!u;vPakx zO0niKa!#n{N)aeSpd0}g0u=y2%76@vb0NuiILe#?@f6@eTFOE$FB@Yr>@)v@%>W0`WdSiUjitjv_vW?Xfdsv3ajqbRLx z6e~Ao%GYFE^-I;Z68#bZU~=G_I-<5wtm&NXpL;r8(=XQa3$EpHawuc25~!-Lfm4{k z`~jk6PX;72tXb}q4~~>L8z{(3z(aBkOX|M{fh9|7wftpBMspQcBAz?@<_`b6JKiT>Lo*o9s(B7SF%AvKY^O%x<^xkWkC907&o8p3Bb7DMV535Kw&lZj}hb z1xbh7Idq`Gsqa4tiys!tBAUX%INAU*ukz_p;5hr*>vzw;103J`=pzvMGEVl=Z1(cx z?>~EMX<@5#NNbP`(BnujazxVg z4G#|ugR~hR2N}#EX(^eIJoWL>V{F7jbDN=x`yK*Y5O@fIZUB-2ga8tPvW(dQRWTYs zPEfeHO$ezb8u7u{{=m^N8-a}=A5F zK%DHKr!2{XX{ubL$_1(%mG)m~I^QzWD^MM2szanYAoGo$SiLTFWM(2=_pn&^@Z_F( zyKCwPsmIdxHqqWTxjREyUK)CC=)&Gq)66z;9RhttS?RN5~*E6K5|{?O+mcN z$||1wfMRPDxBoAT+aJ2*!&p8;q?El1wMZxyb?Q_>B`P35h9T8kVS)@26B@ptP@xT~ z#^@!Uz4#2!n$S*AJd*LbP*m9x)L20jsQZFIvFGuTK;a$5l6sSK<%}jlzDt!tnBy|4e!k9Uq`xEa8 zA}XOt=(hvvx#@%kgo`2mDirXRUN}3saOtePRdXBj!*dJCsfDxe-Fx>Ok~LUaygT!w z?2Xr!tBk7E((P7JdE=Grjq};$MbMB`g2a9Ai}TsHK~+U6A!uva7v^yLY5m%#*M`?J zlBQ>b+Xn1O+V(EyTe1~P8;?bz89&eR!7wXnBheEQ9pkxCTr{#^W3U{wa_ECfmS7|{ z4(GWKSs@aIv;rhxpqBSX0$|wiU_t>EARuXiQOWS=DV~iD41t{3jr!w(e5log45640 zQy_!L5XzZyI+?cCHyV*>gyf4J9$P_jtYV@Ob|u-t`Cz0$cw!dVE;*^;ARMbiyZg4i z@s_P4q7@RdH$>i2~ zN4e-&liGUa;k2VebaW(j^XBp?_VR(Wxj{4orq0-sac-G9DLA*JN>Wd}TMijH&K#KS zxc-!!Pxd5x=B*Xey34V&wL!E(N3F4CMu(G(vkUd_%J=VrJj9s;a*FzX>1;+Q&3Tk& z$U_98rRC?lr;lC=r%Ri~(&i+csalgXO?95NC+!(C5_4D(afe`P0!kr)>@uu<@Q_(a z`zYFoD+?F308)zrwHB~k41sBnHjOrZ3Wk<94K}jCK0xdDomcOE_-=OU)$h1h;Ov#R z2Da_wtsxs=%CpzK_|hx)KDw|t{SIs)j1)%-u+lh$O`3iAoz_8*79WS)Ftkg0h7Cj+ z)}z5Qh)=s1nnri-D68_1!ZuMUv;_tBFyOK9#L)4WTaq?HXH7nJ?%CVc`dij|c`c-= zMv-b1s75%E1nZhK)gV$0LO$Z}r}}hAF5^#E2S=;?>5Rz82bo$tH5g6KJ2T1`)JHI; z3riqDBuKRc1!G!UFs8K$EjIx2nW}`r0%ubsXW@&l zNQ@O656j*Lsn3O8DWopl7Z_U2&jg$kaGvg-p!wBBQs)j6@vx)_jg2`1WG{y6Gw? z@7D?nymD;N50FNi3oCt}2~)zvZD!1=syvo5Wh`figsFGgI#a*vgqcSQX)Ywp@YQPa zCB~|r!RJWTYO<**!D>Q>W-g2&8yg4px?t?b39dv%$FZCaquo#NkL3HqUU29036e(| zaxRS3=W}3;slK&x7N3EZ3Co0a!p8UI@sS>vAQP5^HDOC=uEJisR@iUKz7uxUs|?>Gi@m6NInu4hbT)t~f$5cNMbN3PmMPFatw{24bwR-&CWPG0q2n zec1KHm?uwzDTme9!XL<&VQp>9=NT7M!K{K4q2PswJaT#Az*I6-mzK#-HK%CRqNjo2 zk)4+djRbdg!l_#C)%-L0Rv5o6pI4tP@a6?c1FO}!av!GVdae51K~sjc2*}q4HSY`Qmc+gX!$08Bog>UimP4;|);AzJnY4moH>r{)z0&#=}ZP z937k;n>xET-+lgryEncV#1_cuD)}-6IaFK06!EtfUB2;Z=&|s@-@)kFmtM?Xc`4_z z!P;AEYb%Gu=C@=s_AGu=w6WlRqf*Tx7YdiLKC{@32P1}(JHV_Su?>Ozo*G+kk z=JrrD;19)iv=%Ym075ZztW6R^!%jKQmZoB<-G!p8sH1}-j&_{N_`@b7xO$XxBHv6X zm1dY3xW>{l>~W3(3WM=Ah|cAofV1e%jrZ=p`cd|$&trelE6TeK=);WyqC9n0dFpar zFKjr*k%FXLH%k9gYaCsz%B`{n4Z1QmH^~l3)3rH{2eVd>XjI;JJy_7%k=oDs5fU{PgKIw>a!tiJ zq_#-4n>|E9qk6p&-v8#a$}HzV5us ztKcdgkMkT-=cE$c7hPQj;vC0D0;ePjInSk%o}s~>1H;38gFR3CdWH@Rj!5>sJD>6m z_l*qaDzFud9cbM55iM#g8jN879fy7sJK>icg(g3JD(MN{w5&?t-cfSwu_V@jN;iMdLIAPvuV#E(Q`;>xGy@~;1w2Dt)FR`t4=%mL`R?C=*zg; zZo4{fxjKce-n6SvbfL->V|Mey;^v`@!}HH0;^sZS-G5Nre`LP8^S3*^OFHfMY+n%o z{(t~5)c#Ny=rS5}9al`lj zjS_c1GhfvOU!~auU!@sX0RUfR-GhNZuYvcl*af>MZQdZ7H_Z5NncD<&+q`qN;NCWO zNPKv|aNx~}q=xoZ=xHHw8GBs;6mGzfa%b>n-$!hSx;sfv% zVJNl#0RbplzdO(pmUbBAk8rDEsazr~I^Zr3vS=qaeNb_|Hcf94=`8}i1vd9o$%X#& zduJGd?o892BHam@CH?hRmol{np3ZiGMBatAym&?WTPe?XSCc?ik#m{r4SK zDE!9!y?q9)V9-HcqJ6$F-0}N-=pR4+VAMArjfMh1r4W$a`aC>e{|A8kU9b#!U8A;I zzspS!SSv)qi>JE8WwifmdZg;^hyw6xa*f{{Q({;WQWaJj1a%;MBt5~`9cICjW%7Jv{9G|3wVdD;= z_K|e$BVz3%w`=#^s@<2a9Tsbcg^J}dX_>cGO>duBm9}jXZJPx8ff1|s9&*sr%1r`m z++GBpL;$NlKY*O%YULuLwxmJ0VW0xu#w8YzUWV6Nf6cwm*5 zL)w+B${BEL)Q|N|S^JYUv@mE@p&06y|FZ?2U&*d$K>sX}KZbMMALH59K$K%!b2r1X z&TEnz;(RcKFNkE_*eKVYU=L$GmP1|%4!J614e|oY{SX0kzbjG5c9PZA!>AJJHfYNr z72W5Pazmi8xpz@J`m8y{6}A!=k3?8bGUPJYOq0zgbie#9y&YaC9%6TJ{{%%6p4eVk zq)S?oBr}9<68|!UeG>mNgmDu86iwzy{QHvFDy$U0BytFuVrEz%{!GjeJ;H-yhVTgL zm?0Vjb<7Y=f;!%hBX`LVb%HuB>FdbqneHV5lAEB6Kq4G?N=}cA;WX2xbY^H0|CTBUstPFL<@VnLz&Y>$ literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_push_rollback.cpython-311.pyc b/dock/dcm/__pycache__/dcm_push_rollback.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8ae74fb921615ac919015553d0a27271100ba08 GIT binary patch literal 11457 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 M=^nNDMxYAw-vRmz4FCWD literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_push_upload.cpython-311.pyc b/dock/dcm/__pycache__/dcm_push_upload.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6930206e1b04b7ffb6ac23135603f9938ad1fa32 GIT binary patch literal 9075 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 brjCo2y11oYwA4qt(J!ed$V$M*$MnAdaUrr) literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..009d15a1e7b8a5c38112dea70657eb1454c1566e GIT binary patch literal 10394 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 ADF6Tf literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape_attachment.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape_attachment.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a22b637074e92979a4195593f508a4bd049b295 GIT binary patch literal 3657 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 XovU)pYMEJ`HF9wYVwQq#?GFA6=PG9N literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape_convenient_form.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape_convenient_form.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3958e66c32992de348dab030a3aa133d96062d5b GIT binary patch literal 4737 zcmd5?iszvKewxTNW!0vBM1E}hr{CzZ{Q9LOC56k}r?&+*2~rVB8`rTi>!LI}UkK95eYzu|gO zqa|vFEwZ}d$sGACb@X@cw$={!%=LXUQzuh552g;EnLT+xcVFi0cJ4V-8*`>_V%`Sb+)(NzkVq5)>Ykz)VV)` z9jVVh&3yPl`pih?)+hub9lN8qtJ9r6KAC=JBEQNlWVg6Un~%<^N2FLzrvxWWH$aXh~5&;GG&VD|7YC>N%HR<5K%*0gY=6kTB``1sd z-?;JX_`dZ|tb?w5)9+^{&go3LiD$=W&t1!0nFhB3b4B4PTcLXkq-LhB&0f5gKJn>7 zLH*!m1vTSeThY8n2;pXGphhADzZ}4?H1Ea|TI;AMkyKzVzuNabRgg1i>TZn9un2E zwg5nmh;_co!n|sE0goytM$o60hw&>EgS=U=X*7}&;1haEh>c2^X>XJS)zYkpcbC8q ziF7n|qWjzJ&Cd(LXb6kVF9>9?xtj7=yzP*_j{DaMacC>m$Kk-Gd z*Dr)$#^C@COJ2I(rpS<53wAE#=L{o{z=TVnikE?8;1@@8OHlda@ojgPt@_ybao|e$ zqi~{Wv(mIV(a@qaw8+b78L1zs|MQES1ZodvUBxcpT~ErYm?Qrb4Iq(I)^ARo1LaP1SG!mn@UvaQXCEAiznCo z$s~98$d2BGqgQeC%C_$muJ-}hizu4svH-yn8S(x$)@gNC7b3R?|HAqxT_8G`y{z4qw%SzPE*#~CHkK&madjZB>%8#Xq8xYRWsg~u| z;N^XiuhFD(G!#@5kNpCVRc=5O!m2SS_;^t@i&*kW5>d^$$l^#H%tuxLDbPeUkX67F zDYVjnA~ev|Wxjq1lkbg&H6ZgtI;s_j9t}z7($S8uoDH{~4Z0`e9e12dMt8iwH{o2X zIM>1_XB*zGlrDW=7~h-Vo>sW0#a3%VGN*S8~6rv=oC&x}m z1|@)x5dG1xMDqjyhFYedi0pCF&dh5o;6z4)QadD+CVD2DG${28A-He?vhEDPMYCLk z&Ush|L>Gs`{(ztwe@Zb(Wg}6ESYXptht@+6oj*zl4*Q2xj$*g!Xzl83eXgfxYiH}v zy{%o(b@r*{9b10p?b+JblVuQkBvms&I6d}+hPP^?fWB+U8}Uhd8ck#^@bskO2!dta z5DrIk_)Z}~yYA}1s4Y*m=U3vT14McfDMaKzqPibT{ykd$0Hd>u7RhE1+Xz+s8xS2F z0J9(2G}Umu`HLsNY>|KIPb}w^<-EM)JB6#hVtDY^qf+RU6+~S8}fgXWdLo^Jd(u>V%ZM(TNp#9(C-b!2)3Uo%ttW;U?Sk- z{9mX-P1!udcEVx|0Y17CEKZ;PD1GQ=YIw5nFw@Ij_Ef`lt&t@(EYu1;cKZWi-rFA_ za3`c;04K?og9kYBTJ&jgDAN3v4aIft&{{3_VGf~9!ZM$gY+6j=!yt+IN3l& zEcE0y!Q!(wy%6N+phy{7iqm(^8u5t|Zi-5QpnhaabjB-qmj(%hUb#pO)o1S!g27!r z|6ul3SyA|{-6(0wP+7Rn>xi?S4yCA|c|x^#yC6O(z{qCYC<%gBK zIe{cvDi@a|S}7Nohsi9LQBk>DQ57#sR=VTmcOA~SJy}&Jql!5bGFFbXB)Ll2xeCyr z%n&b`D@EPRi_9Ea#Pnt9xL^O8TCcyqs4DC&YOiCN&68^YNtn%Fyzu3V@-O<8j@{bl zLQ;xanQoY%lBvx-)fi5cu2D+YB)GK-w^q)TxhllffOgH5{{mv;oxT77 literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape_extend_info.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape_extend_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b40c947808ae4ee4271e42011ab1514c645f0f9 GIT binary patch literal 3495 zcmaJDZEzFEbx%5dTe56q6Oy!uNl0uamLUD0rPQq(VJs3p?UIhBspfpQvL&B%%H0Vt zJx>LJ@KF++76%UmVupm65Wa>?Lu1K6Nt8ectVW}j0 zyL)fnzOUVV``&)y^|}!h?|?t`p%bBR$e>zm3&5jE08SzfaTJeY=AVjD#@G_G8h<)Q z!{5T&()O4=?T9%jLTBZjX;;i;z%=hpdtx2~w((7AZ_I1JcHWn6jx_`9!2X1Va~_~$ zEu0J6IX6bB)~Yesv8)QB1kHJXY6(a6qt2!W1iz)zqBZpm3~bwhrL2&Vu;wvGZ|1Nh zcT$>zW7Baan*`97qpO)k6hHRPt1LFuDtJRdE#36oh#qu z_qz=aef&Lx{Qju}-_KEg_hbF|dHwi%5Ayqh8#fFDD_6fLU*G@m)-h9Q`Pd=-%Bjkg zYx+MXq7e}HWTl3chd$MhPM2>?>jytBe{iHcaTv&huL#0O5(|>y}`{%6h~hX`Qh~qSHK& zU`H||cdiTS2X9pNow$E}MnC)KAklH`!OUdk%13iIzYMMc6Xs@S^rPp@c`$(N77?;{ zd3abz<8V4Dg+X9=2+LA#P#(=a|Z7w4ip!$=~GtG zMeq)S%Za?!v@j7L6u41|95d+pHWJ<`@VPXW!mkVBNO+qlq%bQ>;ccVxu#gEyeiRLJ z0y`20_arSYu_BYjacsC@JPBSH%8qJF!N$e7su5xfsPRFQegjgi5BMjhDwR@phE&%tXnuIZ{OtdYO zRE|JYz!jmPZ;_VpnWa zo<<%rR_vcy8+1Dq$5hjkXyvCLV^iczjZ%kgu44BB0P&Un%vK*$C?v0_(tvi9BK~0% z&fc(B&hMmjG*G$}r{bvFty)x0Evd?14}juopbJ!I>*?g0Rj~7IRT{-nzf%w&Q8hmP z9?p2oJ?4?Ss?@N4r{X$|IG5sv-mQ3`_nc{vK_P__U##P5QmCn=kEJ|@>dzRYdCxSQ z9m!YyM(gx#*&BdclH+BPC_)?~Ga)`H$#toM2H|9b9+8thiIQ+2Z#a#mK%!7A3iVgZ zF;_pjh(uygr&Y6rR%;HK8RT(1q1j26iYKa(L?rI|t$RJevO+rBy|;@=$XI+*SO~4> z1(xBZ?$BfM7l2**A#{0EXdg1`#a~UYz1BOvYjSrDC6cox`I04JDo0lQ&5LV)5!MU5CE~J+arte=q z0w#nypVDX&jA1J=D{xq&Q<9L;Y`nm5l4u7m%|im~;AlL{$itc?%W19%9A&S_q_GI^ zA*40+CNa-#X0q92W=OLoWt^7CyC4G;nnT3q#@N#!o|iP&hICdQjbFQY7otyw%`r zDV>;UWO$V9qeW)a{J3A_i+Y%^A;SffsWU ziBpioYQ^wDZP|DFow$xLK$G$izRh%03Osva^W8wloj^x1uu2WA%5R%Yc>}BeX74NAD#92kMWZ~FZx!hzLoin_Y8;qx_i6Y{fE2V+?{T& z*quD8IGjUwYE_7vI#M#(hQqTGhWcAH7F=@6v5|=(dT3+Wx}jcD1vo z*xqyd$A#^`EzoU6dZ$Y7EYLe=TbE6Cs86q&-dt>5r?#%kzdGv;jQ>t;TRF9-=v}RP zSLb_6Klyb&`n7L`>g!T{Yl`&CrPV)OKy;|Smx^?kzWil%`7duKi_81e<$Z-^jkQR( z=lc#1&(ci=@5ppJbWm^ing7BYcmeQ^6lq?i`2x+C9If++vhmcc(_d&w-G1h7&yG7i zJBmF6YR>>5%vyAgsLqi>wa!}!hskGN$@3%x7vFpaxwDKU<4{gc^5zX{H4&^J;t)Kghv1r+ zzWa=MCkU$MK8 nsp;qD5&1u%f4-ICscC*5k^jGz{`&`o*9X-oYmUFaH+JHGFyt8j literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape_form_data.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape_form_data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89c21b4fc162dde96131a1b34a12b021d5a2011f GIT binary patch literal 4090 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 NyoJ)%KR&AV`7a8E?M8#~yg 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 HclrMSY8Y6! literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape_process_info.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape_process_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2204108869e13a40af4739eb378c974fefc3147 GIT binary patch literal 3859 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 literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_scrape_task.cpython-311.pyc b/dock/dcm/__pycache__/dcm_scrape_task.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c59b7bc9fddbc686892eabdbd85efb51eec281b GIT binary patch literal 4082 zcmaJ^Yj6|S72cIrFF$12$OhUHL?9T^S~f{3gc4JTV@TQ>IYYtkx78>P@yftX^+BBNs3xb8-LX9@@MZscku|}JDd$7b?LQ^*F$1UKB ziG#C4DS_gEQc6m>GE&CPBc<(j&e?17&g067nX4dFmovLqVp*6asdk2&-^+N*Il6^% zFPuhxliQ#aHEiGByp2dpHN{!YQi3my2q z&L7;)N_(bXRNAO%8dOgI3#0O2_lwP{_sJ4_V25aPRi94oWFcAo7}3M9gc{!SkZ_giomnS}JGP@eCG z>JJo+heP*RqEwWQ8luK16E$&$bLJjv)V$vmHAk%+vmPX6Ty-XO<>;s-YQ1Ejs4GzP z^eG(NUyM7bLE|9?3}}k{8$K1dAWfaPqxOH{s(W{fYvSHL_1H!A$l%n)(e&vX>Aq_} z#SRo`Qq!?BQx6Winzy!WcTHWtlfHRC9r&9zX!1;N-nbKAsr_H3`v)e*MnH|)cUsr< zZ2Y)BQ&#xU^u_O9PNJ>k@HgtQ(e$lR_25_Ob4SyskAQC1yMnMMK%R{sQ7;{EZEkt5 zrKNFeQ)At;@uO3RZ)KJ;avRjEKYTy+^_S@j15*!9sb??X*v&0ln_TM2K6UI=ZYLYX z4r!gXC!BNk3AW>H2V;7!{ zpZQHqP0i}HIhC{BB&$-re^Nc#2Roh|-j6H6sWHuVqO5HCdOwEW zyWXs+nXMtu@p39oe6Ei5t5*lp*S^bNdtX}n1eHx#m;sFfg-d+9q6eC2xtO$&@iAW5_B6x6I@WGBt-TiQu#!pgO^RT17AeA79CiX zVpWFKJgg8S6$_4th`dsi8RlyfxNgY?-!$*S>cuKuE_f!K`V)03Skqr|c?$#6TRvcgst&?4vxCGSW;-ogHrr;N&W|mPU z@5FxpwBq&D{G}=^SeK}!3zDTP zM(5x7Ribo5vUEed@RiJLcn-5ridD>EyhvoR+XrTf5PuHeKqiNv4ZzhAG)i^hXe|xr z&@s!w_7apUIYy5`#^cDOE@PI88ZN=;D>)54Mj4HqNnN=yQ6op^<8BWl7iZ-tsw;>1 zQD%-tK=)os#-q-puG|d6S~$=0vu*y$>*nsn95oF?@P9Q$&EFd5%x#HUhKpX6gy{0K zm)tHqb1mg@vkwYtAAI8&&h)MM64>oZ&UQ2GR1b~n_(mOeDXma#3z+6NZO?X|H71^LG>$dAMVMx9H!_>`-`4TyI zUgw%V+&6Xo^z_i*AqZ1?`K1 z^~m)L!Eo)qbdJU==6|ZR3^W_brYi`sJd@{5* z#}Z!$5qygI5u`#=({}6xLSdfulgQ8EWBSN|dJODwR=qbj@J7)2(&sN?7&?19{lyIk z9ueF=@H~+vK+KNRvu!(Z_bR+FP#l5OHF5Ww%(0#uoxFM!BD=>uPhk*E!63vha71Cc zBq5}jc!A|4(F~)MLWIV)Zg>I8or)n0&pr*};n<02MS&tM5^Q0^;XtTEF$H82loT_( zAwfd1h(z0@Vh)0OUQ%rD1;cW8W2mlPDb+~OM&v+{XsiN>{e<}1SlQnxB8n)ckEOsq z;w}*Jj)=>URh;!UEe_1;qT?tw?IEj?aN2NsT)R@zE{Jo_`XUn|@+-#7qBtqxJFX`bVql6RR7N zs~do!=Y-XlwEE)NJY&QW`Z)boSVRl~BHCO;C>IfmuT<7V(BCRt)+c2>AK~q#(>nGVw&@0IxfD zna=PFBJse#r4I05-S0|tcg3L^2*t%4eiK+`G89@Bm)sxYSgQq;V7Hm9iKnD%>0t&RV${FM4dJU^%M zyYMwdRmSskidq`a&ogGqPq)*l-6U1DYR2>${W~}vtVieR?ZXzmjqmd5L~3Q#j1eVL x8#ZN#KYgSVPb*i>R-wyh>P@th-c1ijXDDnRR{pap-rSnpv`g>*|5`KYe*uBgOoRXc literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_security.cpython-311.pyc b/dock/dcm/__pycache__/dcm_security.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..562ce4bdec0893a9776519d67ffd91c0e6890e96 GIT binary patch literal 5567 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 literal 0 HcmV?d00001 diff --git a/dock/dcm/__pycache__/dcm_send_sms.cpython-311.pyc b/dock/dcm/__pycache__/dcm_send_sms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca009fbf7bcd116c66d9cd2be012cc7281db6b0f GIT binary patch literal 2617 zcma)8VQf=X6uz(TwS8;XbsNj5I5_E$Pa({>(w-|c}Wuz_S)^2Gw2_FdG32{Z8v{#-hHR{ z-gEA`_nz~e^LowUupywXeRaXC8KFmHP#_rV^mq_{CJ>K!DujIG*A$|{CZ8!x`)JCT zXM7CI(;;)%;8!x0t!0vJ0XF7rqD$P z8NOnW@m29Q&~@_E`>4sjNTlseCbe=$XXl;{L5hkINl@*^=;N3m$xW1M<+-pw5bcF7 zD{wucKO}aOJ{EKhiKB{}AjCj_0N%&jfbBvQxhZvol1p@g#eP54kETor&A@BW0C?k5 zx(iLv$Cx9geiNLMj{OEQn;S?Q{#v^?r;VOoOnjD`ygvWS<=j_;3$wSgw~lJFcNcD5 zev~+x{o-!+$FuYIh79FW>7PHJ)b9S2J^#IS`MIlx0uc?}p1*%4H-2m3 z=72UgW!TZK4(7&=fmxe=fYxXL2g2?HA|G#c8z<04k7m!_)UHoxLw5`vEXXk&X?2%) z(QO=0mw&R#+arbruN0QNk`Un!AA}wR0TY0z&WLcBINP81ZWFCCWINSLCSy3H+CjY_ zm1tN4j(O7R?Gi(=upoK&iTIFr4;F(0CrjQvak)o~c(-ld>E%W4kQYKpUO(B`4~9I^ zxLVgO$i<0*Wn2Z8?eI!1z!u<1prtD0Y)rh9alMdod2d?p)FoY8Q?9K_?edn`mA2O_ zO#Ne6iU?2&zyjCcxF28>6S!i`L8pis83qPwN)gkK{=>|r(sjhXPE){V@_mG+0AVvl zlu@YE7Edjg@&?M{uv`z2fs1@lCr5HOE^A{W05k&&x#`cfxzXi_C|t4Ct(`fg&5f7D zJCB)?tGBurkDpqYJ-c{rQX8Jo_l(uT?xn!a4v%RkCfl26Yyk--Wkt2{0w?kUK&6_@ z)c})-H{5PaQeLeO9FzsTGL&^7TuqeU0A_^ppDJW_4DUbr;UA8*zdP0@9qUt$^+~2N z#WX5RV}_|zV3JvvV%8~zhMlmwEZ51&^jz-&U$2bXN>9rKu6csYFN;!H=2LQ^T7q`4 zxT-wLq&{3C$XP-C%VJz=8_+;tou?qVgHGkPdAbiAT!tH_{JcM(#>C~*!0gB0WS&htM3-uDe2x{c!(nDbRT&jdz zQXDTQndTJJtT4?P*M{lNq-#^kwMnU6-V(bq)vL!Hr=8=@#Ll#B)#%0(8_(I3w#Jl= zq~6MwvD#DCXOq^3l(j)|zoJ-4%dpi-&1*@vCB?QVY)hK08n$S*TCkC3Yc6!Ap5J`` z)zq82GxZym%(UI|7XtP-VM~ZMKQ+@+zNs1$RH(JGh$8`B^z_JblneC=5gC)r#00E( zEm2{K=RNR!F}`KB(SRfi9!|u92mUj3_eQ$)NUIfw;IME&4~6Gcq!MU}9oq`M`ie6tMth0WHotssI20 literal 0 HcmV?d00001 diff --git a/dock/govc/__pycache__/govc_api.cpython-311.pyc b/dock/govc/__pycache__/govc_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c616c3592b73cb578dbd07c9e8e913bbd11426f5 GIT binary patch literal 2507 zcmZ`*Z)_Ar6ra7_+x!2Ao>)*$E@EsedS^?mSkR_cRBWLYT8xddIc}%zmAl(Iy9c!1 zB`ctU5ouJU(V`^*L`eiA(TXMb#l(+y<`OTPkdWBBwx762_`)aO?B2E48s=u+?7VsJ z&6}Cudvo8qTy_NIkAK$<=Pd~RLkE@ODu3KDF$kSTBI-vXBbs7N+|&;&_-12lKMOq< zOlUUe^8MD~ISG@u!UJbqtU z>;GiEu)1kj-xbvsZOc~&AMKbOc9cyc(IK(}oan?((S=>28+s3Ri(c#zS7FzHQ>-~^ z?)Qr9Zq(pQJ_cI$!t~+BrcKSy6fXZ-{N#L~y=Qx%@XfWkvufd^Z|09&|3^J+U!c|- z7|qizk0v6o_V)I4#G*K%>;xVkkrD&Z!Ljv2iZap~3N;0rTY`;E!NzA>S~oxaTw_Qc z)6I{0L@BaAI4B*61V@N;XjHT7oiHTHO67Y*BFflCMIwo?CRfg_M;Fwo*=col`gpSyDri?5+>nzpnNs02DB!x6wau( zXFi_&?R@cz$>N!*!i-utcXjT|qk+C%0KLlm!kMoNKYUy~brslM)#j=%xHVv`I(z*y zFj+Y9Q}MW3oIGB3<>cYwsVmj01{x|##gpT+*UlBc__Z)|vRbN)6%hOR{o?7Lp)Q=d zQ7urxP-~z#ak(&lVfOlsxz8`opS@T(aq9NWv6lf}w`YzUhK)t4wad6`4ZyC!zv77> z!1dy!x^QV?c4ngR{;|1h-x~4+D@l@Q4bXtwWxzU6K3{qRgkljQ&_!cPkTHZyR2ki= zS+@1`c6V;uIp$p%i(|F-3DU9V`%F}#H?%K4a8M^PX)u}y>9m2@_zp?hAH{^iMdMzj zq0D!aD2Qm5U3dT!OvdbeGA2*84?@BK4VFeJN28O_Xd^Up5hqo5;X?`u3+40q4mH>` z+hWcj9xCD(#EKBnmy|V*PHL+ui-Z%R6c^xPh!Ca=Y2GDC;XpW+#F{sO59--OC?iPY z+ee~(;Ekh)=A#Zhk2Sc7m3l-|`3eUKeLxI97(sIxZj{~C>{V+!_7UkIb>#pinnjX@ zL^zH~E!}{nytYgukY3_ax%X^nj}%MBu^f6sBKt$&)G&@Ha;Rq%a#$kt(x&Ypy5kVN zZk-4aS;3J}%|mxaqpWP4)PwQ0(BwHF^YBwqX&o}xXSllRn)g5Px4-2te@oWCIp^P; zsa;vK+yiR6n!d}U`X_UB?b*7HTwO<&drW<8+%fUSt(x^WeYH7X{YC$@E$e$R=X){l zeKcRQG4I=+e=tyTSyuBU1Vr6Y@*=AzV|ggQrg`G|%$nwluK+pb0>d6(&b^@ov;0lZ z>Wnimeh>)MylZXVvpVms&97RUe{e&|1roa0T{m*pmOPcA`bwz=IeqHRJFr#Muu8KC z0-TZ{Xf>5*Wa$MVkYN#d7zjK#VOhb!hy)0w`IxNNxdv+JgGK_B!2(0G8Bbi2V9oQ= zwwK%cx_X7)?K`%0_w{P5tPntAG@@wcftVClD7K}Teg$L=HQYc+9gut0t#L_A#_$W| z85q((5b`RZC6-~BlF7tyB}9o8F`*0!R@Ym|q3XXp@@JOUTec@u-q5xqPvF=<&b>Yp zXw15sa_%P8Va)J|^HAeEnDH&A+dgfZ+Ir=Itgj{KYcaUfTW+*nYx{F+=FPXWTlVI* z>^0c8IKSGR bool: + """ + 验证验证码是否符合要求。 + """ + return bool(re.fullmatch(r'[A-Za-z0-9]{4}', code)) + + while not validate_captcha(verify_code): + base64_str = await fetch_captcha() + img_data = base64_str.split(',')[1] + img_data += '=' * (-len(img_data) % 4) + img_bytes = base64.b64decode(img_data) + # 利用 PIL 预处理图像 + img_bytes = enhance_captcha(img_bytes) + ocr = ddddocr.DdddOcr(show_ad=False) + verify_code = ocr.classification(img_bytes) + echo_log(verify_code) + + return verify_code + + +async def fetch_public_key(): + sm2_pub_key: Optional[str] = None + + def after_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + nonlocal sm2_pub_key + response_body = response.body.decode() + response_data = json.loads(response_body) + sm2_pub_key = udict.get_by_path(response_data, 'custom.sm2PubKey') + + pub_key_url = '/rest/loginaction/autoLoad?isCommondto=true' + body_data = { + "commonDto": json.dumps([]) # 空数组,表示没有额外数据 + } + public_key_request = dock.new_http_request( + f"{govc_api.ApiUrl}{pub_key_url}", body_data, **govc_api.ProxyConfig + ) + request_queue = asyncio.Queue() + await request_queue.put(public_key_request) + await requests.async_concurrency( + request_queue, con_count=1, retry=dock.MAX_RETRY_COUNT, + after_request=after_request + ) + + return sm2_pub_key + + +def js_escape(s: str) -> str: + """模拟 JavaScript escape""" + result = [] + for ch in s: + code = ord(ch) + if (65 <= code <= 90) or (97 <= code <= 122) or (48 <= code <= 57): + result.append(ch) + elif code == 32: + result.append("%20") + elif code in [42, 45, 46, 47, 64, 95]: + result.append(ch) + elif code <= 0xFF: + result.append(f"%{code:02X}") + else: + result.append(f"%u{code:04X}") + return ''.join(result) + + +def unicode_escape_to_utf8(escaped: str) -> str: + """将 %uXXXX 转换成 UTF-8 的 %XX 形式""" + import re + def repl(m): + code = int(m.group(1), 16) + utf8_bytes = code.to_bytes((code.bit_length() + 7) // 8, 'big') + return ''.join(f'%{b:02X}' for b in utf8_bytes) + return re.sub(r'%u([0-9A-Fa-f]{4})', repl, escaped) + + +def sm2_encrypt(plain_text: str, public_key_hex: str) -> str: + escaped = js_escape(plain_text) + encoded = unicode_escape_to_utf8(escaped) + + # SM2 加密(模拟前端 sm2Encrypt 内部逻辑) + # 前端:CryptoJS.enc.Utf8.parse(encoded) → Base64.stringify → CryptoJS.enc.Utf8.parse → SM2 加密 + utf8_bytes = encoded.encode('utf-8') + base64_str = base64.b64encode(utf8_bytes).decode('ascii') + # 再次做 UTF-8 编码作为加密输入 + final_bytes = base64_str.encode('utf-8') + + # C1C3C2 加密 + sm2_crypt = sm2.CryptSM2(public_key=public_key_hex, private_key="") + encrypted = sm2_crypt.encrypt(final_bytes) + return '04' + encrypted.hex() + + +async def build_login_common_dto( + username: str, + password: str +) -> tuple[str, str]: + """ + 构造登录请求的 commonDto 参数(符合服务器要求) + + Args: + username: 用户名 + password: 密码 + + Returns: + (commonDto, cmdParams) 元组 + """ + # 获取公钥 + pub_key = await fetch_public_key() + if not pub_key: + raise Exception("获取 SM2 公钥失败") + + # 使用 SM2 加密 + encrypted_username = sm2_encrypt(username, pub_key) + encrypted_password = sm2_encrypt(password, pub_key) + + # # 读取验证码 + # verify_code = await read_verify_code() + + # 构造 commonDto + common_dto_data = [ + { + "id": "_common_hidden_viewdata", + "type": "hidden", + "value": "" + } + ] + + # 构造 cmdParams + # 格式: [加密用户名, 加密密码, loginType, false, verifyCodeRandom] + cmd_params = [ + encrypted_username, # 加密后的用户名 + encrypted_password, # 加密后的密码 + "0", # 固定值 + False, # 固定值 + f"#undefined #verifyCode", # 验证码随机串 + ] + + # 转为 JSON 字符串并将双引号替换为单引号 + common_dto_str = json.dumps(common_dto_data, separators=(',', ':')) + cmd_params_str = json.dumps(cmd_params, separators=(',', ':')) + + return common_dto_str, cmd_params_str + + +async def login(): + """ + 登录政务服务 12345 系统并获取认证 Token。 + + 流程: + 1. 从市12345平台获取公钥。 + 2. 模拟前端的编码和加密过程。 + 3. 提交请求完成登陆。 + + Args: + 无参数。 + + Returns: + tuple: 包含两个元素的元组: + - dict: DCM 接口返回的完整 JSON 响应数据 + + Raises: + AssertionError: 登录失败(`resultInfo.success` 为 False) + ValueError: 响应体非合法 JSON + HTTPError: 网络请求失败(由 `async_request` 抛出) + """ + login_url = f"{govc_api.ApiUrl}/rest/bmfw/bmfwlogin/loginaction/login?isCommondto=true" + + # 构建扩展头 + user_agent, browser_ver, os_name = dock.get_random_user_agent() + + extra_headers = { + 'Host': '2.46.12.176:8091', + 'Referer': 'http://2.46.12.176:8091/sz12345/bmfw/bmfwlogin/login', + 'User-Agent': user_agent, + 'X-Requested-With': 'XMLHttpRequest', + } + + # 构造 commonDto + common_dto, cmd_params = await build_login_common_dto( + config.get_config("dock.govc.account.username"), + config.get_config("dock.govc.account.password"), + ) + + # 构造请求 + request_body = { + "commonDto": common_dto, + "cmdParams": cmd_params, + } + + # 构造请求对象 + request = dock.new_http_request( + url=login_url, + body=request_body, + method='POST', + timeout=dock.DEFAULT_TIMEOUT, + use_form=True, + extra_headers=extra_headers, + **govc_api.ProxyConfig + ) + + async def after_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + cookies_data = dock.get_cookies(response) + cookies = "; ".join([f"{k}={v}" for k, v in cookies_data.items()]) + await first_login(cookies) + + queue = asyncio.Queue() + await queue.put(request) + await requests.async_concurrency( + queue, con_count=1, retry=dock.MAX_RETRY_COUNT, + after_request=after_request + ) + + +async def first_login(cookies: str): + grace_url = f"{govc_api.ApiUrl}/rest/szbmfw/szdesktop/szdeptindexaction/getIsFirstLogin?isCommondto=true" + + # 构建扩展头 + user_agent, browser_ver, os_name = dock.get_random_user_agent() + + extra_headers = { + 'Cookie': cookies, + 'Host': '2.46.12.176:8091', + 'Referer': 'http://2.46.12.176:8091/sz12345/bmfw/bmfwlogin/login', + 'User-Agent': user_agent, + 'X-Requested-With': 'XMLHttpRequest', + } + + async def after_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + # 读取 epoint_user_loginid + response_body = response.body.decode() + response_data = json.loads(response_body) + controls: list[dict] = response_data.get('controls', []) + epoint_user_loginid = json.loads(controls[0].get('value', '')).get('epoint_user_loginid', '') + # 读取并组合 cookies + cookies_data = dock.get_cookies(response) + full_cookies = "; ".join([f"{k}={v}" for k, v in cookies_data.items()]) + full_cookies = f"{full_cookies}; {cookies}" + # 组合 token + token = json.dumps( + { + 'epoint_user_loginid': epoint_user_loginid, + 'cookies': full_cookies, + }, + separators=(',', ':') + ) + await TokenModel.refresh(platform='GOVC', token=token) + echo_log(f"成功刷新市12345登录令牌.") + + grace_request = dock.new_http_request( + grace_url, {}, 'GET', + extra_headers=extra_headers, + **govc_api.ProxyConfig + ) + request_queue = asyncio.Queue() + await request_queue.put(grace_request) + await requests.async_concurrency( + request_queue, con_count=1, retry=dock.MAX_RETRY_COUNT, + after_request=after_request + ) + + +async def get_cookies(platform: str = 'GOVC'): + """ + 取得可用 Cookies。 + + :param platform: 要查询的平台,默认是:GOVC,市12345 + :return: epoint_user_loginid, cookies + """ + _token_str = await TokenModel.find_by_platform(platform) + _token = json.loads(_token_str.token) + return _token.get('epoint_user_loginid', ''), _token.get('cookies', '') + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(login()) diff --git a/dock/govs/__init__.py b/dock/govs/__init__.py new file mode 100644 index 0000000..9539358 --- /dev/null +++ b/dock/govs/__init__.py @@ -0,0 +1,3 @@ +""" +省12345对接模块。 +""" \ No newline at end of file diff --git a/dock/govs/__pycache__/__init__.cpython-311.pyc b/dock/govs/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9972bce08b8988f01e7c1d72377e0e9b05f555af GIT binary patch literal 214 zcmZ3^%ge<81Wz(}vgCpEV-N=h7@>^MASKfoQW&BbQW%37G?_{zfdX95=QJ7`8Jn0s zUBB~L-_mC*7CxOj{c%GRm!Br%E%x~Ml>FrQ_>~NwL5BP?*AL6jDa}nS)(_7w%GM7k z%FjwoE-BUzs4U6I&(n7?_S6Te&(=@RFDurMkI&4@EQycTE2#X%VUwGmQks)$SHuQ1 c2jrq+kmVnk85tQrFflQ*d|*H!idcZM0PnjxU;qFB literal 0 HcmV?d00001 diff --git a/dock/govs/__pycache__/govs_api.cpython-311.pyc b/dock/govs/__pycache__/govs_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89ee279c94ece9ef0436e4bb400d9857c4d916ab GIT binary patch literal 2335 zcmZ`)X>1cm6rT02*LNHeuFz84LJKCvZo*X(Xq1MdlmICtphhh#1L~TQ&l0cBSS|kn?QJ_KeXMZ}CrCKXhibU)rzp#Y(!>_*CwS%GR?97{)H}9Bv z^Ud39E|(oaxw&sa=W+`|Pw1pjT=~H>6NAtI5>YD>8PODGBBoYg!I%xRtt|9hn2VTO z&A>Ca@-}2b*xU)$j3V+^LH`CcqqJIW2wTZ|Y#p%`c*VVF5!tII(12zX^4|Z-TK^+k z3f@hf`l+b4XnWN=?9q~7>Ok1qi&`{dA(s_K=iR%dQK$ey|4sc+iq$y|Imc}dNDcX8_2qbKT7`!u!gWi)ra z+!YOe(%jtC5Dwv}vIBU0P>QyN+Pf<{6eU*U_gAg0_EoR(RjsS`Rei9^ABcsz-J%pc z;A@u-%Dxzp4tHsGy%RblSt*QT5>dJ<6^TRxqT~z4Nm!r;gSsDI1I%CZt#M5|cQDOX{1V`g>?iR{#>Vm!g3xBn?!TMD)K{Yk0W6|!iP}8afL~kuP{B1SL~S!_C%}VEQ}F4 zj}p*^O$jDpO0Wqo;SeoDyi)Q~;xcoF>2W5U003M3TR^eh10{Rm__R7QabKOd|BDWR zsawBg?;Hh8Ox?L;*F|b#0TPIK^5L#AfHHJDGps%xJvH(BmF&5p?C?ltRLxwzJNeUb zPs=UQ}v1zgm4vV*rWeOD(QJ)ZpW`qZWC znbZAGM|(E|WS)+mGz=R~ina4ts`0?7VSm}vH(~49A$9u3;Kb-)=0xx0!)t~-!AhJ& zYdrM%>@uM0$;X%80HOGT_vE7QM36B&fsoR*qucT^4iHQ{-HQUTSU406C?P58@06uz zttZ$KATm}qv@~yC@u8R1T=j7PJqdN|Lfy^=DXPFTUD4bX!`=248Iu+D?ZEKz8cUxH zN1qCz&?hv35DFyCg%2wvAmk7BGxAzB+sp$dZYpAH$BLjx2XIv5=#n;vGDsjQN)Z9# zNrV7(qm@SSA^jN(`D@nr`dFw1JUg0bB7prt>hQXVm3pVy^R>+onzId`X2YMo$h>1e zkq%LB2Qkqsk}O065lqVI4lQ|yGBrTMJ5ye)_3x3w@d%dvdnIzf4;$&kK}GgAbwTW+ z{*5cQ`stbc^d58~0OWnKF3n9(L*bvdP2K_9^P$NpAXD(EDEB5ZFH3Sui)*@U(cfj> zzskI0WtFM2%B1u4s&0DDqh;@>me!9gZAdL`7~__xpY}Nh_l{RAA6cEOSPnHke_eX6 zCuia2@HqrT-JY`}t2=2~keT^t!;5yF690I0vizKkhk6r>E}_5U6Qa zMcO?lT{^-xsZ33W)TDki6CfYg#`UF zHxtNUh|B{5sXQPn*cX%luQcD01v-~v!}v*%HzQqU!>M&M# z#JQ>Q?Mz?oK;8FsBkOO^9xGpyDqmx8@7F%Adsz49`sC+dj@9l<)$TLc}ZXNp^x zd~4en*O=lOlUyTgZyWO&ldklqD(jNF_9jD-9Q!U)oYYc-R=6_=Rl{SVmxQz+k{3YvTug#EN5CXa*4I<&k6uR&28EC=~L`yhXlZM=|8} z^a?76R?#cyl|9O$R80uVuo%{(rHoIj9VL{B&Zcr{qJLEioz~4$_NXZ{V3pEkRNjCR zV=xZTJm0(8cc0@vLE>U*cc=liJcCvwWdV8jD+&6({xvC$EQ%Aru{B|7@%%dvP$@Y zYx@-ZZ|#QU7lZAFA2Q~#K4A9{Ze(9 zIH!76-9?nEBT+!sl|h(&>7$vK|K;ZSC#tvDw$^;{$tN=}yfyp5g&R*jbK~sg8!!Ih zA0tod1jH{#UY-5xb7lMXb{#C6{otb;e|&Q0%quCno3B1CBYo+enU{ZexpMCDbq&9E=8Q9PK&dmOGZ05B$FzrKKdppZ! ze*5Cg$751unwj2Ey-+PIGu0<$8$w~GyuNJaKcAU-;SEv8j#ddx>aem69vBaR_soa0 zPmIm};oqQwUrc=hHBG~o`ZR@o^W8UZjDD2F7}{tRq%yRd_MD={!X9BnSYfCzgTm-b z4l@;>B_MZDJSi%HbE1F_s zadro&!H@>t;dUtdZkP3Nz#H+=A?uLHt)4I!qulbz^_RDExjRtq4tCk1sNQi4m=Qe5NRyhO!_1%)nlM7U zrQr`a%iu3ertX~7WJ;er(X{D9Dddx9Sb1Aa?p+~7wen?iI4cZokqZ5I3<)8cMQIjjONMh(Z-Jx4JxcZ} z7Vylbv`-UZ+|kLTqq_I97Rruh%OvB`FNA(P z1%xpF*_*&KVo{*TrwKXNrFm`#E(@@v*18MH*-J>yz%zqnbE-;rAz8PCM2Fc6sq0Tu=NVY8InL%>FVyq>{wqXg$8F*%pTzC(X zjY~+*z%zqnse*UW&!#0LXW%JHo+wV$GyNI1&zJgFrt&gqNeVgy87h}Dzn6CbR_#mD z%B>*6(zt=uR5Hq=+47YKjJDjA(RK{J0K?0M^|xKI^B1hxInkUm3TFH^>vqnfb=yEK zhjqI#r90^LL)UG?9k1KQs8J@lETu)peqNn=oBj;9&X?q^xHNB}zvUX-pW^ltXpn(- z1`XDHuLhaFU6w^-U0$mUJTpjMdk>NemXMr*rzrU+on=j_n#g9EQ9eG>XBo`QG+RSv z5fv_&U#6%D=2zQo=9ft>V>p*8g4u=rxMcXjvn%(GXO}suj+$k%Z#M#!3-H!U+Di75 z&}(vTWs9+-MU|Mi+u6D#?+hAbsLzk4K?dHU20!V@-IHol*~m4^N7aInyMLaBDvoMi zBd8ToHKZ$}N=QqhDo9tozTg|9fXxJEeK?)AG^&^=pXW87lUwG%t?YG`TqD?s$%Ty= z_?NGa3<9O#JExp|Vb4JD!)MOC1fKcK+fUy-^X$#ZU(bB}vgDc9!)}esSC(?0bj6jXq~GE@ARa*!Skhh6v^^r&OL}jC`|5`sPpGA; zvmMy$X@4lf&`wv#?eSbx^6LIjz|U)_h%XqrsNfZkg>VZdYfEnTx_sS~t8r+}_trIu zwF4+`i8Z}^^78Q6HDepc_fH;T%{I<#LuMOfK3_9J2sQn&tHe zXBF6q$*XaKJL6}mV*Afu#g3o9iujZLGw(}n7U)yz2%j-NLk3ST4`Q0?Hf_++E1BoZdV5z)D*yHC(0QH1DKKcht?JnSV zs|}J7;%fcpu50@bayt*9ov=9hSjQuAhb!iAaSk_fxB+r@-Pq>wldo9Eta0HWzV+e%PMYM*oZS18c^H#pi~M_*%>QRia#YqNod@wp-Qy2> zU2b~FB$_Fe!p1s2Tgv>A`F?{%FjvInK9D7|P?&W9xl_}G#?(pEq?l%fc=7Sc(^J}U z`$Vl+hV-9Zo3ewv&|tUPA%Rt7h&|AC?a;&AuA^wzQPy&jJ?@Mjr(?%y?szXc-V2bk zJ!9U9>Q@3|fw-kIW~pSYEu3X1vg~9b`c97gbIyRhEE<>rXj29+&%#(6Do&p!^M|#& zJ)wKD@PD)kGXxq+6J~%d#xZ@uBtb$YB6v6O#^{-u7r=daZRV4Y#goFXK7BQUosu%_ zDhVf&GatPJCyfCAPO%|^y$v|fw#hRi|AH<-{P|ucR}n z8|0Fj@!7b4MA9rV+rt}#Od6ww{g#sk073a_MTsgT%ntCl7X-lp=go^9QRXyMeIxus zCEC;(^tT4WRB+}oRIq-sm@{rg#;qd{P8%%?A*OJQcTJN8h^#{7)^AjbCi~y*4S%8&WG72@eyvj0YQG^MnNtxO!;1lAup>h~ zr*4=d347CLkE5pJsHq#(xZ^dRSdB-bvSTW{cBBK7R3Jk&r`|HBC93N`v!eQgsQzJ8 zbu?b(j8!=$l4?v+og~?S44XN11(0lSQ(SeTwtm#+M-72^LnPJ^k!ZGKn(av%8#2^# z>N=pQ-J!T@L_1EP9Tc+Dar;2bJ|NK~DbkDKm96alLokRq^)7M0$5(MhJ5bS%QNwlX1E2X&V<&1nfUI3{ zt0QK0Tzlj=XLTa0bJTEl7$zJl*u;^W6ZY1tkD|6?sO!2VYdx`APeOl^)t`j!sng-6)`h4C^L-#o?J`XUPl^ z+PPpQvaFxXVQafM%Ryv02$MI<@-?B?>{Lt_7N6HoIJv?aR9G{bb6v0zuC*LOE#2{! zzF12i*V2z#`oVDEiU&~f0JL-I=6I(%q_h!$m=@@rgbSAHWtQVf#vuahg|jPWS%o$pcp@k(gL})}!4x1C0I0^~G zE8E29=Z}d`_867S0K_Qi z0jfZpQ(+2`PB6^iBL-hKkKgG8&x~|=0?uF{;AQX!)GcjxKml)WJO%$-c!Nc}s3+fQ z!58-AXTY#`Po&Ov;Tj({=8_?*rWCM&0ub4Wu^a=oq?f=@EtCjnTXG)isA>@pT$5zz zm$0!{eklbe;69ROBE9y~8@O~R+t5j`BS@FdlVPg}KEGm!YQoYj9e`sKMhIc)&Iv)h zr;+O^=Zm+j%e9>guDS`G1Xn&a!hJA4M>Gaf7vproZ8SbBl(KSNp+Udf6W}%b@x?J- z6^w)#d=kj#3U8e5fZrWqz*=<=@@jkojyLXUe%N`S?cjl<&YgP?bsiLKi*juyHF$mc z&Of%Wgv+4fCAAQ43ekWO?G~+gqmFqN;4i~J^c5uHeYu&EipjpI>?_;2l0B$o4_mM> zj_RjL;~D*U|KtW(FX8*gk~R@CFu9E*YYTWYIowr#UPsIBAks%vX)_Uh`YEjHWMDqC&!Hv6`r zyhX$tZRP8jUqNGv-isEjYA{(DJk0SOVL#;xF?(@=#1?fV?D2|wi{@~E@w=!%WnVZP zbbCE;7ns5Gguy+>Jc$z*CdXg`y#i4;t?<1$h-}xdO!4|W!<-S}mmpTL z@|YmDv+|fAwzBe=AgWk-Oc3i?c}x&1S$SM6YfhV|XjV*g%@O!{`OuW@ifXFv&rY$- z?mx^Paj?gn>=7s0e?kOiNH-r>D0&nV(K$lMTv`9O3U=QkV2dRInIA1GLaiSiim8fN zRZ&8HKP!)OTD?X$9+~Kxyno{1^N-JgJ))bcxsrFqeWmczCZWh4ImSMEg6*c+M`?7V zM}Q?t*GMEPAQ6CR$tuFQZnO^>)^djR$gqB7S3Xgiy?j@|G8+jNjUJizI_ XNblv;KBV@ssW?|osEa{YVdnoYB1B5k literal 0 HcmV?d00001 diff --git a/dock/govs/__pycache__/govs_scrape_order_master.cpython-311.pyc b/dock/govs/__pycache__/govs_scrape_order_master.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af58e90c17cb869765af57650213310e9121b666 GIT binary patch literal 7559 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 literal 0 HcmV?d00001 diff --git a/dock/govs/__pycache__/govs_scrape_order_process.cpython-311.pyc b/dock/govs/__pycache__/govs_scrape_order_process.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b61b8de9ac4b814fb4877fc24517cb44d038b1fc GIT binary patch literal 12159 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 literal 0 HcmV?d00001 diff --git a/dock/govs/__pycache__/govs_security.cpython-311.pyc b/dock/govs/__pycache__/govs_security.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2a6d531374c6c2a67650fd9188a7e9e47f87014 GIT binary patch literal 4978 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 literal 0 HcmV?d00001 diff --git a/dock/govs/govs_api.py b/dock/govs/govs_api.py new file mode 100644 index 0000000..21d3bb3 --- /dev/null +++ b/dock/govs/govs_api.py @@ -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 \ No newline at end of file diff --git a/dock/govs/govs_create_order_delay.py b/dock/govs/govs_create_order_delay.py new file mode 100644 index 0000000..e781830 --- /dev/null +++ b/dock/govs/govs_create_order_delay.py @@ -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) diff --git a/dock/govs/govs_create_order_return.py b/dock/govs/govs_create_order_return.py new file mode 100644 index 0000000..d01c3cf --- /dev/null +++ b/dock/govs/govs_create_order_return.py @@ -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()) diff --git a/dock/govs/govs_create_reply.py b/dock/govs/govs_create_reply.py new file mode 100644 index 0000000..4d45453 --- /dev/null +++ b/dock/govs/govs_create_reply.py @@ -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()) diff --git a/dock/govs/govs_download_file.py b/dock/govs/govs_download_file.py new file mode 100644 index 0000000..1c5794e --- /dev/null +++ b/dock/govs/govs_download_file.py @@ -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, {}) diff --git a/dock/govs/govs_phase_wise_completion.py b/dock/govs/govs_phase_wise_completion.py new file mode 100644 index 0000000..af11dff --- /dev/null +++ b/dock/govs/govs_phase_wise_completion.py @@ -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) diff --git a/dock/govs/govs_save_sign.py b/dock/govs/govs_save_sign.py new file mode 100644 index 0000000..7d739d9 --- /dev/null +++ b/dock/govs/govs_save_sign.py @@ -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) diff --git a/dock/govs/govs_scrape.py b/dock/govs/govs_scrape.py new file mode 100644 index 0000000..5274ef7 --- /dev/null +++ b/dock/govs/govs_scrape.py @@ -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)) diff --git a/dock/govs/govs_scrape_order_detail.py b/dock/govs/govs_scrape_order_detail.py new file mode 100644 index 0000000..fa63bca --- /dev/null +++ b/dock/govs/govs_scrape_order_detail.py @@ -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')) diff --git a/dock/govs/govs_scrape_order_master.py b/dock/govs/govs_scrape_order_master.py new file mode 100644 index 0000000..9c15c44 --- /dev/null +++ b/dock/govs/govs_scrape_order_master.py @@ -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()) diff --git a/dock/govs/govs_scrape_order_process.py b/dock/govs/govs_scrape_order_process.py new file mode 100644 index 0000000..b915890 --- /dev/null +++ b/dock/govs/govs_scrape_order_process.py @@ -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', + )) diff --git a/dock/govs/govs_security.py b/dock/govs/govs_security.py new file mode 100644 index 0000000..ad54fcb --- /dev/null +++ b/dock/govs/govs_security.py @@ -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()) \ No newline at end of file diff --git a/dock/govs/govs_upload_file.py b/dock/govs/govs_upload_file.py new file mode 100644 index 0000000..f057fa5 --- /dev/null +++ b/dock/govs/govs_upload_file.py @@ -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) diff --git a/dock/oa/__init__.py b/dock/oa/__init__.py new file mode 100644 index 0000000..a459062 --- /dev/null +++ b/dock/oa/__init__.py @@ -0,0 +1,12 @@ +""" +OA 系统对接模块。 +""" + +class PushException(Exception): + """ + 推送异常,用于发给OA系统。 + """ + def __init__(self, message, flow_token=None, return_code=None): + super().__init__(message) + self.flow_token = flow_token + self.return_code = return_code \ No newline at end of file diff --git a/dock/oa/__pycache__/__init__.cpython-311.pyc b/dock/oa/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b588729fe3b21eeedb1fe18f5769324f0efe067 GIT binary patch literal 936 zcmb7CL2DC16rS1LWH)I;@t}r!X`obc$qM34#G+DqNdtmHE<@Oz*fg7LoZZD%a)_Z| zgb)>!=t0zjF_MD^FV*x%R4Eu1Jk=z4G7=A^C*LI5py%mX{?Lefh2gFIO&7q zeQqe>SnF3dHCnJUX5KjiJA(^gy;}2I=}vdI+}+vvzFX}-YJUAt>)wCZZ$DjI9&_RS zd1+jec=w5M68ssa0UqNzvSALMHNa_k0ebOUX1a)qxB=^}X%3^JP((!VDvBaRo>Z73 z$iVTaH}BJkd;@f@;a4M%tBg)Ur}HD3jrA zrU1ABLNp^~S%g^pb8&g4qjTyt({P|zy>7CsnqcMz)oe>mY}xB(UcG#FRn<)`tC}g* zYaT0XF+Ut|`@|pfjd}S1C7)yr0QJf z)KW#-SHgRvdkA|_REWo;el9hi%26(rkesPI2KQ$Px#^^gNk%3FxDhg9m1^Z@+_u#k6hJSwtd5V(QqZ#U}r w290Ytk0a&$SkDD^970aex&$x~F~&dxOF#oakulKt8y>9w;Hy9Xks3Am1uB~CV*mgE literal 0 HcmV?d00001 diff --git a/dock/oa/__pycache__/oa_api.cpython-311.pyc b/dock/oa/__pycache__/oa_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..841d735e357a2618c919647b8c432ad25ef58213 GIT binary patch literal 3452 zcmb7G>u(cR7Qf?}@%S;0A9?U9tP?|1r;u#KW2J0a6dvjF2&6O=u&Nyy&m=h59`Bto zAz4Rqf$kAUoF^54b+w9RNLyS4#KYfR6cw|l!ZuE?PASo(cTQdHrPEDmYLu%sTAMU7K~ zVM&gunklA8fr4br2jhII)A`|E0CXt<@N33!7J!25X zt(U1ddzl)snvq%pD2P$*C0Pg!kTvKjPF34d-J(#$PMd*LcK8PapG5R1H zq^ed`hPT?VAnO8(1E8l3aVl+FZzFu12jFnAKf%UZ+>mk~o}5oy zoV%5nyY;D#pU2m~%6)Mh0QLBbOBS7_BkITgu)BEl9R-w4-N;TSp3I(_``6{%`KjFW z#q4Y%`|-_3e?95mw--=Y8lRv3FnjG(?%Yl2-c>2C53`(L61l0d zqA6#N=g!@zoLWFzX;SXY_}uM}bLYR#&Yr2{Dq<1XzI;D7@g?N>bKg`llrY-i&P`s= zj?c{9`R38tD~~T-$(}ytRT3cq;hr<*Bx$HHQ1LseGN>{O(o{9PR`9v zX5Sxubo-M6KbF)Oj&!(x+uPf-x8OF!VafiI6Tk%P;FVU{FDOzVhJ)(R?qP>N8Vv;l zel;jZ+6ELk;-!cAEpkMaBI=snp{S&>y~IU_E&CJ+uj%ZEey>rZ!Fx4^I3*?k#l#aZ z0gGmnj;Yu$6wUVX8dvmvOc1V_`z2M-onEu)f_uuYPx!QYGLHQbQ4R~RaaiyZjhG>G z8DgKtim`B1(P%}*nj<0&3WrrSDin`dGj>LU`*6r>!vu_&Bc$3wOhR&uNWE$Lg zj;S~15s*Z8-hxc_B)2rPc-`d2BFP&@u^%Gs25Hl<9Sw8@h!x&h zmIonnmg^T`{P{EkT?-fVi*Ew)XEApKRo)Hj&4FGb|~aC(^?JuK*n|5Z-Fe1+Ed$OTHQK>8s^C>;&{ISQBx2P&=yZQZ3d-!w8)-0pZ)hl_Ra^niOacvzL&dkGkfMMa?cac zca%a+c_jP!#G}t<=5GC6A0IZZI=CvlO58n9c3z`@xnA_Lno0Dl5{Z@?9gzn$6F6|6 z-elp80IqKo0{*~Z$!pNSFGc^5#s%aU1R|`E6+!@OB*GdX9!MA@K@h`1e@KYL!iOY` z*TCRT_>~sWS#X{oEyz}%=9k~+8}IRrDc+OjJ@@(5_xROU4y5=EX?{au+XH)D9#fd_Rz4W||Zi?AcxFSDP*Lw-(FhI_}#=6j1MC zjS~cjS%LtMF=Z$c2+H_XV8x4oXbjvM3a%$rOVqEZQfolQ5{9=9(p(E2t?>5i4@UY6 z;oCw&d*N}Y)sVVBpazdh0=xYG_ZC#!FcJ&Iq+q<{z+1HB+>NQ3T22_ph z3(0e^xe=Pgds)YED*X5NTO9+K@IcO)lG>Y}t5a zWx8c!s%2BUWfSSpbIQCUZQhYE77VcPF066`~>6>Ma9%9BUW6Z~t~)c*omC&8uw literal 0 HcmV?d00001 diff --git a/dock/oa/__pycache__/oa_api_request.cpython-311.pyc b/dock/oa/__pycache__/oa_api_request.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e816f9c6dca25f30ec36e1628a05ebb57a6fa535 GIT binary patch literal 18880 zcmdr!X>b!~maW6KYz)RW#)NQ)BLOFXLjXg}6=ElZY&bStJ7i~7vbt>xB(=A@6#{Fv z1ejn>JHf+gz%YQFaM)}JF_JAix++yuHSU)DE1qIfH8r)hweNkOy45Yo zPMiWJGmZSI)&0HqUGMtddmrtiN9QnbeSdTO_TR2zm_OhT?aGSZJeg-^LaLvqbDRQzcMr}k{zGW5dc0#Gw0Gc4 zLx0cR{>PWpy`4D&I5X=?EON%?fR_)zO(ySlx$C@e=iofGyvxVez`Z@__1DPRtjF(? z+~N+=1FNTBcDUOQe^2V6_y@*<>uT9!kqmXV>-X27&UN^s*M5ez{+n$JbISIKeUG)? zTFOP2KDI)TQFV`9?rG&vedXop;pJ$6?Olq1tj+{Z*H8^OSP& zc=Xn#I0DiIc&;A1_1%M`$~{<(^UBTBN@EwUYV^yVXwOA6X4+Zxv+(t>zT2haF67Bg^ZTv#Ld09Pmci_~Wq$UO4 z=c{sic4+WM*dw1sd(Ox19ss~$FmNBibZitn>)d`$Yj)t&t%1v@)$>=?`_0J>a-L1Q z>NsO<^-?3ScJy|Sa^Xl)ZGt?xKELZA)?ohz)Gp-FTr9ZL3n(;=nd2$gjWKAtPdpGb7CkMfve*Mi4%-X z^mji}4`1!Se?JMoRu>uo2Goa4t4CYDiglh(h4AfK6Odq*vOOG+F)Kq3Heg{_Yd~-Dd~(cMP2BNP-hN4~#tx;Qgb+15)nY z>J|bpe`%RFcJrF@>EX1@3sbR5U^hx`$;3|sr|+t_zEY3FTJ4V>zN?1zQt|fzCk2Io zL9)KPY14)exPJ|DqEzlwFEk8<+LWsgLCHArjnk-VKrBXEI{NP)SDKGUdu}QZ_Nh0o z#=7>yLrqNXE-U3eh6N3{eOq~9$9jR~gi_X9 zQfFO>Rn8V@H`ej8L*grW6tAD#1XR)Tc;GrtfHAs6PVl;Yu0YUV#R+mA&?)MCR~)v) zE})qvXW_G;;FELWFI)j$&eiU#cy^bFcu_L{WohXa-WT+9VkvgHbc4We=RA^Fx?z`8 z%Lhs;7py5|dC!hg-VGmPWAo~EVaFsF4$H;kShao#1%MsRII$Lr82k${gCAI!ymu|3 z)!!CRjubEKEiQ``mxc3_KcV-2Uo^FE+~mOw+qld@28z)8-{&v|6C(MuI*M5XEL{PR$Z`%LX_x5!i1XO6PW%FcgIYhog{QjpW2?6c z8TB^Sx_dOknC+Q*M$+vUhB4bS!l(62o8hh22m^M}HL#(s>voh_G-OS?xu#GfX;wF& zV>9ZRbIf_GxsHXAvJ-7>&(QAc8FRgVhQ95w@5x{@+i=_|SG~Q>&{pC#n`Oph{HY`kF)C^`mBieUbKeqjX*=eybzW^H? zp6`~XCU8UVMwuan_oQjY_lX^oxm}^mG!wbzw?H}R*N*uqBMdmD8HqlzqotP;zmiwz z9K((|pTrgWEU-t&@>=#}OS2M?+jTmqx7BB}dFnT}SanF4#xpX!{V@M-&AA z4+x#8ruaVM45rB&C8mRK9y|)ia^k6q{-OVoywyI7|!Mo-gl~vC+SnV_k`Z5EuHGo zy|~4Y1IRsBs%{ZJ%>`spPLK#g^yQvDMQ7wQ>M# z+v6sH&K_7CeD%pO<&!hem{HFr+ZEVc>~Sj?m`>=s)=aG=)rsY$pl{v&I%v$p`;%K$ znva5phPEj*wJ2dr6npp(+Kx8d1x5a2df?^fty%w;TZFm95%P6hVD`Lu^X52b301R7 za59}x<*i*3C$3qa*kRbXA;|5{!Wo4L-R;n(HaH15OuhMQpjMbfoO~ypJ-W>+cM642!OH+I29fvm zs&uU>#l^^4YY?(ks9Go61=y+uQG~@o9zI6P>OmWloyIbhG=nIn`@4@(`qW?Q#2K6q z?~#(gZ_?D<|L7d<-VFo~ub|&a^gd6v<34;7m`f^ zh$Yw>%2qEgXT=Fbw(_EEuX9VaawhjN7 zwjl6=Y?t7cn1litCrn9>fbvl&!eTrYI2_pl`=~nDfpBteyaO(TlXD40YJGB$UwqYe z*-}krKt$gknuPQcP&vp2Xy66we7u|0jl0+3#VYs{mx8E`8+WfTc?&|T`?B%}8B4}X zeU7|v;mlsg%*L$mGZ{xg^UU+M-mFQHtVx4RK}OKhHnY9-a%opd@APGn>C5P|Z|wh_ z_n&#;4cmI({4nz7hxFmMjF)mg=Rc=?{Jn$8$!{Ll{E^o84D9=oj>;QrdS{eHW|R#w_=NO( zVXtFR#IY#sSVX6Qz34sbJ+q^AM{nUPk-}FR9gR2@c@x4D#IM-yg}1lg*$%h#(>q3r zjFG|)X)u$?E@)mdXtNp(^1iXfeMQsy@{0QMiw3hZ3Nru5K=CImpaZ$X78#i$?h=0E zm{qyd_M4^l%9UCFWwk&lJNz8$b-TQ*$ybT&wjchUya$D5ty9}>D34{m#S31_8@6j; z-(QD6dV$4YH&_fA!45FZ(^w3sCq`~DEHqdQ2@GcC4gv~1yt=nBt;I0s8QKd5l~Bri zBu<(OvW;{5mwIa5g2+kBH*b1p?h>l2%#KKHWi*xMT#HNPq>e%|htGP4oo~=>MB$gp zw$*QMDzPO*xqv!j-RD+=ptK4L;2EhR;wH`Mq~dKHiZ_Avt>IBAANy9KBt3vgztDC)0)J25 zghHd_p=lVbXXx^fo^xApIUqFBF9{Y~y1pT=UN~?E{}YsQPE@`$nHw1<&g4H1LC=*Y?55Xxm|vTA2C3QW1p`~#2vc#DYx#$9-oi3Lue}=5lDrXINt|coo|=cN!mT?^h~Xd^ZpJm zRC!qdBL3AifoeX@AP?M79)wciSx43pTc2CyOv;6+Qgw2YSCA!e zy8XBmz?c6KIsE02^n<110 zp^Jkf5vXMefoi~@k;8}tDzKQ|1(0@n>`}Sd43SYY%D-tr>#5~DJ1XerNn?7oty{f# zRr%xqNDLUfdgKAj9SIGFARuF$8i*c4K$b90LK@e)Aw~#}ckpj}Ak35|cxggG0=?Kw zXH$Ro7s`SC(K~HP+67|65MxMjkYJN@o!*3uYw3j8iQ0BB*7-^F#x>MZLj+eTB*+oi zs$(2PANh%6{SRAd_yfB{qQ(8)_no`on!9%GT$Xjd`);}4UoMIo5oiO~aaMBGMuDx| zx-f9&fJ!jaxL|t7+MwSZFaiPt#3g?tF6%2Sy!6Q1gRC*5A8Xdc_IodoLo&)8|LG{ZHN_aTj=OU3`{XvPW z5i(yx1+wr4id3UN@WB#iMM`H0B@>ah1tpDMO*jEjZWC6sW zq=?%1)|xuqn<2I5D0(s>RY$q^rP9@YZiSQ+$H0#Wk`;tHddD^kK5b>q@{}6h zBDniBnbA*Nm`dw?^-L$W^5_V-iUAFD0K)lT0)Wjr5K6H&hZ4`~S9`(V4N@z*?;aIg zMx;t}17UuPAan>3Jr^N} z-u>aD>8tc?B((8|5`-`6_Gp5-buqXSMBwjG45Ei1ZjHXiX-aYiWa{E^! z=iN6FylcSl(|Ff_;O~xiQELR)bex+q>c0x-R(m-g$t#3_^6d+1s9jq^Bj;|lWv!6DZa1bVB~srZ`NW>$gtr-j zNcP}&W=5xWA7rhgZ-RKk!9I;|W+5T;LhDHOF7sa_RvucC!(h~iD6I~Uo;{%)?4%|m z%~_0BBf$cCuR!`NdbqHAW@JxgN0V4CU1fcJ6&)L2?d|Wm5n9V)PpTk4MjSD+j&=CZdpFn}cg0#dR~+IUgV!o0Y?Q62Hcg zbDggP6A~L9&_PBxcmWrvH!hieC|7RB?qKdNY$23;|1og6VfKO&Z6*_#7L+@Uby6!V zgr*3p)K`n()!tS}Wlw!o2Ctei+9H-6erNFXJiWjPCvDkMjrb@72cPZA$PoR`7qbBfT=&&)us zn(ceu9C?o3d<20sU0hJEqj@mea;J^i&7TkQfdXswMkEBl85lj@Wo+Gy=BcoMd{%)- z4QF~xhfSUj(GG8r*07kFy%G<{ET?n}ab(OmNR4t7A(e_yn;fjtDRdoFUX2urVU9%b zxYE)+D?FwGJ_aRM(3`(7lD{xKD&=EeNQrR*&5n0LP>LLGaiJ;i!c?Rp?c7S@9XvKA zIO{}PZ6Mm@lxA~MXj~K8Kof%ZO}nIQ0@t+GkYuhkjRJ3|)zy%s9$rF&RXZ-0#{ZwtiSbiS zChE}94sovOhmb!Pt+HU)9b>SL1y5Dn`CZBR+;GIDnlG`*cH~*PDD7D=xlm&k!y&JA zkTWs`h}w`Ql0jL0Jw8PRGqj3wBJE$D~&B|Uj1oq zOK#t^H~OY79L%!gQ3ojCs6+0LI_f}$Lrx9~an%VdgAu78ab{@Z%%5uVd7wG{LZ$gMv$K|I3_yboMg}lsN$G_D zBh0;@oT5e9&d{o+isCygavXicN0mQ##%JoLr_l#Us3|EasS(h$M384$gmw(sD1;8^oE(I6o1UaDs6&$jPKycLJopb%YOor5pX40+A}D!% z^iYr8hyP_j0V2g+f}BaufXVhMp7#mJ(gNm*3%Jb}FqcNaNV2dBiwY=Y8+alG^j8V! z5)g1dKr9>qHI9J7LBO>Ya7sR>D@#nJzc`lpc{b?dRtN!j1(U^ylVMK{M@TG|L95MT zhhx!DI2cP#C>#IvG2{zxCWOtO-!l`#=1(6pD{TJsF)xPApTW%6EGs)I2O0QEE~+g{ zEU;UFqHR$}VMo;sZ^x9#j3wmBHyg=Uc*{2Oom_lm8Eg3`OGn`#gWqY&m#13Z7-aOK z(lP~q>&07^iTGPD$|vAYy(rV~$g{GkmX%#LM7OfrL%zZrpnvc)xu~?{TNZT`b%^A!CloYN4gweMH(m*wsjlMv8<)#z~&=itS6E_S`$R zu>*9k&)m6l{^rcR=X}SXwX}!`#vlJJB{z8xx7xBrG(keH@)uQ+Z-Le;WYh~AKsAG*W1j^##^<$-Dmx4Ps zzZ_h=J{H{3`)aT>cF8_@!JfMTi=}T*mCjDz$sZ6!VA$VGl>T<4bpC>)a`)yZoxi`6 z-|zUaPkdZDbKd^?5-gTJIaE4zV)46a$6j0^arJe$tmc;n26pu;T2{?yiX}Nu?;47x zcQBS$jYVVGL=-Dkj-bSc)o4l`0x56Ei8xUcDsqjis-Cc-|P;J6H|*&2JFODA3`Bj>sdHnTN52I-Il)sf_?tJQk@SY4{h z5K2}P1?51B_PLGIWW z4=s%!UYhyP{^lgjJ!06Mjp0~2SUvLYV8vqY#`3lE)OAgxmI~d$l_MvX=RRJU{M0^r znkrdbT$L4zO>09dw;(8^4W>s0RXRFuANw?9wEv2Zeev4zmzSUxjAwpDt3Hm!>sLzC z9}tTGSip|_V|niD(zOHj^vu$g5A5&X5A}5j7Pm*0M=iIkka#i47mG`3YR2MIYE0HJ zITcG9QuOGa(P%cN4_o}ZTH>!thZ{G62osRP1RJa-;wdV}^cZd?&z`iRX|W+iBTmWI z27|wB3%{YJ3=o9hRPji77gm!>T-U<8M)hGe69#+;%W8Zitj2(#LNz0$M>DFP7#s~{ zN3Hd-L0!R({AwNJ$Kf@8xU?T&T83X9mA$AraD3ZOQrnMGTTyB^rS|-e1+Mw%!2Z5N zefhoxUtmI<>?-<#rZ1R(c|r1-(vy?BXWT_8Y)WC!^{zF&ZReV1{!sLGn%>U*i}N1o zsCHm)e(!?dE3|Gd3SFkqRiI=O&huB_gbAo9Y&V7N1!4PbkH3ri`1VMqW*2F^su!Npmk zQyFBxDHy#3@Zo@wUIP*YonUnV>2=o08bIVwjsbW9*=$XI5Xr8+>>KDbdzb^LsVmw0 zkknYR`?wr;t`69$K_Y;p#yIMI1KhRT*G$&lljC$>9iL<%9SvOCHwjxO`&1d9vzGxh zXKTQ@>TUJ=^8l#PPR1KI!4)lT2T+_IWPfp-l<)Z1-J8ctV>2uH{S{cDElsRPjQa=J zy|ZWe>cORnnbP#?oasdFqK62Q;c%%~V10Wd+0W`2yQ(Pt4Wn^@Vr;X2Q zsn#FuoD{Df<-^$*@YK;vQ3aD6u$~}5(oojb^0hoMZ8Q^4s22Zc(q1h#Yv@=YMy*r= zk!0cq#!4nWYHMOS8*mLtD#|C&&E>;C8J{k2$<9oewwIw$Z>?!m~+Vf zY)sRYP+Y}I2>w(JB{D-!V-X#Jp(j#KUukj}?sX`-_-F_y;Tx~8Ra=-Q558Dd6W%Cuqd49;e$;WJ-SzAVM RyQj=X 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 literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_push_attachment.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_push_attachment.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1da2d8ef4e66cc7c77d8e872c3a875f48b6fc36b GIT binary patch literal 8652 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 GV*dmFXT-q( literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_push_extend_info.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_push_extend_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30e0c404d905e3cc35969db54e9c3e76553daccb GIT binary patch literal 8477 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 O%N}R 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 literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_push_order.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_push_order.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4751a93960f3c2d863e7e068e96ebc51937c45e GIT binary patch literal 12805 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 literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_push_order_detail.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_push_order_detail.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fd156079c29811dba77690389ec036a34412826 GIT binary patch literal 10190 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 W89`2WBDzz^BnW)aRX~qyxBmfK{$j%b literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_sign_task.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_sign_task.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b8e8e6a83971f6ebfc03f633497301ff30f9007 GIT binary patch literal 5858 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 XbFBm=xlSV2iF(f45IGO5so(a0oAnE} literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_update_post_delay.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_update_post_delay.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..363c29421d640afa52292f52dfd4ea0508624e08 GIT binary patch literal 4808 zcmbtXYitu&7QW*dzs8Oo=K+LAa2C?k6m0P*;u%y03TVNhA?+^yGiyAPU^DilGh@+! zBN>*qp**WXK?MY;g=QBhAh9iU>F(eDSZT*-R<%Zogamfr7p{Om{ju6}XFSGsLRZ?o zK6B^Jz31F}?mg#x_xS6E1{y(eKX)bm7*;-O1dL%{oBqrBs~$2 z-gfZbWMiaJZ##KkvMJJ}w`smP*%E1iwu@^Wu(0lfRAd_4z*$)jMM z)(c!S*ha1mBz(}G$<1P$c3UuuxDd2z=4U78MCK64g19+}c2o2UG_IN-C^8pD@iUWc zxz>6GPU0#&MydS*?JV2%ConC21UTs8XGb6XaISdjX5sqj!i8fK-&~sb;$Y#s%Y~z- z3V;2!c z_uruD!i~Fy?@ty#|G0SROJjGnSA#3v1ET# z+KuPtV!H%}7Y3niO|$VBp2tyUl{5;Lj>amd2MST}{1KjK>!JEBBCwYcBSZ#`SmY|c zM5Ub}GL|b~!K=CzK?uZIw2yobohCn`wxfV0ymx7c`#|QzRFwanAnxuJSxyYG@xizp z<$Jb<(nI(FwuwTFlce<=A06JD8V~{&(GHVo-W0b7@3XQ$Qf$EecHEa1dCgtk4YN~V zhb0om{C@v08$$01d^*WVA-rS=W>0W2Sqg0%mUjuM(E6pDL#z^EoU zeBG+ATX9Vys{0uTB5V}X>LMqL!wgt6&BZFG3OqRab=!OZr*GIMgA!O;FBdB|F?;a4 zHg>>@CWgQmSPo=fMh}5xeCFknz`mfDdcF}-24pfz!e+KIBumu#!U*+B&N{Cdv1TZi zEMq}qwU#VZuVt#KX55;wo<-*;?IlBqT{V2R3~{k)f2P*;XLJ^^_VZ-@nydqm1fGC1 z{UJ!w;ADOS6{=(V@s00_w+<9OI|^t}MvJcwPMr7%2mklSA7&5Gy5Opzs2EN9O_Q(k zmnCl8eSGUuEl0%_R{5WNeDv{+Q%}xZD2$%gnKUXUu$)FFrNJjZf>LP=wv&}Aj-Dud zav_+WHRWjGt6Pt6Uk12JcTG87z3QVo*Nf*4Vcr(t^*_4v=kkWnjZIuXRQTJ6!EnH# zQ8*Gc8_QwsM0}fRR6-I`nw1x#tR!OJ(OfvPb`CQ`QF)hUeP4?2w;cjniVkuTb~$VTzh7*^1HXw50Koiuq|O7z+3-mJfXax}k_Wk4j;#I3 zHTS-2ZqC)Ax;nD!$0_${|Nij7a5g;N*m9CSvnR~!A=&Eu{{)iwLfjxk%#6;fRx z;Po`Cp1BvDV{hg>i&f8Jh@Ddk^td!dJtwaAiOFuGm!Jq-NQ!uSI+#-Wg+u}w68Yd!_ZSk@p9Y&i0esUB}3f2 zUSA>YhP8_4t`)CcG8RZ+9UHs57~! zll6CQTGhFARp)jM1xU>q6H-h}NT+1Ys%H^eg8_`?9O+e8z&A(Wj)2Jn({#8Nv7>5@ zQE508V+P_W76Wyh6)~D!n)F98{EihP()=7%$-S+aa!u^G{n{+F+m^o#2<|wX(RTsk}Jq1rS?x7bR zWNlCy)}Gl2l|ebGTcx@cs(ZY3w%WRAY%tflTy0&hcjrjTEP<=bC4P+ONeW8=8nd%}Kb%aJY0%q7aQ%^=u@ z1>5KnvNXTC8nwT$A>WLX4WD{XdPl8!PxJBkC-F1(oX4+v{EA}=8uv_7J+J58zVrR+ zoTWc3Q&;zn&s4GdZ=mMB;;tkgn9HhHbTE?C@RTZFeZpx5dJ(4 zLaZBk9BmAwgj@{mU3k`DsDffz#7H9Hs@ZeLTzFcz!Xr)iKv+{ IpKh=J0KsxuqyPW_ literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/__pycache__/oa_upload.cpython-311.pyc b/dock/oa_dcm/__pycache__/oa_upload.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d048ce1d1b3a946795b8ba78be63cb0f5ec14df GIT binary patch literal 12227 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 literal 0 HcmV?d00001 diff --git a/dock/oa_dcm/oa_push_attachment.py b/dock/oa_dcm/oa_push_attachment.py new file mode 100644 index 0000000..d266f18 --- /dev/null +++ b/dock/oa_dcm/oa_push_attachment.py @@ -0,0 +1,140 @@ +""" +待办工单附件数据上传。 + +对应文档接口:7、推送附件信息 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from models.dcm_task_attachment import DcmTaskAttachment +from models.dcm_task_file_upload import DcmTaskFileUpload +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +DcmTaskAttachmentMapping = { + DcmTaskAttachment.id.key: 'id', + DcmTaskFileUpload.oa_media_id.key: 'mediaId', + DcmTaskAttachment.media_usage.key: 'mediaUsage', + DcmTaskAttachment.act_def_name.key: 'actDefName', + DcmTaskAttachment.upload_time.key: 'uploadCreateTime', +} +""" +附件数据推送映射关系。 +""" + + +async def after_push_attachment_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code==200: + dcm_task_id = getattr(response.request, "dcm_task_id") + await DcmPushStatus.set_push_task_attachment_status(dcm_task_id) + echo_log(f"推送企业待办附件成功.") + else: + echo_log(f"推送企业待办附件失败:{message}") + + if retry_queue: + echo_log(f"企业待办附件重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def push_attachment(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待办附件数据及其数据。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).order_by(desc(DcmTask.act_id)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的附件数据...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次推送待办:{task_id} 的附件数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待办附件数据...") + + dcm_task_df = await DcmTask.query_as_df(task_query) + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + + # 预处理数据方法 + def preprocess(df: pd.DataFrame): + # 更名,并仅保留需要的列 + df = df.rename(columns=DcmTaskAttachmentMapping) + df = df[list(DcmTaskAttachmentMapping.values()) + [DcmTaskAttachment.dcm_task_id.key]] + return df + + # 填充附件数据 + await DcmTaskAttachment.fill_attachment(dcm_task_df, column_name='attachmentList', preprocessing=preprocess) + + # 处理无附件待办状态 + empty_dcm_task_df = dcm_task_df[dcm_task_df['attachmentList'].apply(lambda x: len(x) == 0)] + empty_dcm_task_df[DcmPushStatus.dcm_task_id.key] = empty_dcm_task_df[DcmTask.id.key] + empty_dcm_task_df[DcmPushStatus.push_task_attachment_status.key] = 1 + empty_dcm_task_df = empty_dcm_task_df[[DcmPushStatus.dcm_task_id.key, DcmPushStatus.push_task_attachment_status.key]] + await DcmPushStatus.save_batch(empty_dcm_task_df) + + # 过滤空数组 + full_dcm_task_df = dcm_task_df[dcm_task_df['attachmentList'].apply(lambda x: len(x) > 0)] + # 删除 DcmTaskAttachment.dcm_task_id.key 字段 + def remove_dcm_task_id(attachment_list): + for item in attachment_list: + if isinstance(item, dict) and DcmTaskAttachment.dcm_task_id.key in item: + del item[DcmTaskAttachment.dcm_task_id.key] + return attachment_list + # 执行替换 + full_dcm_task_df['attachmentList'] = full_dcm_task_df['attachmentList'].apply(remove_dcm_task_id) + + # 处理数据映射,适应接口推送 + mapped_df = full_dcm_task_df.rename(columns={DcmTask.id.key: 'gdId'}) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + dcm_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + push_request = await oa_api_request.get_push_attachment_request(**row.to_dict()) + setattr(push_request, "dcm_task_id", row.get('gdId')) + await dcm_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送待办附件数据...") + await requests.async_concurrency( + dcm_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_attachment_request + ) + echo_log(f"待办附件数据推送已经完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + _runner = aio_pool.get_aio_runner() + _runner(push_attachment(task_id=2054174091237265408)) diff --git a/dock/oa_dcm/oa_push_extend_info.py b/dock/oa_dcm/oa_push_extend_info.py new file mode 100644 index 0000000..8f822ad --- /dev/null +++ b/dock/oa_dcm/oa_push_extend_info.py @@ -0,0 +1,135 @@ +""" +待办工单扩展数据推送。 + +对应文档接口:9、推送扩展信息 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from models.dcm_task_extend_info import DcmTaskExtendedInfo +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +DcmTaskExtendedInfoMapping = { + DcmTaskExtendedInfo.id.key: 'id', + DcmTaskExtendedInfo.display_name.key: 'fieldName', + DcmTaskExtendedInfo.field_value.key: 'fieldValue' +} + + +async def after_push_extend_info_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + dcm_task_id = getattr(response.request, "dcm_task_id") + await DcmPushStatus.set_push_task_extend_info_status(dcm_task_id) + echo_log(f"推送企业待办扩展信息成功.") + else: + echo_log(f"推送企业待办扩展信息失败:{message}") + + if retry_queue: + echo_log(f"企业待办扩展信息重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def push_extend_info(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待办扩展信息数据及其数据。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).order_by(desc(DcmTask.act_id)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的扩展信息数据...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次推送待办:{task_id} 的扩展信息数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送待办前 {fetch_size} 条扩展信息数据...") + + dcm_task_df = await DcmTask.query_as_df(task_query) + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + + # 预处理数据方法 + def preprocess(df: pd.DataFrame): + # 更名,并仅保留需要的列 + df = df.rename(columns=DcmTaskExtendedInfoMapping) + df = df[list(DcmTaskExtendedInfoMapping.values())+[DcmTaskExtendedInfo.dcm_task_id.key]] + return df + + # 填充表单数据 + await DcmTaskExtendedInfo.fill_extend_info(dcm_task_df, column_name='extendList', preprocessing=preprocess) + + # 处理无待办工单扩展信息状态 + empty_dcm_task_df = dcm_task_df[dcm_task_df['extendList'].apply(lambda x: len(x) == 0)] + empty_dcm_task_df[DcmPushStatus.dcm_task_id.key] = empty_dcm_task_df[DcmTask.id.key] + empty_dcm_task_df[DcmPushStatus.push_task_extend_info_status.key] = 1 + empty_dcm_task_df = empty_dcm_task_df[[DcmPushStatus.dcm_task_id.key, DcmPushStatus.push_task_extend_info_status.key]] + await DcmPushStatus.save_batch(empty_dcm_task_df) + + # 过滤空数组 + full_dcm_task_df = dcm_task_df[dcm_task_df['extendList'].apply(lambda x: len(x) > 0)] + # 删除 DcmTaskExtendedInfo.dcm_task_id.key 字段 + def remove_dcm_task_id(attachment_list): + for item in attachment_list: + if isinstance(item, dict) and DcmTaskExtendedInfo.dcm_task_id.key in item: + del item[DcmTaskExtendedInfo.dcm_task_id.key] + return attachment_list + + # 执行替换 + full_dcm_task_df['extendList'] = full_dcm_task_df['extendList'].apply(remove_dcm_task_id) + # 处理数据映射,适应接口推送 + mapped_df = full_dcm_task_df.rename(columns={DcmTask.id.key: 'gdId'}) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + dcm_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + push_request = await oa_api_request.get_push_extend_info_request(**row.to_dict()) + setattr(push_request, "dcm_task_id", row.get('gdId')) + await dcm_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送代办扩展信息数据...") + await requests.async_concurrency( + dcm_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_extend_info_request + ) + echo_log(f"待办扩展信息推送已经完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(push_extend_info(task_id=2054174091237265408)) diff --git a/dock/oa_dcm/oa_push_more_info.py b/dock/oa_dcm/oa_push_more_info.py new file mode 100644 index 0000000..a922a57 --- /dev/null +++ b/dock/oa_dcm/oa_push_more_info.py @@ -0,0 +1,139 @@ +""" +待办工单更多信息推送。 + +对应文档接口:8、推送更多信息 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from models.dcm_task_more_info import DcmTaskMoreInfo +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +DcmTaskMoreInfoMapping = { + DcmTaskMoreInfo.id.key: 'id', + DcmTaskMoreInfo.create_time.key: 'time' +} +""" +表单数据-更多信息推送映射关系。 +""" + + +async def after_push_more_info_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + dcm_task_id = getattr(response.request, "dcm_task_id") + await DcmPushStatus.set_push_task_more_info_status(dcm_task_id) + echo_log(f"推送企业待办更多信息成功.") + else: + echo_log(f"推送企业待办更多信息失败:{message}") + + if retry_queue: + echo_log(f"企业待办更多信息重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def push_more_info(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待办更多信息数据及其数据。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).order_by(desc(DcmTask.act_id)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的更多信息数据...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次推送待办:{task_id} 的更多信息数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条更多信息数据...") + + dcm_task_df = await DcmTask.query_as_df(task_query) + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + + # 预处理数据方法 + def preprocess(df: pd.DataFrame): + # 更名,并仅保留需要的列 + df = df.rename(columns=DcmTaskMoreInfoMapping) + df['content'] = df['time'].fillna("") + ' ' + df['human_name'].fillna("") + df['msg_type'].fillna("") + df = df[list(DcmTaskMoreInfoMapping.values()) + ['content',DcmTaskMoreInfo.dcm_task_id.key]] + return df + + # 填充表单数据 + await DcmTaskMoreInfo.fill_more_info(dcm_task_df, column_name='moreInfoList', preprocessing=preprocess) + + # 处理无待办工单更多信息状态 + empty_dcm_task_df = dcm_task_df[dcm_task_df['moreInfoList'].apply(lambda x: len(x) == 0)] + empty_dcm_task_df[DcmPushStatus.dcm_task_id.key] = empty_dcm_task_df[DcmTask.id.key] + empty_dcm_task_df[DcmPushStatus.push_task_more_info_status.key] = 1 + empty_dcm_task_df = empty_dcm_task_df[[DcmPushStatus.dcm_task_id.key, DcmPushStatus.push_task_more_info_status.key]] + await DcmPushStatus.save_batch(empty_dcm_task_df) + + # 过滤空数组 + full_dcm_task_df = dcm_task_df[dcm_task_df['moreInfoList'].apply(lambda x: len(x) > 0)] + # 删除 DcmTaskMoreInfo.dcm_task_id.key 字段 + def remove_dcm_task_id(attachment_list): + for item in attachment_list: + if isinstance(item, dict) and DcmTaskMoreInfo.dcm_task_id.key in item: + del item[DcmTaskMoreInfo.dcm_task_id.key] + return attachment_list + + # 执行替换 + full_dcm_task_df['moreInfoList'] = full_dcm_task_df['moreInfoList'].apply(remove_dcm_task_id) + + # 处理数据映射,适应接口推送 + mapped_df = full_dcm_task_df.rename(columns={DcmTask.id.key: 'gdId'}) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + dcm_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + push_request = await oa_api_request.get_push_more_info_request(**row.to_dict()) + setattr(push_request, "dcm_task_id", row.get('gdId')) + await dcm_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送更多信息数据...") + await requests.async_concurrency( + dcm_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_more_info_request + ) + echo_log(f"更多信息数据推送已经完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(push_more_info(task_id=2054174091237265408)) diff --git a/dock/oa_dcm/oa_push_order.py b/dock/oa_dcm/oa_push_order.py new file mode 100644 index 0000000..dac8c38 --- /dev/null +++ b/dock/oa_dcm/oa_push_order.py @@ -0,0 +1,241 @@ +""" +待办工单推送。 + +对应文档接口:2、推送待办工单列表 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +DcmTaskMapping = { + DcmTask.id.key: 'gdId', + DcmTask.task_num.key: 'taskNum', + DcmTask.other_task_num.key: 'otherTaskNum', + DcmTask.bundle_deadline_time.key: 'bundleDeadlineTimeStr', + DcmTask.rollback_deadline.key: 'rollbackDeadlineStr', + DcmTask.event_src_name.key: 'eventSrcName', + DcmTask.rec_type_name.key: 'recTypeName', + DcmTask.event_type_name.key: 'eventTypeName', + DcmTask.main_type_name.key: 'mainTypeName', + DcmTask.sub_type_name.key: 'subTypeName', + DcmTask.urgency_level.key: 'urgencyLevel', + DcmTask.event_desc.key: 'eventDesc', + DcmTask.address.key: 'address', + DcmTask.processing_deadline.key: 'disposalTimeLimit', + DcmTask.district_name.key: 'districtName', + DcmTask.new_inst_cond_name.key: 'newInstCondName', + DcmTask.case_closure_condition.key: 'closingConditions', + DcmTask.reporter_name.key: 'reporterName', + DcmTask.reporter_contact.key: 'reporterContact', + DcmTask.reply_intime.key: 'replyIntime', + DcmTask.first_depart_name.key: 'firstDepartName', + DcmTask.second_depart_name.key: 'secondDepartName', + DcmTask.bundle_warning_time.key: 'bundleWarningTimeStr', + DcmTask.act_ard_state_name.key: 'actArdStateName', + DcmTask.operation.key: 'operateType', +} +""" +数据推送映射关系。 +""" + + +async def after_push_order_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + dcm_task_id_list = getattr(response.request, "dcm_task_id_list", []) + dcm_task_push_data = [ + { + DcmPushStatus.dcm_task_id.key: dcm_task_id, + DcmPushStatus.push_task_status.key: 1 + } + for dcm_task_id in dcm_task_id_list + ] + dcm_task_push_df = pd.DataFrame(dcm_task_push_data) + await DcmPushStatus.save_batch(dcm_task_push_df) + echo_log(f"推送企业待办成功.") + else: + echo_log(f"推送企业待办失败:{message}") + + if retry_queue: + echo_log(f"企业待办重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def push_order(fetch_size: int = 50, batch_size: int = 10, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待办数据及其数据。 + + :param fetch_size: 本次推送数量 + :param batch_size: 分批时,每批大小 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select( + DcmTask.id, + DcmTask.task_num, DcmTask.other_task_num, + DcmTask.bundle_deadline_time, DcmTask.rollback_deadline, + DcmTask.event_src_name, DcmTask.rec_type_name, + DcmTask.event_type_name, DcmTask.main_type_name, + DcmTask.sub_type_name, DcmTask.urgency_level, + DcmTask.event_desc, DcmTask.address, + DcmTask.processing_deadline, DcmTask.district_name, + DcmTask.new_inst_cond_name, DcmTask.case_closure_condition, + DcmTask.reporter_name, DcmTask.reporter_contact, + DcmTask.reply_intime, DcmTask.first_depart_name, + DcmTask.second_depart_name, DcmTask.bundle_warning_time, + DcmTask.act_ard_state_name, DcmTask.operation + ).order_by( + desc(DcmTask.act_id) + ) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的数据...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次推送待办:{task_id} 的数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待办数据...") + + # if apps.get_active_env() == 'prod': + # # 生产环境只推送 DcmTaskProcessInfo.action_time > 2026-05-18 00:00:00 的待办工单 + # # 构建子查询:查找满足条件的 DcmTaskProcessInfo + # subquery = select(DcmTaskProcessInfo.dcm_task_id).where( + # DcmTaskProcessInfo.dcm_task_id == DcmTask.id, + # DcmTaskProcessInfo.action_time > '2026-05-18 00:00:00' + # ).group_by( + # DcmTaskProcessInfo.dcm_task_id + # ).order_by( + # desc(DcmTaskProcessInfo.action_time) + # ) + # task_query = task_query.where(exists(subquery)) + + dcm_task_df = await DcmTask.query_as_df(task_query) + # 格式化三个时间字段为 yyyy-MM-dd HH:mm:ss + dcm_task_df[DcmTask.bundle_deadline_time.key] = ( + pd.to_datetime( + dcm_task_df[DcmTask.bundle_deadline_time.key], unit='ms', errors='coerce' + ).dt.strftime('%Y-%m-%d %H:%M:%S') + ) + dcm_task_df[DcmTask.rollback_deadline.key] = ( + pd.to_datetime( + dcm_task_df[DcmTask.rollback_deadline.key], unit='ms', errors='coerce' + ).dt.strftime('%Y-%m-%d %H:%M:%S')) + dcm_task_df[DcmTask.bundle_warning_time.key] = ( + pd.to_datetime( + dcm_task_df[DcmTask.bundle_warning_time.key], unit='ms', errors='coerce' + ).dt.strftime('%Y-%m-%d %H:%M:%S')) + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + # 代码转义 + reply_intime_map = { + '0': '无需回复', + '1': '待回复', + '2': '已回复', + '3': '超时未回复', + '4': '无需回复已恢复', + } + dcm_task_df[DcmTask.reply_intime.key] = dcm_task_df[DcmTask.reply_intime.key].astype(str).map( + lambda x: reply_intime_map.get(x, x) + ) + urgency_level_map = { + '0': '正常', + '1': '紧急', + } + dcm_task_df[DcmTask.urgency_level.key] = dcm_task_df[DcmTask.urgency_level.key].astype(str).map( + lambda x: urgency_level_map.get(x, x) + ) + + dcm_task_df[DcmTask.operation.key] = dcm_task_df[DcmTask.operation.key].str.split(',') + + # 处理数据映射,适应接口推送 + mapped_df = dcm_task_df.rename(columns=DcmTaskMapping) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + dcm_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for start in range(0, len(mapped_df), batch_size): + batch_df: pd.DataFrame = mapped_df.iloc[start:start + batch_size] + push_list = batch_df.to_dict('records') + push_request = await oa_api_request.get_push_order_request(push_list) + setattr(push_request, "dcm_task_id_list", batch_df['gdId'].unique().tolist()) + await dcm_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送待办数据...") + await requests.async_concurrency( + dcm_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_order_request + ) + echo_log(f"待办数据推送已经完成...") + + +async def push_full_order(fetch_size: int = 50, batch_size: int = 10, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + from dock.oa_dcm import oa_push_order_detail, oa_push_process_info, oa_push_attachment, \ + oa_push_more_info, oa_push_extend_info, oa_upload + + await push_order(fetch_size, batch_size, task_id) + await oa_push_order_detail.push_order_detail(fetch_size, task_id) + await oa_push_process_info.push_process_info(fetch_size, task_id) + await oa_upload.upload_with_attachment(fetch_size, task_id) + await oa_push_attachment.push_attachment(fetch_size, task_id) + await oa_push_more_info.push_more_info(fetch_size, task_id) + await oa_push_extend_info.push_extend_info(fetch_size, task_id) + + +async def push_sign_order(): + """ + 推送并签收工单。 + """ + from dock.oa_dcm import oa_push_order_detail, oa_push_process_info, oa_push_attachment, \ + oa_push_more_info, oa_push_extend_info, oa_upload, oa_sign_task + + # TODO: 查询得到要推送的工单ID列表 + task_id_list = [] + + if task_id_list: + await push_order(task_id=task_id_list) + await oa_push_order_detail.push_order_detail(task_id=task_id_list) + await oa_push_process_info.push_process_info(task_id=task_id_list) + await oa_upload.upload_with_attachment(task_id=task_id_list) + await oa_push_attachment.push_attachment(task_id=task_id_list) + await oa_push_more_info.push_more_info(task_id=task_id_list) + await oa_push_extend_info.push_extend_info(task_id=task_id_list) + # 推送结束后,签收工单 + await oa_sign_task.sign_task(task_id=task_id_list) + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(push_order(task_id=2054174091254042627)) diff --git a/dock/oa_dcm/oa_push_order_detail.py b/dock/oa_dcm/oa_push_order_detail.py new file mode 100644 index 0000000..1d9c6f9 --- /dev/null +++ b/dock/oa_dcm/oa_push_order_detail.py @@ -0,0 +1,165 @@ +""" +待办工单明细推送。 + +对应文档接口:5、推送工单详情 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from models.dcm_task_form_datum import DcmTaskFormDatum +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +DcmTaskMapping = { + DcmTask.id.key: 'gdId', + DcmTask.part_code.key: 'partCode', + DcmTaskFormDatum.func_limit_char.key: 'funcLimitChar', + DcmTask.reporter_name.key: 'reporterName', + DcmTaskFormDatum.media_upload_total_num.key: 'mediaUploadTotalNum', + DcmTask.return_visit_flag.key:'returnVisitFlag', + DcmTask.func_forbid_reporter_info_flag.key:'funcForbidReporterInfoFlag', + DcmTaskFormDatum.tell_num.key:'contactNumberDd', + DcmTask.reporter_contact.key: 'reportNumberDd', + DcmTaskFormDatum.deal_person_org.key: 'dealPersonOrg', + DcmTaskFormDatum.undertake_user_name.key: 'undertakeUserName', +} +""" +数据推送映射关系。 +""" + + +async def after_push_order_detail_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code==200: + dcm_task_id = getattr(response.request, "dcm_task_id") + await DcmPushStatus.set_push_task_detail_status(dcm_task_id) + echo_log(f"推送工单详情成功.") + else: + echo_log(f"推送工单详情失败:{message}") + + if retry_queue: + echo_log(f"工单详情重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def push_order_detail(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待办数据及其数据。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).order_by(desc(DcmTask.act_id)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的详细数据...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次推送待办:{task_id} 的详细数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待办的详细数据...") + task_id_list = (await DcmTask.orm_execute_scalars(task_query)).all() + task_id_list = [f"{id}" for id in task_id_list] + + # 查询属于这些任务的所有详细数据 + datum_query = select( + DcmTask.id, DcmTask.part_code, DcmTask.reporter_name, DcmTask.reporter_contact, + DcmTask.return_visit_flag, DcmTask.func_forbid_reporter_info_flag, + DcmTaskFormDatum.media_upload_total_num, DcmTaskFormDatum.func_limit_char, + DcmTaskFormDatum.tell_num, DcmTaskFormDatum.undertake_user_name, + DcmTaskFormDatum.deal_person_org + ).join( + DcmTaskFormDatum, DcmTask.id==DcmTaskFormDatum.dcm_task_id + ).where( + DcmTask.id.in_(task_id_list) + ) + + dcm_task_df = await DcmTask.query_as_df(datum_query) + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + dcm_task_df[DcmTaskFormDatum.media_upload_total_num.key] = dcm_task_df[DcmTaskFormDatum.media_upload_total_num.key].fillna(0).astype(int) + # 代码转义 + return_visit_flag_map = { + '0': '无需回访', + '1': '待回访', + '2': '已回访', + } + dcm_task_df[DcmTask.return_visit_flag.key] = dcm_task_df[DcmTask.return_visit_flag.key].astype(str).map( + lambda x: return_visit_flag_map.get(x, x) + ) + func_forbid_reporter_info_flag_map = { + '0': '否', + '1': '是', + } + dcm_task_df[DcmTask.func_forbid_reporter_info_flag.key] = dcm_task_df[DcmTask.func_forbid_reporter_info_flag.key].astype(str).map( + lambda x: func_forbid_reporter_info_flag_map.get(x, x) + ) + + # 未爬取的字段暂时填空值 + dcm_task_df['violationTaskNoDd'] = '' + dcm_task_df['telReply'] = '' + + # 处理无待办工单详情状态 + empty_task_ids = set(task_id_list) - set(dcm_task_df[DcmTask.id.key].unique().tolist()) + empty_task_data = [ + { + DcmPushStatus.dcm_task_id.key: dcm_task_id, + DcmPushStatus.push_task_status.key: 1 + } + for dcm_task_id in list(empty_task_ids) + ] + empty_task_df = pd.DataFrame(empty_task_data) + await DcmPushStatus.save_batch(empty_task_df) + + # 处理数据映射,适应接口推送 + mapped_df = dcm_task_df.rename(columns=DcmTaskMapping) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + dcm_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + push_request = await oa_api_request.get_push_order_detail_request(**row.to_dict()) + setattr(push_request, "dcm_task_id", row.get('gdId')) + await dcm_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送工单详情数据...") + await requests.async_concurrency( + dcm_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_order_detail_request + ) + echo_log(f"工单详情推送已经完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + _runner = aio_pool.get_aio_runner() + _runner(push_order_detail(task_id=2054174091254042627)) \ No newline at end of file diff --git a/dock/oa_dcm/oa_push_process_info.py b/dock/oa_dcm/oa_push_process_info.py new file mode 100644 index 0000000..f4921c6 --- /dev/null +++ b/dock/oa_dcm/oa_push_process_info.py @@ -0,0 +1,164 @@ +""" +推送办理经过。 + +对应文档接口:6、推送办理经过 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from models.dcm_task_process_info import DcmTaskProcessInfo +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +DcmTaskProcessInfoMapping = { + DcmTaskProcessInfo.id.key: 'id', + DcmTaskProcessInfo.action_time.key: 'actionTime', + DcmTaskProcessInfo.act_def_name.key: 'actDefName', + DcmTaskProcessInfo.human_name.key: 'humanName', + DcmTaskProcessInfo.unit_name.key: 'unitName', + DcmTaskProcessInfo.action_name.key: 'actionName', + DcmTaskProcessInfo.next_act_def_name.key: 'nextActDefName', + DcmTaskProcessInfo.detail.key: 'detail', +} + + +async def after_push_process_info_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code==200: + dcm_task_id = getattr(response.request, "dcm_task_id") + await DcmPushStatus.set_push_task_process_info_status(dcm_task_id) + echo_log(f"推送企业待办过程成功.") + else: + echo_log(f"推送企业待办过程失败:{message}") + + if retry_queue: + echo_log(f"企业待办过程重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def push_process_info(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待办附件数据及其数据。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).order_by(desc(DcmTask.act_id)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的过程数据...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次推送待办:{task_id} 的过程数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待办过程数据...") + dcm_task_df = await DcmTask.query_as_df(task_query) + + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + dcm_task_ids = dcm_task_df[DcmTask.id.key].unique().tolist() + dcm_task_check_info = {f'{tid}': '' for tid in dcm_task_ids} + + # 预处理数据方法 + def preprocess(df: pd.DataFrame): + # 格式化时间字段为 yyyy-MM-dd HH:mm:ss + df[DcmTaskProcessInfo.action_time.key] = ( + pd.to_datetime( + df[DcmTaskProcessInfo.action_time.key], unit='ms', errors='coerce' + ).dt.strftime('%Y-%m-%d %H:%M:%S') + ) + + # 设置 check_info + for tid in dcm_task_ids: + filtered = df[df[DcmTaskProcessInfo.dcm_task_id.key] == tid].copy() + if not filtered.empty: + min_row = filtered.loc[filtered[DcmTaskProcessInfo.item_id.key].idxmin()].iloc[0] + dcm_task_check_info[tid] = ( + f"{min_row[DcmTaskProcessInfo.action_time.key]}" + f"{min_row[DcmTaskProcessInfo.human_name.key]}" + f"在{min_row[DcmTaskProcessInfo.act_def_name.key]}阶段" + f"{min_row[DcmTaskProcessInfo.action_name.key]}" + ) + + # 更名,并仅保留需要的列 + df = df.rename(columns=DcmTaskProcessInfoMapping) + df = df[list(DcmTaskProcessInfoMapping.values()) + [DcmTaskProcessInfo.dcm_task_id.key]] + return df + + # 填充处理过程 + await DcmTaskProcessInfo.fill_process_info(dcm_task_df, column_name='handlingProcessList', preprocessing=preprocess) + + # 处理无待办工单处理过程状态 + empty_dcm_task_df = dcm_task_df[dcm_task_df['handlingProcessList'].apply(lambda x: len(x) == 0)] + empty_dcm_task_df[DcmPushStatus.dcm_task_id.key] = empty_dcm_task_df[DcmTask.id.key] + empty_dcm_task_df[DcmPushStatus.push_task_process_info_status.key] = 1 + empty_dcm_task_df = empty_dcm_task_df[[DcmPushStatus.dcm_task_id.key, DcmPushStatus.push_task_process_info_status.key]] + await DcmPushStatus.save_batch(empty_dcm_task_df) + + # 过滤空数组 + full_dcm_task_df = dcm_task_df[dcm_task_df['handlingProcessList'].apply(lambda x: len(x) > 0)] + # 删除 DcmTaskAttachment.dcm_task_id.key 字段 + def remove_dcm_task_id(attachment_list): + for item in attachment_list: + if isinstance(item, dict) and DcmTaskProcessInfo.dcm_task_id.key in item: + del item[DcmTaskProcessInfo.dcm_task_id.key] + return attachment_list + # 执行替换 + full_dcm_task_df['handlingProcessList'] = full_dcm_task_df['handlingProcessList'].apply(remove_dcm_task_id) + # 增加 checkContent 字段,来自办理流程数据合并 + full_dcm_task_df['checkContent'] = full_dcm_task_df[DcmTask.id.key].apply( + lambda x: dcm_task_check_info.get(x, '') + ) + + # 处理数据映射,适应接口推送 + mapped_df = full_dcm_task_df.rename(columns={DcmTask.id.key: 'gdId'}) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + dcm_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + push_request = await oa_api_request.get_push_process_info_request(**row.to_dict()) + setattr(push_request, "dcm_task_id", row.get('gdId')) + await dcm_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送待办过程数据...") + await requests.async_concurrency( + dcm_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_process_info_request + ) + echo_log(f"待办过程数据推送已经完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + _runner = aio_pool.get_aio_runner() + _runner(push_process_info(task_id=2054174091237265408)) diff --git a/dock/oa_dcm/oa_sign_task.py b/dock/oa_dcm/oa_sign_task.py new file mode 100644 index 0000000..798c567 --- /dev/null +++ b/dock/oa_dcm/oa_sign_task.py @@ -0,0 +1,98 @@ +""" +向 OA 平台上报数据已经完整推送。 + +对应文档接口:11、签收 +""" +import asyncio +import json +from typing import Optional, Union + +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + + +async def after_sign_task_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 签收工单请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + echo_log(f"签收工单成功.") + else: + echo_log(f"签收工单失败:{message}") + + if retry_queue: + echo_log(f"签收工单重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def sign_task(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 签收指定数量的工单任务 + + :param fetch_size: 本次签收的任务数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).join( + DcmPushStatus, DcmPushStatus.dcm_task_id == DcmTask.id + ).where( + DcmPushStatus.push_task_status == 1, + DcmPushStatus.push_task_detail_status == 1, + DcmPushStatus.push_task_attachment_status == 1, + DcmPushStatus.push_task_extend_info_status == 1, + DcmPushStatus.push_task_file_upload_status == 1, + DcmPushStatus.push_task_more_info_status == 1, + DcmPushStatus.push_task_process_info_status == 1, + ).order_by( + desc(DcmTask.act_id) + ) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次签收待办列表:{task_id} 的工单...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次签收待办:{task_id} 的工单...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次签收前 {fetch_size} 条待办工单...") + + dcm_task_df = await DcmTask.query_as_df(task_query) + # 格式化为字符串 + dcm_task_df[DcmTask.id.key] = dcm_task_df[DcmTask.id.key].astype(str) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + sign_request_queue = asyncio.Queue() + task_id_list = dcm_task_df[DcmTask.id.key].unique().tolist() + for task_id in task_id_list: + request = await oa_api_request.get_sign_task_request(task_id) + await sign_request_queue.put(request) + echo_log(f"开始签收工单...") + await requests.async_concurrency( + sign_request_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_sign_task_request + ) + echo_log(f"签收工单完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + _runner = aio_pool.get_aio_runner() + _runner(sign_task(task_id=2054174091228876801)) \ No newline at end of file diff --git a/dock/oa_dcm/oa_update_post_delay.py b/dock/oa_dcm/oa_update_post_delay.py new file mode 100644 index 0000000..59c7e41 --- /dev/null +++ b/dock/oa_dcm/oa_update_post_delay.py @@ -0,0 +1,88 @@ +""" +操作数字城管的工单延期接口后,向OA推送更新后的工单时间信息 + +对应文档接口:12、更新流程延期信息 +""" +import asyncio +import json +from datetime import datetime + +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +from dock.dcm import dcm_scrape +from dock.oa import oa_api, oa_api_request +from models.dcm_task import DcmTask +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + + +async def get_update_process_delay_request(data: dict): + api_url = '/externalWorkOrder/digitalCM/updateProcessDelayInfo' + request_body = data + # 构造 API 请求 + return await oa_api.new_api_request(api_url, request_body) + + +async def after_update_process_delay_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 更新流程延期请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + echo_log(f"更新流程延期成功.") + else: + echo_log(f"更新流程延期失败:{message}") + + if retry_queue: + echo_log(f"更新流程延期重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def update_process_delay(task_id: int): + """ + 流程延期更新之后,推送最新的时间信息 + + :param task_id: 对应的工单ID + """ + + echo_log(f"本次更新{task_id}的最新时间信息...") + dcm_task = await DcmTask.async_find_by_id(task_id) + echo_log(f"开始更新流程延期...") + await dcm_scrape.fetch_single_dcm_task(dcm_task) + # 重新获取更新后的任务对象 + dcm_task: DcmTask = await DcmTask.async_find_by_id(task_id) + if dcm_task is not None: + bundle_deadline_time_str = datetime.fromtimestamp( + dcm_task.bundle_deadline_time // 1000 + ).strftime("%Y-%m-%d %H:%M:%S") if dcm_task.bundle_deadline_time else '' + + rollback_deadline_str = datetime.fromtimestamp( + dcm_task.rollback_deadline // 1000 + ).strftime("%Y-%m-%d %H:%M:%S") if dcm_task.rollback_deadline else '' + + request = await oa_api_request.get_update_process_delay_request( + str(task_id), bundle_deadline_time_str, rollback_deadline_str + ) + queue = asyncio.Queue() + await queue.put(request) + await requests.async_concurrency( + queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_update_process_delay_request + ) + + echo_log(f"更新流程延期完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(update_process_delay(task_id=2054174091228876801)) diff --git a/dock/oa_dcm/oa_upload.py b/dock/oa_dcm/oa_upload.py new file mode 100644 index 0000000..df7d91a --- /dev/null +++ b/dock/oa_dcm/oa_upload.py @@ -0,0 +1,204 @@ +""" +待办工单附件文件上传。 + +对应文档接口:3、文件上传接口 +""" +import asyncio +import hashlib +import io +import json +from typing import Union, Optional +from urllib.parse import urlparse + +import pandas as pd +from sqlalchemy import select, desc, exists +from tornado.httpclient import HTTPResponse, HTTPRequest + +import apps +import dock +from dock.oa import oa_api_request +from models.dcm_push_status import DcmPushStatus +from models.dcm_task import DcmTask +from models.dcm_task_attachment import DcmTaskAttachment +from models.dcm_task_file_upload import DcmTaskFileUpload +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +file_url_column_name = '_file_url' +""" +文件路径列名。 +""" + + +async def done_attachment_download(response_list: list[HTTPResponse]): + """ + 下载全部完成后处理程序。 + + :param response_list: 响应列表 + :return: + """ + echo_log(f"文件下载全部完成...") + + +async def after_attachment_upload(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 附件上传请求后处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + task_file_upload_id = getattr(response.request, 'dcm_task_file_upload_id') + dcm_task_id = getattr(response.request, 'dcm_task_id') + + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + nas = udict.get_by_path(body_data, 'n_a_s') + if nas: + oa_media_id = body_data.get('atts', [])[0].get('fileUrl') + file_upload: DcmTaskFileUpload = await DcmTaskFileUpload.async_find_by_id(task_file_upload_id) + file_upload.oa_media_id = oa_media_id + file_upload.status = 1 + await file_upload.async_save() + + dcm_task_id = getattr(response.request, "dcm_task_id") + await DcmPushStatus.set_push_task_file_upload_status(dcm_task_id) + echo_log(f"待办:{dcm_task_id} 的附件上传成功.") + else: + echo_log(f"待办:{dcm_task_id} 的附件上传失败.") + + +async def after_attachment_download(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 附件下载请求后处理程序。 + + :param response: 请求对象 + :param retry_queue: 重试队列 + :return: + """ + dcm_task_id = getattr(response.request, 'dcm_task_id') + dcm_task_attachment_id = getattr(response.request, 'dcm_task_attachment_id') + dcm_media_id = getattr(response.request, 'dcm_media_id') + # 计算文件哈希 + file_content = response.body + file_hash = hashlib.md5(file_content).hexdigest() + + echo_log(f"待办:{dcm_task_id} 的附件:{dcm_task_attachment_id} 已经下载完成,附件 HASH 值:{file_hash}.") + echo_log(f"附件下载重试队列中有:{retry_queue.qsize()} 项在等待.") + + # 保存文件上传记录数据 + file_upload_data = { + DcmTaskFileUpload.dcm_task_id.key: dcm_task_id, + DcmTaskFileUpload.dcm_task_attachment_id.key: dcm_task_attachment_id, + DcmTaskFileUpload.dcm_media_id.key: dcm_media_id, + DcmTaskFileUpload.file_hash.key: file_hash, + } + file_upload = await DcmTaskFileUpload.is_exist(dcm_task_id, dcm_task_attachment_id) + if file_upload: + file_upload.copy_from_dict(file_upload_data) + else: + file_upload = DcmTaskFileUpload(**file_upload_data) + await file_upload.async_save() + + file_obj = io.BytesIO(response.body) + file_name = urlparse(response.request.url).path.split('/')[-1] + upload_request = await oa_api_request.get_upload_request(file_obj, file_name=file_name) + setattr(upload_request, 'dcm_task_file_upload_id', file_upload.id) + setattr(upload_request, "dcm_task_id", dcm_task_id) + upload_queue = asyncio.Queue() + await upload_queue.put(upload_request) + echo_log(f"开始推送待办:{dcm_task_id} 的附件数据...") + await requests.async_concurrency( + upload_queue, con_count=1, retry=dock.MAX_RETRY_COUNT, + after_request=after_attachment_upload + ) + + +async def upload_with_attachment(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送附件数据。 + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + :return: + """ + from dock.dcm import dcm_api + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(DcmTask.id).order_by(desc(DcmTask.act_id)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(DcmTask.id.in_(task_id)) + echo_log(f"本次上传待办列表:{task_id} 的附件...") + else: + task_query = task_query.where(DcmTask.id == task_id) + echo_log(f"本次上传待办:{task_id} 的附件...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次上传前 {fetch_size} 条附件...") + task_id_list = (await DcmTask.orm_execute_scalars(task_query)).all() + task_id_list = [f"{id}" for id in task_id_list] + + # 查询属于这些任务的所有附件信息 + query = select( + DcmTaskAttachment.id, DcmTaskAttachment.dcm_task_id, + DcmTaskAttachment.media_id, DcmTaskAttachment.media_url + ).where( + DcmTaskAttachment.dcm_task_id.in_(task_id_list) + ) + + # 生产环境过滤掉已成功上传的附件(避免重复上传) + if apps.get_active_env() == 'prod': + # 生产环境,不上传已经成功上传的附件文件 + query = query.where(~exists().where( + (DcmTaskFileUpload.dcm_task_attachment_id == DcmTaskAttachment.id) & + (DcmTaskFileUpload.status == 1) + )) + else: + echo_log(f"非生产环境,上传所有附件.") + + attachment_df = await DcmTaskAttachment.query_as_df(query) + # 格式化为字符串 + attachment_df[DcmTaskAttachment.id.key] = attachment_df[DcmTaskAttachment.id.key].astype(str) + attachment_df[DcmTaskAttachment.dcm_task_id.key] = attachment_df[DcmTaskAttachment.dcm_task_id.key].astype(str) + + # 增加文件下载路径列 + attachment_df[file_url_column_name] = attachment_df.apply( + lambda x: f"/{x.get(DcmTaskAttachment.media_url.key).lstrip('/')}" + if pd.notna(x.media_url) and str(x.media_url).strip() else "", + axis=1 + ) + + # 处理无待办工单上传状态 + empty_task_ids = set(task_id_list) - set(attachment_df[DcmTaskAttachment.dcm_task_id.key].unique().tolist()) + empty_task_data = [ + { + DcmPushStatus.dcm_task_id.key: dcm_task_id, + DcmPushStatus.push_task_file_upload_status.key: 1 + } + for dcm_task_id in list(empty_task_ids) + ] + empty_task_df = pd.DataFrame(empty_task_data) + await DcmPushStatus.save_batch(empty_task_df) + + echo_log(f"正在准备下载队列...") + # 创建下载请求,填充请求队列 + attachment_download_queue = asyncio.Queue() + for _h, _row in attachment_df.iterrows(): + download_request = await dcm_api.new_api_request(_row.get(file_url_column_name), {}, method='GET') + setattr(download_request, 'dcm_task_id', _row.get(DcmTaskAttachment.dcm_task_id.key)) + setattr(download_request, 'dcm_task_attachment_id', _row.get(DcmTaskAttachment.id.key)) + setattr(download_request, 'dcm_media_id', _row.get(DcmTaskAttachment.media_id.key)) + await attachment_download_queue.put(download_request) + # 提交附件下载请求 + await requests.async_concurrency( + attachment_download_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_attachment_download, after_done=done_attachment_download + ) + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(upload_with_attachment(task_id=2059900525595328517)) diff --git a/dock/oa_govc/__init__.py b/dock/oa_govc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dock/oa_govs/__init__.py b/dock/oa_govs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dock/oa_govs/govs_push_detail.py b/dock/oa_govs/govs_push_detail.py new file mode 100644 index 0000000..7c08fce --- /dev/null +++ b/dock/oa_govs/govs_push_detail.py @@ -0,0 +1,193 @@ +""" +待办工单明细推送。 + +对应文档接口:5、推送工单详情 +""" +import asyncio +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from models.govs_order_master import GovsOrderMaster +from models.govs_order_detail import GovsOrderDetail +from models.govs_order_user import GovsOrderUser +from models.govs_push_status import GovsPushStatus +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +GovsOrderDetailMapping = { + GovsOrderMaster.id.key: 'gdId', + GovsOrderDetail.order_id.key: 'workOrderNo', + GovsOrderUser.customer_name.key: 'name', + GovsOrderUser.customer_sex.key: 'gender', + GovsOrderUser.customer_credentials_type.key: 'documentType', + GovsOrderUser.customer_credentials_no.key: 'idNumber', + GovsOrderDetail.call_number.key: 'incomingCallNumber', + GovsOrderDetail.call_time.key: 'callTime', + GovsOrderDetail.order_source_for_view.key: 'workOrderStatus', + GovsOrderUser.customer_connect_phone.key: 'contactPhoneNumber', + GovsOrderDetail.belong_platform_name.key: 'acceptancePlatform', + GovsOrderDetail.form_type.key: 'formType', + GovsOrderDetail.case_is_visit.key: 'whetherToFollowUp', + GovsOrderDetail.case_is_urgent.key: 'urgencyLevel', + GovsOrderDetail.info_protect.key: 'informationProtection', + GovsOrderDetail.relate_order_count.key: 'relatedWorkOrderNo', + GovsOrderDetail.service_object_type.key: 'serviceObjectType', +} +""" +数据推送映射关系。 +""" + + +async def after_push_order_detail_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + govs_task_id = getattr(response.request, "govs_task_id") + await GovsPushStatus.set_push_order_detail_status(govs_task_id) + echo_log(f"推送工单详情成功.") + else: + echo_log(f"推送工单详情失败:{message}") + + if retry_queue: + echo_log(f"工单详情重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def done_push_order_detail(response_list: list[HTTPResponse]): + """ + 推送完成工单详情后的回调 + """ + unique_task_ids = set() + for response in response_list: + task_id = getattr(response.request, 'govs_task_id', None) + if task_id: + unique_task_ids.add(task_id) + return unique_task_ids + + +async def push_order_detail(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送12345工单详情。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(GovsOrderMaster.id).order_by(desc(GovsOrderMaster.id)) + if task_id is not None: + if isinstance(task_id, list): + task_query = task_query.where(GovsOrderMaster.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的详细数据...") + else: + task_query = task_query.where(GovsOrderMaster.id == task_id) + echo_log(f"本次推送待办:{task_id} 的详细数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待办的详细数据...") + task_id_list = (await GovsOrderMaster.orm_execute_scalars(task_query)).all() + task_id_list = [f"{id}" for id in task_id_list] + + # 查询属于这些任务的所有详细数据 + detail_query = select( + GovsOrderMaster.id, GovsOrderMaster.order_id, GovsOrderUser.customer_name, GovsOrderUser.customer_sex, + GovsOrderDetail.call_number, GovsOrderUser.customer_connect_phone, GovsOrderDetail.belong_platform_name, + GovsOrderDetail.area_code_area, GovsOrderDetail.area_code_city, GovsOrderDetail.area_code_street, + GovsOrderDetail.address_detail, GovsOrderDetail.form_type, GovsOrderDetail.case_accord_type_one_name, + GovsOrderDetail.case_accord_type_two_name, GovsOrderDetail.case_accord_type_three_name, + GovsOrderDetail.case_is_visit, GovsOrderDetail.order_source_for_view, + GovsOrderDetail.order_source, GovsOrderDetail.order_source_detail, GovsOrderDetail.case_is_urgent, + GovsOrderDetail.info_protect, GovsOrderDetail.relate_order_count, GovsOrderDetail.service_object_type, + GovsOrderUser.customer_credentials_type, GovsOrderUser.customer_credentials_no, + GovsOrderDetail.call_time + ).join( + GovsOrderUser, GovsOrderMaster.id == GovsOrderUser.master_id + ).join( + GovsOrderDetail, GovsOrderMaster.id == GovsOrderDetail.master_id + ).where( + GovsOrderMaster.id.in_(task_id_list) + ) + + govs_task_df = await GovsOrderMaster.query_as_df(detail_query) + # 格式化为字符串 + govs_task_df[GovsOrderMaster.id.key] = govs_task_df[GovsOrderMaster.id.key].astype(str) + govs_task_df[GovsOrderDetail.relate_order_count.key] = govs_task_df[GovsOrderDetail.relate_order_count.key].astype( + str) + # 拼接诉求地址 + govs_task_df['appealAddress'] = (govs_task_df[GovsOrderDetail.area_code_city.key] + '/' + + govs_task_df[GovsOrderDetail.area_code_area.key] + '/' + + govs_task_df[GovsOrderDetail.area_code_street.key] + '/' + + govs_task_df[GovsOrderDetail.address_detail.key]) + # 拼接诉求归口 + govs_task_df['appealCategory'] = (govs_task_df[GovsOrderDetail.case_accord_type_one_name.key] + '/' + + govs_task_df[GovsOrderDetail.case_accord_type_two_name.key] + '/' + + govs_task_df[GovsOrderDetail.case_accord_type_three_name.key]) + # 拼接诉求来源 + govs_task_df['appealSource'] = (govs_task_df[GovsOrderDetail.order_source.key] + '/' + + govs_task_df[GovsOrderDetail.order_source_detail.key]) + # 日期转换为字符串 + govs_task_df[GovsOrderDetail.call_time.key] = govs_task_df[GovsOrderDetail.call_time.key].apply( + lambda x: x.strftime('%Y-%m-%d %H:%M:%S') if pd.notna(x) and hasattr(x, 'strftime') else x + ) + + # 处理无待办工单详情状态 + empty_task_ids = set(task_id_list) - set(govs_task_df[GovsOrderMaster.id.key].unique().tolist()) + empty_task_data = [ + { + GovsPushStatus.master_id.key: govs_task_id, + GovsPushStatus.push_order_status.key: 1 + } + for govs_task_id in list(empty_task_ids) + ] + empty_task_df = pd.DataFrame(empty_task_data) + await GovsPushStatus.save_batch(empty_task_df) + + # 处理数据映射,适应接口推送 + mapped_df = govs_task_df.rename(columns=GovsOrderDetailMapping) + # 仅保留需要的列 + mapped_df = mapped_df[ + list(GovsOrderDetailMapping.values()) + ['appealAddress', 'appealCategory', 'appealSource']] + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + govs_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + push_request = await oa_api_request.get_push_govs_order_detail_request(**row.to_dict()) + setattr(push_request, "govs_task_id", row.get('gdId')) + await govs_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送工单详情数据...") + pushed_task_ids = await requests.async_concurrency( + govs_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_order_detail_request, after_done=done_push_order_detail + ) + echo_log(f"工单详情推送已经完成...") + return pushed_task_ids + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(push_order_detail(fetch_size=60)) diff --git a/dock/oa_govs/govs_push_order.py b/dock/oa_govs/govs_push_order.py new file mode 100644 index 0000000..2d3c126 --- /dev/null +++ b/dock/oa_govs/govs_push_order.py @@ -0,0 +1,177 @@ +""" +待签收工单推送。 + +对应文档接口:2、推送待签收工单列表 +""" + +import asyncio +import json +import pandas as pd +from sqlalchemy import select +from tornado.httpclient import HTTPResponse, HTTPRequest + +import models +import apps +import dock +from dock.oa import oa_api_request +from dock.govs import govs_save_sign +from dock.oa_govs import oa_sign_task +from models.govs_order_master import GovsOrderMaster +from models.govs_push_status import GovsPushStatus +from typing import Optional, Union +from paste.core.logging import echo_log +from paste.web import requests +from paste.util import udict + +GOVS_MASTER_MAPPING = { + GovsOrderMaster.id.key: 'gdId', + GovsOrderMaster.order_id.key: 'workOrderNo', + GovsOrderMaster.case_content.key: 'appealContent', + GovsOrderMaster.case_goal.key: 'appealPurpose', + GovsOrderMaster.plan_sign_time.key: 'plannedSignOffTime', + GovsOrderMaster.plan_finish_time.key: 'plannedCompletionTime', + GovsOrderMaster.order_status.key: 'workOrderStatus', + GovsOrderMaster.claim_status.key: 'signOffStatus', + GovsOrderMaster.plan_back_time.key: 'returnDeadline', + GovsOrderMaster.handle_time.key: 'assignToSubordinateTime', + GovsOrderMaster.back_time.key: 'subordinateReturnTime', + GovsOrderMaster.complete_time.key: 'subordinateCompletionTime', + GovsOrderMaster.update_date.key: 'platformUpdateTime', + GovsOrderMaster.service_object_type.key: 'appealType' +} +""" +数据推送映射关系。 +""" + + +async def after_push_order_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + gov_task_id_list = getattr(response.request, "gov_task_id_list", []) + order_push_data = [ + { + GovsPushStatus.master_id.key: govs_task_id, + GovsPushStatus.push_order_status.key: 1 + } + for govs_task_id in gov_task_id_list + ] + govs_task_push_df = pd.DataFrame(order_push_data) + await GovsPushStatus.save_batch(govs_task_push_df) + echo_log(f"推送待签收工单成功.") + else: + echo_log(f"推送待签收工单失败:{message}") + + if retry_queue: + echo_log(f"待签收工单重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def done_order_push(response_list: list[HTTPResponse]): + """ + 待签收工单列表推送完成的回调 + """ + unique_task_ids = set() + for response in response_list: + gov_task_id_list = getattr(response.request, "gov_task_id_list", []) + unique_task_ids.update(gov_task_id_list) + return unique_task_ids + + +async def push_order(fetch_size: int = 50, batch_size: int = 10, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送待签收工单列表。 + + :param fetch_size: 本次推送数量 + :param batch_size: 分批时,每批大小 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(GovsOrderMaster.id, GovsOrderMaster.order_id, GovsOrderMaster.case_content, + GovsOrderMaster.case_goal, GovsOrderMaster.plan_finish_time, GovsOrderMaster.plan_sign_time, + GovsOrderMaster.service_object_type, GovsOrderMaster.claim_status, + GovsOrderMaster.plan_back_time, + GovsOrderMaster.handle_time, GovsOrderMaster.back_time, GovsOrderMaster.complete_time, + GovsOrderMaster.update_date, GovsOrderMaster.order_status).order_by(GovsOrderMaster.id) + # 生产环境不推送已推送过的或已签收的 + if apps.get_active_env() == 'prod': + task_query = task_query.join( + GovsPushStatus, GovsPushStatus.master_id == GovsOrderMaster.id + ).where((GovsOrderMaster.govs_sign != 1) & (GovsPushStatus.push_order_status != 1)) + if task_id: + if isinstance(task_id, list): + task_query = task_query.where(GovsOrderMaster.id.in_(task_id)) + echo_log(f"本次推送待签收工单列表:{task_id} 的数据...") + else: + task_query = task_query.where(GovsOrderMaster.id == task_id) + echo_log(f"本次推送待签收工单:{task_id} 的数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待签收工单数据...") + govs_task = await GovsOrderMaster.query_as_df(task_query) + # 格式化为字符串 + govs_task[GovsOrderMaster.id.key] = govs_task[GovsOrderMaster.id.key].astype(str) + for key in [GovsOrderMaster.plan_finish_time.key, GovsOrderMaster.plan_sign_time.key, + GovsOrderMaster.handle_time.key, GovsOrderMaster.back_time.key, + GovsOrderMaster.complete_time.key, GovsOrderMaster.update_date.key, + GovsOrderMaster.plan_back_time.key]: + govs_task[key] = govs_task[key].apply( + lambda x: x.strftime('%Y-%m-%d %H:%M:%S') if pd.notna(x) and hasattr(x, 'strftime') else x + ) + + # 处理数据映射,适应接口推送 + mapped_df = govs_task.rename(columns=GOVS_MASTER_MAPPING) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + govs_push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for start in range(0, len(mapped_df), batch_size): + batch_df: pd.DataFrame = mapped_df.iloc[start:start + batch_size] + push_list = batch_df.to_dict('records') + push_request = await oa_api_request.get_push_govs_order_master_request(push_list) + setattr(push_request, "gov_task_id_list", batch_df['gdId'].unique().tolist()) + await govs_push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送待签收工单数据...") + pushed_order_ids = await requests.async_concurrency( + govs_push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_order_request, after_done=done_order_push + ) + echo_log(f"待签收数据推送已经完成...") + return pushed_order_ids + + +async def push_full_order(fetch_size: int = 50, batch_size: int = 10, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + from dock.oa_govs import govs_push_process, govs_push_detail + # 推送待办工单列表,并获取推送成功的工单id + pushed_order_ids = await push_order(fetch_size, batch_size, task_id) + pushed_order_ids = list(pushed_order_ids) + # 只推送推送成功的工单的详情和办理过程 + await govs_push_detail.push_order_detail(task_id=pushed_order_ids) + await govs_push_process.push_order_process(task_id=pushed_order_ids) + # 在省12345平台签收推送成功的工单 + await govs_save_sign.sign_order_bypass_api(pushed_order_ids) + # OA平台签收工单 + await oa_sign_task.sign_task(task_id=pushed_order_ids) + + +if __name__ == '__main__': + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(push_full_order(fetch_size=50)) diff --git a/dock/oa_govs/govs_push_process.py b/dock/oa_govs/govs_push_process.py new file mode 100644 index 0000000..2f94773 --- /dev/null +++ b/dock/oa_govs/govs_push_process.py @@ -0,0 +1,260 @@ +""" +待办工单办理过程推送。 + +对应文档接口:5、推送工单处理流程列表 +""" +import asyncio +import io +import json +from typing import Optional, Union + +import pandas as pd +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +import models +from dock.oa import oa_api_request +from dock.govs import govs_download_file +from models.govs_order_master import GovsOrderMaster +from models.govs_order_process import GovsOrderProcess +from models.govs_push_status import GovsPushStatus +from paste.core.logging import echo_log +from paste.util import udict +from paste.web import requests + +GovsOrderProcessMapping = { + GovsOrderProcess.id.key: 'id', + GovsOrderProcess.master_id.key: 'parentId', + GovsOrderProcess.action_name.key: 'processingSteps', + GovsOrderProcess.handler_org_names.key: 'processingDepartment', + GovsOrderProcess.handler_user_names.key: 'processor', + GovsOrderProcess.deal_type.key: 'processingMethod', + GovsOrderProcess.next_org_names.key: 'receivingDepartment', + GovsOrderProcess.next_handler_user_names.key: 'receiver', + GovsOrderProcess.adv_content.key: 'processingOpinion', + GovsOrderProcess.deal_date.key: 'processingTime', + GovsOrderProcess.plan_finish_time.key: 'plannedCompletionTime', + GovsOrderProcess.remarks.key: 'remarks', + GovsOrderProcess.attachment_dto_list.key: 'fileIdList' +} +""" +数据推送映射关系。 +""" + + +async def after_push_order_process_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 工单推送请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + govs_task_id = getattr(response.request, "govs_task_id") + await GovsPushStatus.set_push_order_process_status(govs_task_id) + echo_log(f"推送工单办理过程成功.") + else: + echo_log(f"推送工单办理过程失败:{message}") + + if retry_queue: + echo_log(f"工单办理过程重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def done_push_order_process(response_list: list[HTTPResponse]): + """ + 推送完成工单办理过程后的回调 + """ + unique_task_ids = set() + for response in response_list: + task_id = getattr(response.request, 'govs_task_id', None) + if task_id: + unique_task_ids.add(task_id) + return unique_task_ids + + +async def done_attachment_download(response_list: list[HTTPResponse]): + """ + 所有附件下载完成执行的处理程序。 + + :param response_list: 附件下载响应列表 + :return: 返回附件字典列表,每个元素包含文件名和io对象 + """ + downloaded_attachments = [] + for response in response_list: + file_name = getattr(response.request, "file_name", "file") + file_io = io.BytesIO(response.body) + downloaded_attachments.append({ + "file_name": file_name, "file_io": file_io + }) + return downloaded_attachments + + +async def done_attachment_upload(response_list: list[HTTPResponse]): + """ + 所有附件上传到OA完成后执行的处理程序 + + :param response_list: 附件下载响应列表 + :return: 返回文件id列表 + """ + file_ids = [] + for response in response_list: + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + nas = udict.get_by_path(body_data, 'n_a_s') + if nas: + oa_media_id = body_data.get('atts', [])[0].get('fileUrl') + file_ids.append(oa_media_id) + echo_log(f"省12345的办理过程的附件上传成功.") + else: + echo_log(f"省12345的办理过程的附件上传失败.") + return file_ids + + +async def download_and_upload_attachment(attachment_list: list): + """ + 从省12345下载附件,并上传到OA + + :param attachment_list: 附件信息列表 + :return: 返回上传OA成功的文件id列表 + """ + download_queue = asyncio.Queue() + for attachment in attachment_list: + download_request = await govs_download_file.get_download_request('1773611023340371969', attachment['filePath']) + setattr(download_request, 'file_name', attachment['attachName']) + await download_queue.put(download_request) + downloaded_attachments = await requests.async_concurrency(download_queue, con_count=dock.CONCURRENCY_COUNT, + retry=dock.MAX_RETRY_COUNT, + after_done=done_attachment_download) + oa_upload_queue = asyncio.Queue() + for downloaded_attachment in downloaded_attachments: + upload_request = await oa_api_request.get_upload_request(downloaded_attachment['file_io'], + downloaded_attachment['file_name'], False) + await oa_upload_queue.put(upload_request) + uploaded_attachment_ids = await requests.async_concurrency(oa_upload_queue, con_count=dock.CONCURRENCY_COUNT, + retry=dock.MAX_RETRY_COUNT, + after_done=done_attachment_upload) + return uploaded_attachment_ids + + +async def push_order_process(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 推送12345工单办理过程。 + + :param fetch_size: 本次推送数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(GovsOrderMaster.id).order_by(desc(GovsOrderMaster.id)) + if task_id is not None: + if isinstance(task_id, list): + task_query = task_query.where(GovsOrderMaster.id.in_(task_id)) + echo_log(f"本次推送待办列表:{task_id} 的详细数据...") + else: + task_query = task_query.where(GovsOrderMaster.id == task_id) + echo_log(f"本次推送待办:{task_id} 的详细数据...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次推送前 {fetch_size} 条待办的详细数据...") + govs_task_df = await GovsOrderProcess.query_as_df(task_query) + + # 预处理数据方法 + def preprocess(df: pd.DataFrame): + # 格式化为字符串 + df[GovsOrderProcess.id.key] = df[GovsOrderProcess.id.key].astype(str) + df[GovsOrderProcess.master_id.key] = df[GovsOrderProcess.master_id.key].astype( + str) + + # 日期转换为字符串 + df[GovsOrderProcess.deal_date.key] = df[GovsOrderProcess.deal_date.key].apply( + lambda x: x.strftime('%Y-%m-%d %H:%M:%S') if pd.notna(x) and hasattr(x, 'strftime') else '' + ) + df[GovsOrderProcess.plan_finish_time.key] = df[GovsOrderProcess.plan_finish_time.key].apply( + lambda x: x.strftime('%Y-%m-%d %H:%M:%S') if pd.notna(x) and hasattr(x, 'strftime') else '' + ) + # 更名,并仅保留需要的列 + df = df.rename(columns=GovsOrderProcessMapping) + df = df[list(GovsOrderProcessMapping.values())] + df[GovsOrderProcess.master_id.key] = df['parentId'] + return df + + # 填充处理过程 + await GovsOrderProcess.fill_process_info(govs_task_df, column_name='processLogBOList', preprocessing=preprocess) + + # 处理无待办工单处理过程状态 + empty_govs_task_df = govs_task_df[govs_task_df['processLogBOList'].apply(lambda x: len(x) == 0)] + empty_govs_task_df[GovsPushStatus.master_id.key] = empty_govs_task_df[GovsOrderMaster.id.key] + empty_govs_task_df[GovsPushStatus.push_order_process_status.key] = 1 + empty_govs_task_df = empty_govs_task_df[ + [GovsPushStatus.master_id.key, GovsPushStatus.push_order_process_status.key]] + await GovsPushStatus.save_batch(empty_govs_task_df) + + # 过滤空数组 + full_govs_task_df = govs_task_df[govs_task_df['processLogBOList'].apply(lambda x: len(x) > 0)] + + # 删除 processLogBOList内的工单id字段 + def remove_govs_task_id(data_list): + for item in data_list: + if isinstance(item, dict) and GovsOrderProcess.master_id.key in item: + del item[GovsOrderProcess.master_id.key] + return data_list + + # 执行替换 + full_govs_task_df['processLogBOList'] = full_govs_task_df['processLogBOList'].apply(remove_govs_task_id) + + # 处理数据映射,适应接口推送 + mapped_df = full_govs_task_df.rename(columns={GovsOrderMaster.id.key: 'gdId'}) + mapped_df['gdId'] = mapped_df['gdId'].astype(str) + # 这里把空数据都换成 None,以便存入数据库时是 null + mapped_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + push_queue = asyncio.Queue() + + # 向队列中填充请求对象 + for _h, row in mapped_df.iterrows(): + row_data = row.to_dict() + process_list = row_data['processLogBOList'] + for i in range(len(process_list)): + attachment_list = process_list[i].get('fileIdList') + if not attachment_list: + process_list[i]['fileIdList'] = [] + else: + if isinstance(attachment_list, str): + try: + attachment_list = json.loads(attachment_list) + # 从省12345下载附件,然后上传到OA,获取id列表 + oa_file_ids = await download_and_upload_attachment(attachment_list) + process_list[i]['fileIdList'] = oa_file_ids + except Exception as e: + echo_log(f'下载省12345的附件并上传到OA时遇到错误:{e}', is_log_exc=True) + process_list[i]['fileIdList'] = [] + else: + process_list[i]['fileIdList'] = [] + push_request = await oa_api_request.get_push_gov_process_request(**row_data) + setattr(push_request, "govs_task_id", row.get('gdId')) + await push_queue.put(push_request) + + # 并发提交推送请求 + echo_log(f"开始推送待办过程数据...") + pushed_task_ids = await requests.async_concurrency( + push_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_push_order_process_request, after_done=done_push_order_process + ) + echo_log(f"待办过程数据推送已经完成...") + return pushed_task_ids + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(push_order_process(fetch_size=60)) diff --git a/dock/oa_govs/oa_sign_task.py b/dock/oa_govs/oa_sign_task.py new file mode 100644 index 0000000..df3f889 --- /dev/null +++ b/dock/oa_govs/oa_sign_task.py @@ -0,0 +1,95 @@ +""" +向 OA 平台上报数据已经完整推送。 + +对应文档接口:8、签收 +""" +import asyncio +import json +from typing import Optional, Union + +from sqlalchemy import select, desc +from tornado.httpclient import HTTPResponse, HTTPRequest + +import dock +from dock.oa import oa_api_request +from models.govs_push_status import GovsPushStatus +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 after_sign_task_request(response: HTTPResponse, retry_queue: asyncio.Queue[HTTPRequest]): + """ + 签收工单请求响应后的处理程序。 + + :param response: 响应对象 + :param retry_queue: 重试队列 + """ + body = response.body.decode() + echo_log(body) + body_data = json.loads(body) + code = udict.get_by_path(body_data, 'code') + message = udict.get_by_path(body_data, 'msg') + if code == 200: + echo_log(f"签收工单成功.") + else: + echo_log(f"签收工单失败:{message}") + + if retry_queue: + echo_log(f"签收工单重试队列中有:{retry_queue.qsize()} 个请求在等待.") + + +async def sign_task(fetch_size: int = 50, + task_id: Optional[Union[str, int, list[Union[str, int]]]] = None): + """ + 签收指定数量的工单任务,或签收指定id的工单 + + :param fetch_size: 本次签收的任务数量 + :param task_id: 待办任务 ID 可选 + """ + # 根据条件获取目标任务 ID 列表(支持指定 task_id 或分页获取) + task_query = select(GovsOrderMaster.id).join( + GovsPushStatus, GovsPushStatus.master_id == GovsOrderMaster.id + ).where( + GovsPushStatus.push_order_status == 1, + GovsPushStatus.push_order_detail_status == 1, + GovsPushStatus.push_order_process_status == 1 + ).order_by( + desc(GovsOrderMaster.id) + ) + if task_id is not None: + if isinstance(task_id, list): + task_query = task_query.where(GovsOrderMaster.id.in_(task_id)) + echo_log(f"本次签收待办列表:{task_id} 的工单...") + else: + task_query = task_query.where(GovsOrderMaster.id == task_id) + echo_log(f"本次签收待办:{task_id} 的工单...") + else: + task_query = task_query.limit(fetch_size) + echo_log(f"本次签收前 {fetch_size} 条待办工单...") + + govs_task_df = await GovsOrderMaster.query_as_df(task_query) + # 格式化为字符串 + govs_task_df[GovsOrderMaster.id.key] = govs_task_df[GovsOrderMaster.id.key].astype(str) + + echo_log(f"正在准备请求队列...") + # 构建请求队列 + sign_request_queue = asyncio.Queue() + task_id_list = govs_task_df[GovsOrderMaster.id.key].unique().tolist() + for task_id in task_id_list: + request = await oa_api_request.get_sign_govs_task_request(task_id) + await sign_request_queue.put(request) + echo_log(f"开始签收工单...") + await requests.async_concurrency( + sign_request_queue, con_count=dock.CONCURRENCY_COUNT, retry=dock.MAX_RETRY_COUNT, + after_request=after_sign_task_request + ) + echo_log(f"签收工单完成...") + + +if __name__ == "__main__": + from paste.core import aio_pool + + _runner = aio_pool.get_aio_runner() + _runner(sign_task(task_id=2057694366475214849)) diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..e329f10 --- /dev/null +++ b/environment.yml @@ -0,0 +1,53 @@ +# environment.yml +name: d3i-env +channels: + - conda-forge + - defaults + +dependencies: + - python=3.11 + - aiohttp>=3.13.0 + - aiomysql==0.3.2 + - aioquic>=1.3.0 + - aiosqlite>=0.22.1 + - aiofiles>=23.0.0 + - cryptography==46.0.3 + - matplotlib>=3.10.1 + - matplotlib-inline>=0.1.7 + - numpy>=1.24.0 + - openpyxl>=3.1.5 + - pandas>=2.0.0 + - pillow>=10.0.0 + - psutil>=5.9.0 + - PyJWT>=1.7.1 + - PyMySQL>=1.1.0 + - pyOpenSSL>=24.3.0 + - pytest>=8.0.0 + - pytest-asyncio>=0.23.0 + - pytest-cov>=4.0.0 + - PyYAML>=6.0.2 + - requests>=2.32.5 + - selenium>=4.38.0 + - scipy>=1.14.0 + - sqlalchemy>=2.0.50 + - svgwrite>=1.4.2 + - tabulate>=0.9.0 + - tinycss2>=1.4.0 + - tinyhtml5>=2.0.0 + - tornado>=6.4 + - weasyprint>=64.1 + - WTForms>=3.2.1 + - pip + - pip: + - ddddocr>=1.6.1 + - gmssl>=3.2.2 + - javaobj-py3>=0.4.4 + - jieba>=0.42.1 + - jpush>=3.3.9 + - redis>=5.2.1 + - opencv-python>=4.11.0.86 + - pycurl>=7.46.0 + - pypinyin>=0.55.0 + - seaborn>=0.13.2 + - tornado-swagger>=1.4.5 + - tornado-wtforms>=0.0.1 \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..d55df60 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,17 @@ +import datetime + +import numpy as np +import pandas as pd + +EmptyInDF = ['null', f'{pd.NaT}', None, np.nan, pd.NA, pd.NaT] +""" +在 pd.DataFrame 中,应当被替换为空字符串 '' 的数据。 +""" + +EmptyDatetimeInDF = [ + datetime.date(1, 1, 1), + datetime.datetime(1, 1, 1, 0, 0, 0) +] +""" +在 pd.DataFrame 中,应当被替换为空字符串 '' 的日期时间数据。 +""" \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-311.pyc b/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f86e1a7009ea869515cef07e642fc43ec4130dc GIT binary patch literal 540 zcmYjMy-UMD6n}TQ#I{9! z@&g?N`-fEMP;L^O+yZV+UTP!0_wL>M_`UbLyU*uyAm#NLSr-W4BS})pj)SiU8C(Me zifk}`Z)D(6t{8-3Ml3ZQx{)Y`EiKJuZ{lD4hdb7cOcpROuu>kNN}_iPz>_?`GD4)u z7$KRZ-bs$9Kb~W3!L74Pm-H z(s!8Yr#6IQKehLKF~YZ6=de5XfSmq7?hbX&$}N6eT^m6RsMtfxXEbS=^v{KmxS8Y<5_cB(t82VVTK{PJyzeXv3(}9j z`|j>D?>o=;GxPl3cW+y*76juV^V0LsiqJE>m>>Go{H%(>)AAeelzUli{dC$f2LZ%YzO|G4FAS{09M!_qtOeE9Jt|; zlj4w*<|r^hJ9WI?jLeA3=JdRQGw?>CZN@RmfpF~2TtdGFmku>Zp_%VW(tV z(2CDomIwN-MmC2ooQFdm5nD1^V}AbY~tu9l9Qn47+EhHb^%k?3Hz4#Ts5x) z*=9s4yd|w!;7NH4bJ^Gfmiy8b%Up4~CK9EP`|ot-BU%#8+66C5qH!NbEoRwRhdw&i zV#(05NChiuZIR89>;>+^Ohyst-{nKL$h6GJx_*5RWYL$dcV>?86zf*BY*{Ajl+7#T zyK>~~R*VqdyjMjy?;%8W)XREkP(yPIJW$e8q%ZFe&wM_dxc5aO+?}|7L%sOLT<7CN z=U}39VD|B~*_$1S$e22Iadx~vasCgrwU)FZR(|tY>u2%NGwQ9jnb2AFZdg4t1nmnQ z4%&(H_vU*Si{kMXh-+@hyN5|EHi}8^Q_BXd}+h$kW zugu;)8y^};gd>U0c1;lrkN3mPnL8ukf2O}DKJ>?>ZnuLsNJ<^)QE%NQ_TIL~$IoMk z(efk~GSt&q)&^aI%V!7RskP5tZ=LP=OsfPaYxypfOruk+efCe^&OSZ|k`QHl==>7( zxy!gdX&JDVxc4wVb~QfKI&*DM9ep%2dRJ{9P|u#3`)#j!e|Yx6&85CF;jh-7=(?|l zdb9vjg$?5(r)(2cUHq^Y7b=zLA_T3qtK+}3H+YVF{F40@^-F{wX|=Zl+%AMB@KUqj zhwRB4qlPXlFV#WMETFN1=5nCZ?l1y|I{~AdUAWIz3>`5^TERgqGBQJMW z9}ak%eY{wGBp@_Ze<%b_@NP-0{_vFaX~17yw|#%L5AWb&wVUjeELm+0o)SvH(rMeG z{d4Q$h%qvWe+GPQMM*m{+lC8 zi%zD3j7$e9nSo=CUZiE+Ap~1V`TzoM(nAJmyqWYfLcCdq< z*H957OIjiYEsJvfPuQpd*8dqiYK2T!O~CE$U=+RB9OMPX2*+9sW&|S~Mnxy`-bP^q z$SfeIVj)OE$5YSIjKy_PRuoWkn3O8c?+gGm0w963bLiGIS7>;FVZeMv43osbKeD7itGDDhjlhOG_or;@xn0tOu-bO&h)_!W6++NC{% v9}7Q%C9aS7F_0ufQB(}AZY7^-v@x3bO&X3;6eRI#8ge`*obOlQ6aW7LIOER2 literal 0 HcmV?d00001 diff --git a/models/__pycache__/db_models.cpython-311.pyc b/models/__pycache__/db_models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c4484b77f88931d01fb7c726a41566e336dfa30 GIT binary patch literal 155869 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_apply_delay.cpython-311.pyc b/models/__pycache__/dcm_apply_delay.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b74b0303a74fbe1f11d748f8e26d70c29e73b439 GIT binary patch literal 27800 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_stage_reply.cpython-311.pyc b/models/__pycache__/dcm_stage_reply.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4019d69372f1c665aef14fa8c1874de2da568185 GIT binary patch literal 26221 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_task.cpython-311.pyc b/models/__pycache__/dcm_task.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcd80f52d2688dd4095dc7e137ff6dad39733cb3 GIT binary patch literal 38387 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� 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_task_extend_info.cpython-311.pyc b/models/__pycache__/dcm_task_extend_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..708181f6040163440d2642e2dd88c6120bbaac30 GIT binary patch literal 15911 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 L)3Xani}Qa0VQlZ8 literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_task_file_upload.cpython-311.pyc b/models/__pycache__/dcm_task_file_upload.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c1b274b18d0d8c4a0a9de2c84c83aecd3bedec6 GIT binary patch literal 28591 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 TS9%2dXH4p~PfUD?c7gu~!>C+f literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_task_more_info.cpython-311.pyc b/models/__pycache__/dcm_task_more_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad2692c1ebbda4834a17655e2057da93dafe794d GIT binary patch literal 15423 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 literal 0 HcmV?d00001 diff --git a/models/__pycache__/dcm_task_process_info.cpython-311.pyc b/models/__pycache__/dcm_task_process_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8bbc76006df66f9bc843bdbe64d52c5baf21e4c GIT binary patch literal 40594 zcmc(|d2k$6)+buKs-)7sT5DOdEz8KVWO+k)!L}^#HsA$YHV9c+GP1RiRbp|=AZWWO zyA231Z8aEN<};vfj1gNSFf{7!dGj1GJrP-ooRFp_h#}tRlI4HiCm#L1Av)&8_kQQz ztgOtck|p|MVp64>H}7)pxl5jV&b{ZJd@ngU(SS>O+IGw*8VvtT58=zEcz#GT8VnZ< zfxvsjqDJ-Z{PF^Bmb?YSL!jd{$U z*gmHtzcHWrliCY93L6WJ29sg4L2#Zj2+8ikcg+UF`}pTqV-br>L0qak$DQ@A1+n;7 zVvF5LEItkK>F#2E-glAc{cuu4V~JqgW2nfuO(DA~%t7nE&bF@3pnZ3@7x(7&pnYR= zdwcVdc6Ws-X!E$+-7VgrO~Bpjt}q63MEB7)kJl|Wd7fxKdKCALE<^%Q(jKqa)_Js{ z&D|~pllQux^sX1(&HT~1wbScHV)g_mwjJ)yquxj1OWN4g(b3hplK}zvz4f(i^(`HH zn?1+x6T4d69?#a!)-EKpyIUUZYHIH~3V-5#%}3ihn+ah&Iaua~F0rFUttLDP%EJ!> z7z)p02)GT6M#0c%5{!*zlfi8fOz&c7ypMl=HCkDyIV#k~LM>6D2`tna6>3MQL!caq zLV{qw>Uh_L67bKjqsFU=`jAF+956|Wg5zXx$W0WIg_K#qNoF}xS&lRzeHJ-VSdI)O zN9HVY5Oj->6@_aW3(bxSO=r+^81w`ocNWkI6N``+1t*iinWMnTp9P#Oq|FuzSdK!W zXcjpL%WR=o$x$+k9JwrqOUW^J7CG{e)*+O#90@|%EOJnt9l|`8BUzX~iyZk#YY`Si zwIH>GLnv3^ESv?LLYAX~muTetdub?f>| zlixf)d3iAO!fUsGe{S-_XK#IU{a?QFg|3|oogSPR@P~#@x#G{BuFwY;Ca!(8O&Sf2XB7)B8L-tDHJjB+7MEPF1>i`+U3Q!e>r&j!eHo+r&0XP>(@hP zFKh9*_4=vFYeS*0Ut2Qq#-|g%8RB>Wz{CsJs1SxXD^-ghj}LwMDw4arO+sy3lhD%9 zqxg;nsUYw*>_EERG-d+Bcyu-@Q5f z!sIW%p8UgUbVN;cH88w&^<8jD=8onkg9#nrRho|ykCga$b9GI{g_?`l!4SoJG zI+PbcLxLf8bNFNQ`0b?xureyq#7h`VL$_nVE>|kWp`Q8O&98qO7b*v+iJuYRT&fbU zuJ88~AH2=MPWV0nz7rq5f9t(hfGq1@o+|XwTN8cn>DnoDb`Z@NVx7hNiNVtb$JR*g zCsri|1J)DKWjiAn`k%l3_COCqnR)L7mJKF`$mECb-n#y)iBm64zV~d~zo8?0$_ONT zj)kyd`SRLuA5zhwE6-COveZ2lQ2}?(UWnAUe|zfY^*``IgeKj#XZNlN|5u@}UZQ3t zaM}cS21hjtef?_a%nM3ayztu1uU}w;60`?i-UYEhB5ln;u(XrUp1<|U^@-nn0aO?W zZ9puD$hb}d7QFnMUw(;kP^^9C7|>Aff1l$-)m#*tN)w|E0bqFRz?=ReA<9m z5RrNk@wt8Zi(79HB_iU)i4Q1q=&fHXgI6J*zd9Yd_Qk}@1C#GQ%ez+-5L<+Hbj$(V z&3`}So4V?y*b>+BU#&M17`Pn@f^w<8lj1?!LfCUj9(fH+8lV*da7f? zro%|zTUtO09a|CwYc|A?I_%~!Cip&O+SXPsI`~eN%ZvbG@And=y{qDf$AXHk9xGf+ zDu`K7P}Ky-0*W*^y*E6E(sSWyj!5(b8#p(Iznr+(7wUgeZ2`RP)fiF#^3}N-B*9C+ zt3_QILN+V@iCX@EB_zXZ#tVjCqu9}F^dl(t(ys>98y-4v7|RnUEhnvB4a{DvS6y8) z81zdPVn(l3OcRW!487KRgC1V5vDdO1g`TU7f`%oS@D&9ux7X^|lod{eQsIrPTd9x_ znhaj5t#av89aGCedQ+>Z&CqMUVtmm^>ug}QO}kpqMCwI}X^{LtoZkh*QA4lcp~WYS zUQL=_qgP$MiD`%|G_odEg&T!>r?J=gvgusa9>ebpSSL$t^R#t(yv?00?x5M@6@ykd zZQbA_O;y!F2m#j{gVA%?LFWsmps@d9$$jhuwz(?^DMdVDet3QolEL#w=Q8z zZ=NM_dFZO{r~aO}x*cnEEnOYmYfmh#k6BeCF{4W1yQ@~WceOOPd)C4e&a)qgc$UHG zGf49vQhxV8@LAn(^Z$_CA@{Wh~`p!FRgka?G!dDoP|oW5+x#%|-h zarPfZ|`m*u)cfEJ`?ZcNJzVa|n z^y9ca_XX>jgmVe!QU~n*GRaVu? zz0>K*+lKNmXuWRAmO?@8Z5?d@0&y*vLC-xNx4_f2JlZS!QCvh zw{^N%crcX`Vgz6$c%LHZNeIBoE~}&ZKnMh_-ZpQ$TckyQT9WoQx3ve8k2JR&@99P@ z*rTn(EwnXbbo6urJ5~WF^R{$#dI30Sf3&Bgxsz3%`&b%>*o8)9-UJ%36z^mn?^E5Z z&isj>xI4YU92TdHTh<6@505vP=IJ@26J>*0M|wI1z}B~5x)Q2s#55&{wIgf73?)Qq zMc%ARn7$3uJ&$%h(WI+qa<}N_A*@!Z^r&sv6bjI^KsZ5DS9crR5lHXE5N1B3y| zWI?Q{x|>C%c~c^REK!!4Pur=;(-bD{pX%yS=@sslq4{yWk<}doO>D zi8zbo5KrJK2DgoOHT*YavRdtUYvH6BQu2L=rTj&K|;~k5p1Sk~5NXW8;(o`vhA}xNR_QGkwo*f8#d?l2<0i^kZ)~8f zbVFXcA#kH{Gu?8wP!wFm!0p>LK4;!Qfi!=Ebl-mZ9eN0f9yRW#wD%hyq{MVXYPuot zKH~wp~u% z(IqYG4lL`Imvu|Ek4a83;1p%2*tcmsIp6n?RIp@t|M31BR_VcBY0b&Nnv?RHlhW#^ zrQ~M<$33)@>p*-~tV}2wBVeCwnu6!$mmUzMV=r zcaJGE$Tx5saw>8ed-AD;H80#q|-H?=S2wcyHUltpEa1jF+ z12?t6*E_H?kh(}tUDUT_90(OiMQd)XxUoXoe-If3<9(EWhw**_N;hPs8v?f*AD~;t z9*TmC7`S+)(hC0ZYN1ttQ~N+&AZ3A^vY>DCcxFleYROd{$gGhwYk+d5tA9=S)pDc! zhxu5l=PxYXfC$|Xw{f}`wrJ77L(*Lh(%we;H3>-DWqgRT>@z-0^`RT!&<%lmjZJhb zeuSdnA_lHx$sf<1*MCx)zhb0eq~S&nk~Hy=u!)ZZx*;vy5LeGfLLM6la1jGnvSgvs zg$oB(OAFTq3f9R5>)5MSHjpIETQ$-;(kk7*mqAvC6Of=$h5X9kYT(^8hxHO%#K5H& zG`F<>7gE{Ek^LheA`bw}L%c~EjEsoU4Tf4e_rxM8?q!~>l6@e#a+ zy_|3X3@##8PxEr-%v^lu4N2*S0`BFlmcv>NE@I$|+cc@*sA1Ryp8?)KSUKzR}E!_~;VBAZ$+=*{wbNhElb9V%?cFI{h`*u!Q609{-hM(XtCYdn| zEnpbbD=aSla0H^2UJsznAeaR6t46^hSmD})1h{s=0XI=dg6kBL;id?waIvorH$%vT znWl` zMR4yDD&Z~`s^BgWs^QiMOX1cE%it~-R=`~;tb%*DPzU!OVKv+}!dkfNgnQwx7dE_V ze8(tk6zYXdLW8hb*dlBdwp~nk$Jl6b7~EFD?6$cR-1b%zttj$K*d8a$5*yZP6Ly@k z=!NY@2Vm}WC&s3Zz;87PyJGSsA>VAvwFtXo@;j0LkZ}JgV-!Bg2!G&Il=R)0g3vwg z)MIJkIs>P@F)-8JR)>N9gv0vO!h`Mv;Q;;*x-;CFnwr^###7cP46=m%SY~)A-k#ou zW{y#Jrf=fnnVZ8e#@ViWV>opB6YQ$p?t7hf z8TdxbZ@!rLlI0n zV#Ku+nvB%CJMKc@WdcA z&|?xkZsa1PxS2u{n5={%x6qS=J+bs#S#}bN*yA>Ow2^p-wA<++kskI!j6}c=3P_>= z0uD)1+({vs6oP7n^__OmI>|1INr6DAi4b>Fpdvj|{laa2ABCrK;V{zd_cIVYlvx*4LX#jwQXs22Ef)nz>|C#QN6?AKEhKPL zig4s^MZ#o~BN8ivDVRo*;{#~O`ygDh49;)}dtwqP(ntC~m_%YFf*K%HRwRfI0tN8^ zIS0vUBO%zt6tZ@6Io6_#&% zt+c?|^FNS{Q5aecoA{@`iA(SBW#!NZ1EGt9w|{$uZ|(6d^U#&w&{q5DKis_jn^6C` z@YeZ+kDl}!?dp!dszX2})L|W|rRf0C5&c$Je!KipBsrcd(Qt5W^BdJ8Uw@2(m zJVSNzoAaSJ-cs=V@QcZhE@J^4TkzW0U0t1SHUF*(n@F7*v_A2uTXc)f@U!mb6fNno z9=G^ZQ)`>(@mAP^=9YF(bP^j@(2ypWfQ74;E>Z9hII5=0#TF!jN$w}{W;S(sQNW4P z*!4vs@>QBNuOPqY2%Jg$^%?#(!;q0TV1Bvae8E_H>1cYXzxK+C!3W-6J(T%r{_x|Y zHJe5}|KWtR@6hPxLxJ?ea{6JPZM?{Jsp<8m!To`vWpdFnDL4A(OBheh@ue`{CunXH zJT2--M8PzY*ytXI=jRyResxR(pnCNh^>2y(>7)r`S{?UlCU|;H`jpx+9sezO7rEY7 zL7X)An%TIg2}T_W6aFuthqASvZX(=+Y3(WJSA+#UgrGHw#%jGOoB-mV*LxxDVmxYG$4s#@}~};UWFv; z_k+-((+!m<#a!)B0nxOuVL+2uWEe2=r|oiu6hd9*`b(USU~i3&7ClkICYyeESQ%;I z!-J*}MpMKy@Ksp(NQi{ea1hVI@zC2xqd*x3_BIa&6DY`ul4)p1IKd$0ra5f-0Z=!T zGg`KCIPWC8zKDiS%Nxj#glO)j+uaunVJ?cYCkNkAT=qFlsmWB6A-} zQ~D0oKcRc4g9r&MDy;=+)#0MWq1W7N5o{NuXorzzT$RckQ|C}20pUc+*%cK|TSxK2 zsV!o{+c3{!`;k_pm8&<3J2*yr6YMe(Hc8o!%3Uq=ruiV5LXF_ z?7pnm=2!EEbI~18Xj6t=y(YmCkwA{K8b-w@YT_BE64jb;vav63n?c*s|uU((gPuc`A7xh&X4hg8MDvfC~L~}U)bZurF&YJ31?S9ey<| z97lI;-}$q(F=4+R>!=c`4mshJ+Iz1(jS6jaHO3 znO-TerA#h{>X;&>MlzrOaN^vvTtW0-zIt5^Cqlbe;YBN5B*C#Ay-HVPo2$~LZuC{U zbX!tUP$pOz6N8s;eR4K*?u*-}UqDI3=F)CyCxn?wDk!{7S?OXsla(%#IBLau2` zNGbd;h*BMwUw>)h;+Hi~L+=C=*)(}QZ719)M$ zi$GXjxb^WLZ(Y8|+c#EfpsGR+j`U=mOPhhK^XP$7@!Cc-0Lc`usphPhS`rp6BCpSFnkPjf&_M?arZ?j@Jy)2-0g8Ja&I!x>U!0sD zw6?b$hw=$6T2cGcK;qXmPuo^B|=xcO?E8E*kw<`UNvDztzUVMT+<9(S|Y@@Ny2cs=61hz%-& zZcA4WmX~Or4cbsGkR3uE3vxkQS8J;W1t;-)6V*59)FA5#+9CboY~PWmf);v*!!VmY zO+stX>h9?FLP{3htTI6x*Fy?AHc|N2&iaO6I`g6z+|)u8RN-W7a?qhtL9vqRqBd|a ziP2SvPiSB+QY9$#5_&Eo^cz@>kc>2g6iMCLq(K`|-O4Efq18iN6A|C=rAT5*gB^jS zKCL9CN%Ik)cIS*GA&|3K&e`ls8qdnV zuv_B@i)WM`r5bom#I zJ5$f?mYpU39P({Fw{u_xJmJ_Whc!3Rmo$}P$S8X)hp!>X>80bz#lAgqa6UWot*% z)~e6<$YrZX(^h|PHRq)K%>c*OFqLS?%*Ap;QWhyoRg9OEUV7&BX9gb+l&p|TR!DQA zf4*(s=D7U%ft*S?r*bT(b~LAUX#2>VK+bkKXS;9nc)sgWNN{;JXZ zRl~XR-8%yLJLUYHzU{v4-|;lRt-4r+hur+J-15=f@!` z+U)_?_8SjK2OpB0B?0Hdvh!id`S5qC`2(A!R48?oaVeXz;>4` z9?L2p%_<*s1hSUOSxdhwSs<4z9WS0Q7gvuL&6A6&rV=a#DbPEFlVDFvoHB67k;sNn zFoBUjp;afklky&0v3U}G`n=)!1Vwx|9zz7b#0Bw1cD;x(n>57G_ZrV=mbW7#38Csp zii953Y&rUA+{avcERLySxnC8;wORD3vAk&Vl+OlpDC|;8#aw!LR6nmq97CN{L0(&H zZC&+(kDn%=m`j^CzVOYos!h>a)%tf>Tk14WOwi4q0m=!`4n5qH<}(IAoneW&dSh4K z%<7T=R^BvY?C0(UQWI7#@7%D@V9@$|E!uXCjJe1{2ul%XR7O(h7bsYIwa4Q@)Th_C z<5?`P#9z4Z8GI0i130B#7dCPER}*jjfn?NgPT#(KJ#_9gj*Y26 zeHT=O>s0xHt;OeaRH!hB-CXR_0uUhZ<;3r-iK_NX=e3)R1@*M-T)}zv4JT66SZKNL&M(QGaI=UGN8u-5v^4R;th1W-|eg0n>&sO&1+Ar z_^I8-^UX)KJyhc#`wT;wLwi1L{PbbDrhcU5M%iB%{k2MNI4D&&N)I2Gs*m^arV*(N zaPHaCjJ=KcgYf`TP&1vuPT(oy^5=SADgU=0`PqcMIO{crN z{izB&--5b9vDWTpY;kh|+M&{+U{VL2vggl1he~He8F@U!WIaNZ%jLS44WnSP?&Ur) zZ#HAeO6_IB5dZNmfe4(Q zH;{QT|6=iA=GFYG#ZulXIqmMTw5_9QTLWp^W%@lZmUeJ7?O-77keqhNXB|(40ICJ) zU-{nZw>3cj)yDQqFWUU(D~W>}uWk#ZuawhQj-{^|ODpnc}}U8kmr%} zJicT!W^VC?j#r>@dR+GtJ^dYl9Iu??r4@>Tf$ehE!m+Hyqgjj9MG9BNd#P`yO4U8O zU++nOYQXih?0VYg=r23J;QXS1y@((j8rUIcRg7g-jb>E^va02*YSb|`Z=gaW?UKF^ zQ)yxcWIR|9!!Ee{NvL3uiKwI&I^L!n^C7h%B{3GNT2y8Y6{9Ry zMPX-yD4;UHXn*yJvpcBjAe~J(lMoRF`i-9CGYRnpA+NfhghME*$e{~W>&*6+u&aF4 zq8G7@hBFDMC#`R?>sWf&r|C&`pD-LpckhD8lfK^5K_u3g#i_0Ovy~PH;_OO0lOR}m z%J_SWg7v78wHoO0`zqUEGut zSh_I@wo_3%#K>zVoI5Logi{7@O(c)EHsZgtE>W~_9EN!&0Sv9Z2X?k10!Zlc`=Qfh zzy`)`V6z35U=-=YH>8yebg~4uj6=*f+kRsPXf8rlnfM$waQx7sorFaTY))!`a)Svv zD9Tn|H&*nUJrVgKEDJ%$4Tg0Xc2QD_%G4_mXs1X+HL=-*-O{gK3vYfxZ0eizUB#|b z?DiQ~%_9IBN{f#VrXD=ugO)i5kL1FBrd276_TaAsli?!Muqhpu<9#wu!afg{>bF99cDV+nYE) z+Kr9L=tfr|?-KnOWqZZTaD#T!;F;dXKff@9ExS&b*Q_c3yRlpxL12X?Xyr;{)Sxz;)B`Z>?=Oda3PWtr*AUfjz|4twK>h_8A5iUaES%3LMpL zxpJKZ1+$7RBkTDcZXkcFoWFG}|NhbZ`=tjC1@aHe`G*y@Sl7y`^hESokx?xtjYkcT zOQFWfrMW_?U05n@ik0hD|07j`?iD)@zn?0VhY}!}meup$V2yu+`oLtqihRb)i{nTr z{#SBn>cFXe+O>mz0d>rOqhJ~~oJ;?|$)~dF;wSX@DLJ2!^9OR8$)OGm+Mv9QA*ccb zf%FY>`UWVyl`nelnYW)AdOT3RRxV#F&5i!~?Bl7q=brxFknY%o&6}(-XW^)`@Xh^l z(c%wU<*J52QNs1p zXV%YJhRZ){|Fk_&y-}{-h@e1Oy$(%NDD5&QF|!OEZkVYMM0(Zv_=Y~MSA2mU=_L}+lk*#LXeUSfKgo%_ zXE*5){Jw#|27f0X5d?-6lTgJ+=#iYm`1hPbL4E4ql*vM8!^v?j%Ce2y)B5L4nc@2m zzVaz6eCQBIcK@-d1oGPrj@$v~l!N?<21lV(w0J6s9-IcpoPnyTWb&sN97R&`lBraB zNHaJ}Bvg>>HRNAvaLkoTS5DQ^!!m=TKq{=9T22ou zl*+E8hgC{t@1}=3CD}dnu$nbc_0$@ASj+M(n_5Q?_ZpQ<>y7ld0grhDMN=EuQ$3!j z#Wt}gR=ozJp`c>CV9|Kd;_;H2@%)A3uG;ZA3#Rr&l2E$79aH;_CUmmhU`wAeC0cX+ z%Lg+DTL$y4K-R|N@TQTn5zolNuXeLXX08yd1TdB zICPK4h@;7!tK?p9w9cEtsnW2kCSgGhCRRkU6}y6P)JO^_W>NF3^9D<=!qx~p5l2l( zp*xI7Ilg<(cx%4gCPA8Oc>C_e$~4Oe_u`BXhmq8tLQJaUIH~M0sZ!#ka>S%c6%xVKB?U8L=hzIx30f8aps%ZTl`$Scrh!A)OlcTh_siWS2cT!pDZ(Huwj!2 zmWWsYSVywk(&qNmv1gYH$AA={%cUzTY_U^~<-jm2kY#W+)cs_0M|V5ecqlK{sG+bV zF6{1Xf2z*q6??)*e)zV~G{x!l`BgwO6uLB0sG_5&=G0e)Sw!7hug1;*w^k3Alt`y- zD4qJBVzYF%<wtVHPYIbQFMxkGHFzb#egw4^;wmi*nP}OwRy!w|=qpurY zfAK+?8mHDKzrOmMK^Rx|^C^$;35~RS0p~VnVNwJX38i`(XH%6C})x6Tbc#YR(OX-LcYIOTH?mGvI;A zd%{1AMMF}8`|zC!j@CzSg|59tng^eMjNZKY<(1Iza}(#k34Q(!79+7%#+S>mbQl+D zW^q+PKxgJ`8tk<;glCQSC)aOXzcl&$?_o!oAGVzl6>ef}8XUBChCxGghKOg2)1hCK zvNG^ime$eKSy@U)E=uc4yC3cHc2yngK3aP6p&9X1Pk>B=YOImt*&-O#i*YfDylV^( zIuwFgYC2S6j3v(&mFlI-7q3#TxOhb7CVntt21>`QEn~zkuEFvzOgCe2M4>W0kyV)w z%@GVWbDTFFWkpDy#*(v|XNGq!^p*eS@EMw^^luf2 zr)!r%fI}}2#Kj;oDA@6eY5O8iw9BYPCJyBDhZY1X5{K4 zW&KpHe%Y$oa`g%>jIqB1PakWTJgU;u3K+F3X2Z%WXlU$#PBT&zL^SODY!R8AOran$ z8x~&ao}Y<@S2*}EGZ|y{bei_yr}CyV$sW}<(~iuru!&vIh-KAjO611SqmC%f@S;v$ zzHs~5SFsip_f?Ec9aKJ8i5Ub7tvPl+%SP36+QAs|y&1Z8c@{{_IL&Kuz+^vL1SZ~m zG4#e8AjoQq%sitjjS@Crd&h23#7g$9joIQ9y8h+lwQHP-zc~!;<=2%j<=E@0}509FSr;f7Y1jxv|9@GZ2Ef z$VK?m2y(>j&jPQXIIL&MnW(ru-n}{tzhDN10$cW0zfL*x@R{()kSJ67v7n;c!@`;%U4kdM9~yDyG4aU8g7zs6bWNmS2_0naFt7{h5EXNWsq`$iA>d3f>yAB+##F3|_88abPiOIgx zWc$wdIfji|?hG$gv(VAjIYaUMwC)|+5X|eu`7i#rwBZU@&9b_s%V%!Ga3!W~Ltcr0 zqzz$^9iPIwVghLxMpOmW zP`%0?PKlb-b6HR@eKv6$zG$nj$8$zHW2z6EMn%*TDP6E!jWW&?Up-V0_oNNRSmK_9 z)2GwojdrnNzs%P24LV|1Zq20?17TsOb|1~0xV?w4ih8yGJB z9X#zf?9`G;C4Om@Yx3G3Vdg@mBUqMIzI)eqT9>pIrG4`z9cl`z7~-UpF2yncW0vhW zFruUU8+FX=Nr)M`i?JLMHVL9_0d5!sgg110SS>+4!9<#Upxd#asX}zsDcW+e+mae! z&21jH$F-k-&JO(MCQR$>SrD$puyE@qE;9$}bHxk2UL-XTq7!LJ3)!D-Q zFdli9pp!mq(A0Ds|BnZ)3@~o=g+gx8i(XCEeqozlHa`2Dm=oKORXl1PR8l?vhJ4DQ zh*B~k(EdvM%bn*trwk^?Lgt)l@NMWfeG9Fh(l=WIsiksiDQW#I94{)lbl~*^ziqtO z*l+6>Urzinl(t~VCZ(+mq^*?GR^m8>J$cMtFlsM=YQAhQlI%s$ZNYK&)vvD}v|e?} z3-2CY{?%&vo*jXLopQlWDLeYdPE`0#`c6v4ThE+42hVpI+5KfN&p$tZEMviF#sYuO zl@r5zrHloEj16+ehOvxYqZzvb8TZK<_rV-MO7>WaYc$0bNGX+5N+o-opTC!bH_?|l*3>}M(h=9NDs%WK2h6cRjhBNX zwP6iJ&3?E_h}48X4=q~n@sPfOjubJ= z8`9PQHRoUF1C?S8rmSOXD#@CSK7G2i9&-)*{wOiV?141a(0)}F_Fx*PEfJ%RB8_m3 zeaR$JUrk-`9fGO4rtGU83cChTm+a)&CB0wieWCn)cG}K z19|1rne2$p{`p0)eztwzCm3YH8b%xHYWBYJ$I z&uqW~;dIsx69n7U1U*|j8&;Fp#($92wD+tAbyt}dF6U@z0$%u=*H4E|KRbDCn8)6J z^DXT4hQ9eFr)l89*yokW1{(juA@PT77aJZrFB1L&>a7p3Tcszj{G0?R2@x<&8Hyb{ z08p0tNL=Pmstr09;Ne6HK7_0gSN5IbILA?DV30#Yzj8&UWqfcN=Um>2GCW624;W#- zfuM@}yy~GBuVZex^}8>zHlQ{nWgvE#G2dEtQI`Vjuwv)##Y>z;)I~3^QkZjYCjg%W z)hcIz|JN>RO^}+=AH#^3{{G zm7Fc)>>#IsoQ>o>NX`Lrc9FA}oSkrV%LFvnvZFc+;i1o7!dQ~wp%k_NU<4azJY#;` z@FA?MUP~h3{Z-Ss)bLsHLgav9AQx^y<8g5=-BV!Hx$6lmg|XAA9uI-X2v$zjC$6S5 z*ZfP0i{o(muz=Rq`jqDIjGFu{nr@c>*pE1Bk|XfnWsBeFuNiJ0Su3r*pMLuvl=ipu zKQ=HIH~wRaw0lJpnieupGXU^IKhsG=q6I0%G;-)1wrC@Vm_X4Er^3O-8^%nKj3YWI zDv_Kda?T+PUp-)k=a_*tF%Q8WT5~`vuuC}^PS)N~*SH)8k0PrQo;qX?9|Ny86fgAI ze4cZUpF0t-=YN+|_`g>E`L4({OOB>ive!y^wE=srY_Ih_F=eynL$x~W_)?(spX)ZN z{RVj|actqL(S@r53+v>Cb%+S$2y%{qW7YQ5F?;c-y?EdkLxN;44%qLJ?e~n?H;>vk z2kcvA`&P-mbvClncd9ja@djF7$KBWv@JsyvkWbuASWMCwo)Arfk&H9>(fYAhv^lVo zCY;m6DN)SmrkFX}gfqY>CHB(K?I+C+d}KBL5?XJR`7zAWWLhN;a-Q_G*@K^%I|>#{k8qIFE2m8d@QqkG!xbrhv!L|<$=s~a^||R z%Y-^?H}qKFfYs&Q%g+P?5+FU;am`Y$^<; zHZ`|(HDNu{1L;4Q&@5obrM*391LkDZ)UoNw7B@TAK*Dr;JLyXXtu5_cSTA5Q^^brP zmZ^!bvg5VEgfKv(o5|6S&{W4wpg6V770+V7vcAXL^9r@yABBAX2QmaXL*S7)V;S>DGv)^}%H@o51ep>XrQ^=b zmv*1sJ+L|8oF_ZyNzQrW8FMaFy;2p(D3voxC1?EKkKfV(OOpepQ^F4Njb)UMW|Rgp z=E)fZDfTy>k?Gs~Bd#ecveDgaNG>Bwp!pXTzq0t{CFhsmxM$qokNxE{Ci(G4ChtG^ znR#gQN6t^3Qu>4Z7qB0Y?FS_Lfp1eY`d6OmJ=cpbRd^Pm-~MD>vVOVYPs@xO)*1e^ zJaL1|{HJw=8w$*SF0jJ?XO|h_e_mI(vB0dGI!O#?Q|B&p6wHWN!%G8^WdS~U(#m0E zf#7kYSmM=)@B*ylMOFYKw&6ggEm&(r|G%$Jz4+=lpNuv6frWD6a|gQAXRVlUiQdFq zddhIpgel7uf9ipSIcq$_3z)nTA`>cd+hYnrfW91k9_@s4CJOVDW;R7WWNrdm+ctfz{bKXKM;2J$^*WbF3&-%ft`AqX_*<2hlY)!vDZp5r$4 z6fQw)N3Xnxc$sn+>_^Z_$?$b7M__7ZYCP?Lr1{~HdnO%n&xEyH zQ}Rp>n||*+O@3Qg;!g?`R?3ByzMa0EKj)O0|1(aRAzC6oFq903{{r@dvi+cBKRBLR zDA@}=Wa8&fuJ!f}6^1`AG;XXl{JA1=W0v{PwS^l~&3{R?!vB{nGs6E;TUei}pC`@8 z%jQWL9zI8U;R|2!dDl40%Boo?RU;z|0{8fWWgI)RDikcbYx5J9F;}!8gqbCh-wG?G zm>Y?w?zxPtDiIA+YudHvS}&w36@tL&o92xbac7v z3G5tx5uNjgqn?tdfVC_t4Q2}5gByo32e(}5nlj)q;;6|fbRJ8-M4udwQ--jkCZ~WS zbgqBORA?<6%(x001@J^1H6?|vW(iBJMLZ!qQ--jkCZvEZ9Hi4)99?heK7CG}7+8+v1% z%>typCz_Aqka!g)C3m|>%xf^=2`_1Vctl#F6lr-#Orr1%CA>;e4(Wm|Oee*DO0k&? zf~SgvMtr`5!=tt7V44ym9O0oJkqpxXGrD1;$Bp!&yQ;nGD1AnQP1(V09?|1%Yp-IT z*J!3)Y5u*96dw14`$!ejOd$q|sjP^PkdKWe{rG~}R+QhRvFF^U zkM|_;5t`kZ-PxPcE1Fa(WGW6KhOD6i=# za)D3f>L;=OKW_3Qf`6rGGC3n?t6grK?ap_F7zsT zDTnn6=JYreg8In;e`vs=6f~?a;PZq9jTb({J}9bWe0%+D*vGiogYLb1;WHS5Lclfl zh8h3B-i>}=z$=(G4}^VCO1?og%~s#Q-taz188?K2!O*}qQcEFJJ<;9a-xNB)ZfCqc z=D~2-?b#Rf4TJ$x=kx3fxdNfRn0|bZ+w&yrV<4?3bvK5XpeNe}NMt8+hk~Mx!BqMv zr@~7)m0pFDR#HBdSNR^4d;o7TIMpPV&db$6uGXspsPn4NY2>AQ73Z|?DS-;S(^O!X z0bfV(^|ygzglLOTF^xmAfGWW}XhZ55b4wfIHy# zy2BxcjVywaxz~>213&#Z^}C~g`XZKk@r7@Gdop!qGWF*0pT0Qm{}yIEVnveUuR(zn zn>;Dqs*#t!d4KZC1;8V1{pO92=Hkb`dH=1s*r(7Wtbh5V$<%LOO~u|weg2!d)1O>9 zeZI$&-HuSMhPM&k*J|O3uPRPcQ3Vr>D&mk;801^9n|gZZZdg-8sv&h)LNlrkXSXy( z$yXj@jj9=oS8Vk)!Se8dEvrC{Xk6Kk!9hW>S*Ww#c{-KfzV{g+z zj@6%i4zxtsr}R@%dR+0h3S@4uj+8x~>^6k@h-`!RH7|hE)Ltq|J-vKL5tiUY72)ib zD`vYh@^wOQ3Hyo;-vCKTQ7B%a?pao&-w@0uzSGc6KFQf2xcgN;-!UNg%y}-v@A!KAO zcb1d24IciBp(_AP(-R5~t~=6kgB{d1$b>vTmTl__ggov5yABeWN)6BwwiCcG#Wg>j zj*mV5>$Owvf3hVy9^yM5;##)G-S6*xXYbqoaev%@VPzKa8V-g-E2#ZpUyx<0;hUgB z(#!%#GBx;J%|-@&jG#@o0kBoT`uhZ3AM9<|YHooJ?dcaZ`wzOAy{w=ba0h*Y2KHgZ z#|nCv%M)<3tjon}fta0yVWtkLHv%|Pe~T{TCT8CSaDA9c8g28GTHl$pFL}T8ozjGT z1#e$5PpL{e2{>sQQH}InwAGFEz3WZbmh!fx^AugyIjJEZNpW;`?1tVanxE#IpB{O5 z?1_^-BRv-@>f@RZ=|sgczGB%tr6}u6HZOgD_dC1aerEg`5$SqTSM{>`xb}qhgn3LC zZ{&0qilHAYGz$xx68d%f;xR zo}l!51%1#>M5y1(;D9lDEYOJy2aEgsKxWV#-Y1lVB4JnVt5IB5<9mV~~Q*S8L@Pg-h6Ok7<@!qUlGI)^tW%{8$fkFA?LFnQq1hRNrC`P}*E zxYZju^QMG(6K~!${BY8;aKy;fc3o(>(84{r^FsSSxBs!7+w9~lyAqaNyk!^AD6b#c z!r42&Z27X~LhqOD|JeSAcJ9H)x$@pbc`skyJG?Dvts7Z7HZavP)$(QURQqS`U$k@g zKEheIB&=I_>z3gyN!y~4A8_^UQ#+=1Tu@Fu^Vu_BJi~Qu;Db`1zE6qUQVf7=EWkAYu8GhoDJU~SS*g#|Um~GkF2E}FqA)KjD$Lir zSziQsgjAScntDC;(%Y%Gegl%6X@^2!46sHY#U*NlCAu;5#UL)w9%K)OaOerLJU4qC zYEp|qInHz-KxW3QLa-8mN6NiX2>lG+*FFKTg;Kz%$|M0LS5@K?DFGGIAxiaUX+{cC}Nm0=}Me%2FCipSEgE~#WsJa~yi2_bkOyZx-z5J0lkfJ6t_tv{} z$A2ek96c~9*jGV)aG#Iyk%16wz;9i?Lq1Og_A(1{z|F7%?G6M$V9}lcD`IJYzxbHL zEH)pwE!ob3rO!X$b?rHvmOy^*ktId=F3xXTKIUqm!#V+6hSxClXANbo9-~h#99=k5 z)-YYx5bqf8n!IPa@dt^rF21ZQrb$*Vn5kSoUAcUsFHyOQuUy4h^5cwo?X-Dq!raB1 zyEt8!xP4Z@GyV@wHj27AGTETRiu1j>yVzVa_X8$_qrg0X;6VTmnpux&lDh#?@@Xy0 zOjcHC9C6%gUbCpE1Kwg{@0;8_-FRQ3Y#m>=?#?F`_w)b3iA6>zPOS8lAkJ)N|B7=; z7}^7XE}K)5TR5k-%$!Q(`H`i!npFuhE|^=)>;04aryCzkl&$B>*5C2G3R+bDT;4v} z+@iknGk9Nn7>+#0sAD>jOGg#`kSPYHMSAea@nX5$yB(Z9WiK40j#K@}>`3TGu1Nyq z;-QO#T<%_p1dZd_q9Djy(T_?P32{`>3W6LPLP$3jPv5Auxfb2J&GMCbYl?Dw`RKi*gI$B?k%ADTVmi==RYyg$9B6boBixW0 z%na&Wn{Q{|4QZm9n?6Zf>`75g@%2Jx7usN^zGEx2QB72fJiwugJOx`uls5LvVbGq6 zjnBRI;oPf}ph?b+eViJ3FZJPA>db^eJZ9VNYX{woJ7`by>9zLDzx%t?3-5mO`q6Zq zw_lw5`%nJ##fjAKU%2x2=cyAfq((ll`@QzmsT0ztoX!zseBlT)u-3lBcL=NmEO7s| z_Car3H%Nkw@F?FhdjJlZY`CDnk)?r+TW*)shZc+NN&V*UE=_$X_5m6x+SI^m2l6#F z^_~<5N}-)#!ONp6d3YYxOH(iB5zbbX*_%*)5PK7D;jHEwLBOR(u?GDt&NZ{^}0wLW=1QEb8D$oKTGYC|-6ZEPew>Iv}_Ep+qP1 z@F*l+Rf55kIxf*6`mxO<_;MRp{n#)io{qKyGT66OZrFFlr+Oz~;6NRR!|V4k zRJnoerI^QnIP*AyCjiI>gn0^b{)(|+kYHvkO5*H9$GO$#){g&Vik{l=#a6y+D}T>6 zuBC^s-ku%{hfPrXf`j41f^rZnVp-lJ+7WuOk!Uy&%w8rm=<-Ge1AY%OCB}idns5jW zoPx>|8azx6*+fYfX#apqFp{&g%Z-dp(10cGFql4ZAOw}y56&+7h<{K}y9WmYhaGzH zXto(k)q`$7!!jE%ZOX8=JxIg|=dJy4YDaD<=rcuqfp1yMQ!j|WQ`C+8=PRy z;}1t#Z*#ppj0AnK6Ruwu5AE|7%49onADA}vcVDzsjCP$|H@fcKyT`lUUN^puv#sPz zE9W&-S@oFhOy!xHc<=abzIhc_y_&bInXzn|wromRHuD%Cov}PMZFwqT`5|xlVN9Jg zm&<9!*Sz2Lj)ZF3T5v`ar^ii+vgLf)@|m)Z>9USwiRHwOSDY_7(=06lpNZ<_JiwAR zzN8Jych8j6O_$WAdB&6QnOM}p11#y}OFFO8ijvh=Y1(S|mI4sd%t^b)Myd_{9Y*OIEPeY5wX$vk2bH#u)v zGO=uuJs;)QY~t^FIAPk%n>Hs+>5=D6O;^?QBJ9y3!&Qo2V1(2HW2|J}L{%+19Xu6G znjYgMF%QQpp%x5xBJyW@THJLaTm zJ12=Eg~cm?!eXSb*bvi?G>*26-j&eRiKKRoZRN`yGv#g56djI=C0VB3J@%_TK;uXJ1KG1E*?$)nK;4qOP{xEET=!A#yGs3LG*L1s6> zbK2j)``SL>+i9DLG7cGqK|Yt@9$rb@!}O3!YI%sNZsH!UimHm+x8<~>$|&6h4LEUk zSYoY+D$gn8Co?!>spHv`rnJR!dJ53uid$Wx>YSyeKWk}`KLt+y%IwJ>PH4PmmkQr1<{Hr9=gsdm}VG|o+r-2of>w(f6z4$8t|Ph zW$chk+{8HtG4Xo|CFzS7J{P)$1&fSIDd7tyM8 zxEqMu*bo^63?I5G1cOL|bsqqKk?6t1{Ifby@A?t8q!oambnS=t0YNQ3S}$r_C~s6Q z@JzF~6o_60Ky-&_L~=)Jifr39vFs;MiG2dNYFdNRP*ruO1E&Hf2Sx|xDW!f10mnDS z){iJJniq^Uyy;1p8+daA=oTeQl6CcO?R zfa;_xnb9qr)-41v%j@bm5Vjg>;i8$Ew&|L-iO9J_=Y|qB_whCNapn0DM7a_4E{(o! z)>JWLYMM4R#h)B^PIYporiAGM-t+)xdLRkX4_dM+*}8OmXhhFjnv&LqGuD=AYfHkq zgtso?OhsbSR?C@cXU*0adpvR?GGlI>HaEr}8h>P}350#Zd_Ql#Kc-3=K?o}K%aV&4 z&IDuKC${GjrPfzMFNG4N^?YePrz;YnT`?n(W`YJL2ToWrHi+idpTYauY8d)78>Hfp zQ5d3mOi1dABZk=0M}T2O6>osS@J(92eVu-yNzI`WxAcpcaTEaDn1G9eX-6iK#O*6u zRFGiteJkEWd>zW!-vY?)+w7(~kf>SB*R1Bs^CPTivyIbjz*w;%-U2|33Ec)>w;>iz zRV*CaaB|n^u9=FK>57&(JAPoY{Cw4P>ph8zd-;lcV}@jD87};>{zR#RFLiLbBJrZm z!j!5%3NDZl{os6VW6*KvXJL>p^{jsIU9OMe zg|F;lmp(yGJ4&aolF9W0xk#pHmYR}u-OYXc~l$E`fsF< zj%pK0M0E2k@Hi~CpPJ;~D#P}XY5w!HNFkLE@{lkPQ!g{Jk8 zdll$BJRP6=P_}JhFFwJ90moswq4;GELbOyw%ReyINeuiiB>YdP%p;Rc917tzylxGrTSG*fLMqz6gG6ICL-%F7Rs$JCoNWZH{#Zt!N$i*m zTtNmOrjjX#^#(gz3}9_mqTZdCM|ANU-DmWw%`KXIGBBB3?L_aT8c*6!RQ{C;&mr_&gx3#=-j;($>Y&>aFy9-ftRw1HEk0h*^wGb;syoCq$k5Wk23RjHPMX(iGn@8Rjfa z2}>7m>B?e>HmgW2RGyOz7j zAvpiwCJ^X=Zva%M^++6#~?O znVAI`=nHEAB+R|Mxi_YowN}hno2RYK32Q5FZH;N?m0Ep6(qwyO`%Bx$x`8flTFe1; zpxAb*En#iotqq*1c)WhmT#e@sI0Iz>)SIz3Oj{cg)+XNC1P7tQn6%nro32BcS3@=Y zq^Xidu<+SxPA@;T{ABxRJG7u^ygt%;OO)%^iGJ7fYkIQ#m-_R1uB=Cl3Eg&Hx1G~% zzi76ObRIu^;xMPHX77T*|Hp;x>y6Y`2E~R7>MNsR{c8HFirV!n>91C*F}<3G{I4r& zH>{*(LIo8mB2*@bP&uY4a>a&-6+HdqgvtR0Qy-QnVP3d$kvqlYt~UTvQWY{K6jR}> z5=2QZ*AzGC$hipc3w0MX8w8yJgj!Z9gM1CiR}^QrUag!tz637z;${{3^M+_({d8Q? zUU+yqz-0lvXsbJ_lhX#lq|XT^sGT*hWNPB4{5kSEVLVW!!|}Xd2Mwu+sH@48Qxp+( zYIsi3OC&dYPE-w|PCul6PAwC4bljYyo=e?~5)pNGP%kU$r0dBLRl(XZ7OtJB3gm)G zBDRVs!9;A~88LzvPOpsq{PM>i10xo7OHl^cFTeNGxnDjnUSde!29RD{XpLZNTH#v6 zX(4M!a)(6~7#ltzy2h$5c(4ko_?1!t{ z^Z+=?$Q8bF~?dmVCxl8n926N-TQ=M6-E2Y^Jj zrJbQB=~xt(ZBmQ`68RUDZ8s5aBL_!+!Wmm2UNn_+6|M`OKlc5x9};3rn4aZL&vK?` zi7-srm+;LOw=ypYnR2g#kPH)613)fl%OfJ zkUTzF0zixjT`#Ze<#fGCb1kQfRM@8!TBm)4F!Lb?wxeMC%5=bpy9JKPGgGVuq1NlJyNU_3hL3?URP7?nM2A zeEoymg8UdOy*oTJExU+6z|+S z*9>beZLr5{nc~ljW3P!@T6!oIjfmpWn>hfr7~;S}8wNQbX3vO~=`&)8$*3*xt04G2 z&|Ss0X>r{D0HW;U!2GgX_J7XOcRP+TwZ1O-zu7UyAb*#!pRDNmTPV46(S=?yrWr5i zKix2d9;)I3jF$W)12A%?DXAb+P$wFRw@FDHMdBir{KBcwZG;kNBeY-i388A4@%vzZ zh4k-N@edyGS8d@ii@nChEBb70qJIZ}#FqUDQ_&l7ZAB2_A2q<#0ravG~I`r{}scHNB$Vt?_v2J->}8UY#wm;nT+MKGvF zNN%@f)iLHHESN-a9>FIF@~*uNVjcoKf3g1xBQczP&8q4-RsF2mGOw&wgJ}#vTt_}~ zAbYnAF*?cBHXWcfPtX&+6T0#Cc?!PfKo%Wy%L%##%K=F1z}G1-$YqL1Dq9tEn@H74 z^@4c{z=ZW&ElEu6Bp*4Dt%Rs6NToV8T-F32R)Pdp%4CvMwi4#9BbC(ZHu#?abz7Vn zkCMdHJ>(+?vXv0EG~GPtS*5ys5(W)Eav+O~xnip{>V=cda6h?sN_QRx4Zh|;79Ddf zgfyblISi7Lp=BwEyaH#~QOk_O2V7S^j7A0Vj-+PLJ>Ye-WcdgN_D2DCz_ZU6OkZQh znLnvy^$5ypmKk zCy%p~jmyL&)yx%(N$MU>9+T8^u2?LjFiS1vx|;&hX4lhBFNoqLC#$(^b4tr=@Hle?JtkPS0GKKDxBwX(iHGkc#? zl1e2xsQZ53UGtF3TebJu=TYbEv(IDiQ~D@1HCX|l%4Oyw@AWDa|Aa{BqZMy{EKw>H zR~3wcQ92au^jq1k1U$)+)T3%wDJf0et^u0Lq3zMN>w5Id(+Na-=Q z8+%gQQ+v|d(|Xd|(+OSc$mq#z&lJ-!wVNOvog=F!yFHuG^bT{6rQJgA4UU|i-1b~@ zpX`{?lh>X{?o%B3Jq7Ir?vw5QYw-Z8x>6ISp}13EBa8SP<#Ua^wVBU!qOou z!)CE%f2f8~_!mP?vM%kvQ$a;*FmI`G+v&(6BIV+L^ zTDQ$%>+}S)419ZR5M*I(-FCOf##-GcI=Z{zyT=8A73x4rn}@YKyBqB`2NOu$X?xC7 z$J#n5YHV_PY``oAHgtGwJMBF-feuRnfn}O59n4{&aJqM_{j73JB zL3ndy)I37Xl~MD7s$(#Qp2 zVPa3dknkf+jDHc~N0=CY4dF+a7=JC{N0=D@V&Jqei)FR8grtHnu~e24euRnfFC+X2 z6XRb__z@=7Usn)*go&lUlJFx;jDHp3N0=D@YQm2&F|E6X@FPr2YS$8egnDLO0#d$? z@Yf~GkDNMYeZu_6sblIB=0{E)(~vMfa_X233G*XoF4HKJ@H$JMD5D-ERI7|?CDdnR)Mp5_Lq_c& z)I&1rAwunxQ9B8hkx>~!waKV9LhX`Ky9l*gM(rll!!qh&Lbc1Nc0xTOqaGpDqcZAI zpsE>%jOrk~Ju+$!p*m$$Cs1{a3t(bB4LNnpv4r`NQ^!0@_!H5?$Z26%St(hPikqa8 z$eaZ^EsRH&iif1qOHxUsUm>TCIZpT!$rU**%n4bpCkXX98TC0rJt?D}B-B$f>M25f zUPgVMQ2S)mK0-Y$qn-w;jyVG`F};MGI_8Ch`H|DY^viPXC)5{Z)E7zGFD0Bda_X3u z6Xr)w3v*VM>sg@YGOx&}uP|P(qD#rVcGl4Ts#o!xvi&vK2s#(YlI^+RCQ5G^Y-CJc zdvW2Sn%c#|KYS2;{jEozeK_&*E01mr!TsakTzK@^i<6&!)te1`V#w{`;2+-#UjFRi zNPklU2?)OOyGJixrqLgcdeJcrvl$2%N$ro-c6 z-Mxk|EAaKEA@A@$&ap2c>f?KOp)Ng>ysZ1_oj!VPoDq%ME{GEz84?k z{$a0K7BDgR_2kVjgZ=kN;U+)*dgAi$X%s@382l_a@ZqE1zcO*{x`Z(K?yrOA-|IC& zV(`e5Uw$O!a_BVHcVOb`?;l^i zL(1^zvvU)lzZ87+mEi5W62jy=wl{=@XHUS5#M?5(d};t;o&zo zCSLsP;WzJ2oWDs@5=;8WOTml3rWF-SCKVM+9ytf~7efxd^G-bS!!Iu*8RZKK{P@OQ zkgsq{kOm4u89sjxBdYNxm^;nr{r5<{Lu}zVay)Cx-lR2 z@ykPxuMRzCfIyyuQYYU1VB*UGNi|Rc?RG?%rF;=#M#-Ew*N=~U_jkdI!w6J{NkHYWLj4rPJZ=j@Y-nJem?Qm#ecqcjyxmPEK(Nwj4xiE{M}cO z*rU(hmGFUDi;yvGDIqFtEHF(~?7XPP9{0Zu={+JEGo%u|?*za4a^mU-VI}jM zB7xM#?HfVwr6@%ZAycI#(GNlI&EV~~p+bV+T%~nFv{>-*2_`i7R)h=7^)jn;Jxwff-A+`@~zrk6*jkOZ4&OA&EXFNbut8k3aN^r9f3H7K@}p zG&&Vpkz5RkQ;B}~<(E{QK$UK8Yi*$-h)q_C@@l2L2A;zPW4W> zb%}_QS__jhAcnLeu{C;0Ly?{U4K+&rLNfl=)FiCj-?4wUVlyv?e(JpktMN=XJwm8ioO8TAm z9{2Z8+`55D6TR}qFbwISlOEl^Elx9#EY1sl`*rZOSA*VHC+>VrCT3!F-G1-k*S`qO z#i+QFXCQe_p1V77`@CcTnSAFBm_$hkkKcba_}M*p7SIv2m}DrL{OZk#_pXuo9KD_R z>c+&`i#P?rsodiauRysUk@7}DB83fJJP&n3H8f3?lv|oF8Fym{;>d{lJ+`-qfkfqf zb$0Tr*Fuj?5b2=$%_ozud^B@^j*17LR{TVB9SP?H{7GF&c154+ zrt+6cG?T+xC8?z|Y^a6!$eacLKjM7ps-j!br#Mh~TIrGS^eJH-9RA3Po(+{O2@SK! zitbeQDc?xCP}QdRSW%%0B-`C~r`yxv?6d_`ZVwyK0I(ke1A0=`f`IZkEXNg!l9GQ} zUA^1o=tN9-d2WKy{nF$ z{Fmfa4luoUx_XYSIaM8Jg;sTpb#>a@?y6M|S7(RAy#{VV5#ELpxN8CQE4bMQ#B2MG zFIJ3n{JSi^W`j_(fh*tQ>-ebq{q7s~n|7c5{=zWhk5~^Cx`4*+vGuswLcjuQOqwl% zTNYIxJaX?b8ynDxWdLq9I1h9M^j)xagvDD&K;=4gB%nQdqJ!;r2h`4v9$P>QE4yBs zJ7BO{I~^Tvx7F&_L1HE3)6bS->Nr}Q%Kw?iy#fsG4*>ksukfd2O(`^nT7SvhkJ8>x z<4YC@B@3n$>eN~S-Y^cR2fBX9Dje+kfZ?;|30d=|6spYHVJ-Rc)0)}6AD*0_eL$Ff zU|`eW&No^HT7EDW`?Q~`c=LS0Jby}|%&hg#p7+uI_xIm8c=I4-`pK`)d0lf(cR_a{ zeNgW!HwxSXSFh} zPOFv0m2V))&IAlS9mD~{&ahZnEOwoMo(c;Di+4^}AS28OqGSiph=9Rkb9OjAR$xmJ zZ%I2CX>1T-Pp>;*mSR?DKkIS3?L@!`O=Rz}icEUfaTLy8Tfk^{TS?;|x4UftQ48id&}Dbp-G{9n@F0Og zb+~QTP8ax>I6VOa!Ma@?j)2Bv_c&~U6hd)y9I`pwB66(vki*^?&>ePp+{av=KpJ6z zhv;RyF_F{|4$ouvz~ec)TviZk4|>APAb}m7|y0BgP4{9~a|gNaJ;a$4nrH*1f}a+~%-$bamMscH#^Z$i)V5GS=9T9M54p zv`lP3E*8RbI8F%U(iK0zZGSFqzzjE($Qd^%rQ_H!TZaR*+%c$XQScyV(4<*XOjLtm z*(59x*-VG_bve7O#~d9VR5*cb86ngTy(|kQ!RACw3I0lvC?KWmaeF7U=pj_gMD7B~ zv=G?R0RyH?$%YQ_Z33}_N>2;lT08BYlTsW*0vVBTP?$Cw2wVrlu#kJ`nM4_Nc0=J& zMjZ~Tv$y9EXj78}ArH$O4y3UiC#)el@T8&ecR+bVaY%SH%^b}WN(#CQ>)h?*=Mjes1s-rG7{Pucfb@y>4f4!ho!;2(0-`d4CLkn z5CaDHVb=+oeF{~9R-}Yl6&VBRH1;tVf#K0Su8@dzc%W7flk($G7sxoq+Cp+;C6pW- zau~Pf{+FZ z+>%B?i4BPlDMUInJ*oukK=AZ#kgiC<5l@%kBt}SJK(EsjJnkchtk6#ENh|8QKrt<# z4b&l&nt^=52JDoiNUU#)oI_a6|6N;8=JR zL>5IXlZJFK$HQYyHpnw$cOUEUbRM?CfY8fgAISn?gqJG0@+mSLkXqSP%D#; z0jO3Qg1Tv)JL?~Jp+l>za^NMb&MN{3*j$_b|Kt)2Ok_lQM zRh+?f+Mpj;E0k@DCd^Laz1 zV5sb0@6RY0Fmi=8d`7L1QQP0-PtWr{Ke%T2`0(-3(&00opSg2}Thhp-Z{*WA3h5jB zH&KR_!;Qm@BkXX?=Ph?yxSDz{y@5|}5YiifA*XcUd9JKx)G}(hzkamn+oJD^xOLmO zob7zhb|Ggwr_T0g`~8D|Ir#lSZp(hI-~eB6KqxrCsb`Q>2G5Kv8Cf!VXk^XZHTTwVD>idk&3sm~ zkkt%{Jh3*g96y`Fj9G*E-0bC}$48HIJ9dtq`S#3rXSl}Q+>AZ^j6K4PJ)GL&&zn6s zk1JnsUwdE6we7l}_LsEp)3}X$xV*i5-d-VZFQ?8)h-XImU?DeWB|l@8Fk=-aoI86k z6%yuiR|>f+IknlJRWh*JSHx#65V97)<7E^Mqz*dyj7lM+5^CL;<82;HBhBI-ZvDLV zPAgZ_z!^92#tnjTL;nW9sd%80E2-s8iv`nSXoV*>Q`!vgQ7*4)q;aHilpSfg+j6gk zTiMK|ZQ;|l2x(jTH~P&b1N(eMBfCd--%lDjc=zDFgWReuoOvs6-YS^4_P6-6iwBy0 zY4Q}ZxA56pgzPQ-Tc6yBCG&hK-28R-yY6>!Pw%_${EPE@C%5$gS8|XqIVhAIjpnm5=3kH#0(2}O0BI^SPBfABP9%@?m1iq~`Mf`5b~Cee~gUn)E}Us5lW)MIT) z+P!V0`fl~TYHs;v&eY7Cngvs{*gb!?TO=a#iS%I0%z>j^>5|dL(Z>7iXv?=P-?eb{ zZJcE%Z`mnWcJ{aW^UDWULZ`TIxo_ci?!I62m!j{BxJ`Sx{HOW+r-l5dId!hTwAxq6 zEok6NHwdL0umuw-US`2S{@}q8%ZO!k{YcT>qI*T$vQ1oO6Q9{6WH$A0CR&awh|y6Q zEqq3ckkJAij_ARb5$%X}bkRuK-L!jY+|rF)#wI>vlaR3q7_fJ6GZ&3&N457Cji!B@ z_FWpcwv{t)UFzM15(gf}l0%u68$T7;E+ z&MF}X28nF{jM;+)I7rM`Da?RD!a_A}vRo>&H}TnXNEv&6@V5x&*(3-2^&9#EL7HW=$FX1gq1PC^+GOaYqLmb;Bz+! zxuA&5u~Fy{T*sT%3#Ro@m$J4<&GQ}@T*jx)5>jXNH~KRR2MV~NMSNzBkXZvYO)GLO zpR-QLS%(!Vn-DyC#3`IRxR0w?eSgXQCEV`4_t*So&G&1#&Fx&_KE80DP`Hm%=RH}i zVMPq3@p(&yyrozV5>;A1Qhc}gUNN_ek=VWxW|ZQ=Gkj*1kXaS3 z+9iC>QXvN(Dq9q|b$ouEkPl;XZUWlviJe>4+A@@C`q;L~*~jPX6LO$!vi+&K-hG34 z!OcB6hrUc+jX6Bnp z$fuMk18f#E8(=vz2jE<$0^n23Jb?3=N`O^NHNXYTLV$~y8i2LTVt`ASr2v;P%K@%n zRsvkbtOmG-SqpF-QwMN8Q~##&1I(#`*#LMWvk~AXrU~F?rWxQCW-GuJrWN2eW;?(g zOdG(R%r1btnLPmaGEW0+XZ8Wy&l~`Fkg)=MhUox!i0K5#Fg7u#E~Xps!;Br^5#}gB z2h#)4$+!R>W1a=bGH!q#rWfFG<^;g!n3DicG0y|+V@?A+!@K~npLr4BOU%mv&oZw7 ze3f}k%*o4~1N=Pm3xF4x*8%>Lxd?E8c>~}j=1qW?nYRF5VXgukWPSzkZRQ<-*O+$! zzQz?;km06%0t0yxBcEavnZ<`ckgF`ojw&3p!MnE4#wZ<*f#yu;i9 z_yzL?z~3`}0640|2^~H0KCuq1#@El zTjsxGzGwae^WQW71H&;qGsgT!MqvI&=6_*F)sK}IZ!?vc^iGHu73&EW_AfM1@zQ% z)5Ne#5!I1BGr*Qbuof4)g0NCM4g{i=HUQh4uyxc%3_0v-q)=-Lr8K(O-C)XL>o6dL z>>IU8_}3#jlNxKJ^m;<4_8KX@fh0q6Y@)`RooK7sfMmL@+DJ?_J8_$+5vgRWyPjHW z*o{ch^5vOvTZ>svHdi;cM-DzByaO}1?VLMN_NZ57^%ZZM+6D>F-bO(l7 zqC*?OKtTfA;swHnEeYI-fyDkq%To^q1$Gw_&7^U*pe3g+V#r~4lhmj+Cnj(YiAW4O zF=2a2L~7HC34B`25zIO0T~cYKH-Qs z=%c(7vrj9r`VawTkZvGyO>i6H#E?zUZbXe}*1`6w2cr*rIEqvUL987~#5japZN&7$ z9zkNNxPdP|>`|mqktZe~)*%ucjuv7DVtbH~jwS|5fZAx-#5$2`1Zz({4Y0AS3rVS> zu}6%23~3q2Mr}eO`B@~JqF8CNECxWYbT~-2brU{P7tlmbW(Tnifx7WvKr-D!Z-8BD zwwDm7jffDABY{|jBx>LU5{+moY6D9V$>KRe3R#O9!C1teBvI||dWpe^J%z*!snv)? zf1X4qmLsB^plp34IyDuEG&g&iM3)fjaciGFgJdd|_!}wb{BW@Up>m_kXgVhrg6EtfNdDDWMJ%?dgVQa*Wu#t&9Pf{h8 zCYlkLN$RMXlBD_z3{R!T0qo38jGMhcY2oKV+UrP@n4n1HUlK-9)?6&1T_iL#J#9eC zQxj-ub^vL4r05$!sqY}Rr)^+*f+{4D-arPb7dA+ZPwXWOphHdrH9%plzZs#{5i>JM z2D!DInomB6(F!z5ex!oVSj}TX~h1t9nDXq%x@zxMKnPXv400C>7p45 z@+DR%_8L;N&=iG2yN4R0h(NuIA$r=8U{rq((M)QPBIDQ&Vvb_3Bi(>DDJr)<#M6mg zs*X91of}PfH;`(wyBkEy6pSm>FqH}AK>gPwIx00WfwZt7;NyUOSF}o@k_)7UjZy?n zqc*9gkV%SoTC@?1R64atwS^5*)CB~}?upPsN(mnxtWjwZYZOUdkH)CT9cpioB4XJ@ zX(PA>3{X&Nm<)9zUXjke4-d=!8o^Bj=mW&!M3emp!4QIv5&QYSO-RVktw2&()fTY_S2_;b8F(KPkB^@x6$!VtNW7J zqCWMs@ktzv*OvsR-QvYp{cQa3Gxe#aiLdZ!FkShlPaR#-m*MR>&#QJTuBu)I*TOyx zI^XrKhgcK8?w`2+0d)cmioUBisdFSfj}W|h1rDGDU-|vRudW0KE>M?K>QGJ@S}L?G zmNTG%Uc=f*W009Io$#hS+iL^cG?@`qXalNFhg;5~1%oi~1T+t!mTrQAES1h4FI_%Xx_qRZFRc?w>$n2> zHJ;x%mfuLXilvef>Ow-2qH2N zp`swLPhm4+7eg@+{h>;q?Wm;6r7*6Y;D>|S%q=k5--+kKX7|gxwlq9+ zKdoFP+hwN^WO*m|C9^5eT81O$a9YW~R6v>L_ZfU)S||*^p`9!t+?Nx&lYU2+T9zX| zL#~uNbl+!)=8S5?l)jWB;ayjES6_-RFBDJu?O77RjueFMqo{=;DlVIu6vlWfHTorh z(34W6vKZ+D;Q}apMWIxr-<~xQEc#idOK}xMfH2sI1%;j7`xKxP7tX=CH8@%{F>r12 z&ihi$jk87A4ZYD12M;HH_f{a~SO>TwI>~0pW6-J!7ArPjAY`}wl#Rs_S0u4ZP-QOz za7sP<`O~ul8i)O;&D{bk8yD-rBL=Xj4=8rRnR1=o340KoHWn+dq9mZ|2FF}23$BZ< zo`4R&?cnyOrMnq)YgKjBc9QI2k!?h>>l7JHV7&x;KHv#RX0YynzMFOR9)rmOoqvc^ zSmI2`VowZcq42QA#v=BBm$}x})#ZjFq|k3GJ{7nLMv4tdl7%rQNw!|pjLqE{5EJeI zU&O9}#@2HTJdky)jXYaGOHY#o3>%Q%gG$E2cI8W&u3Q)+0|ZyU!Qg{^?a)D=%|6SY=(>1Hw*e^PTvfvrDh1m z0>N0~EB71Iqi!n%W986sLY*ZT=MI%m88kV`-jt~{g{kZ<3tcP-nWg^JB5#|JTEwNU zgv+-Nz;Gi)rc_C$Ped z&s!+uEga8VHkP++Bu7}jh0oh6Xc~yh+guJR@BbT?DrZamyuWBr>YRG--h4Je8 zvFduhdV^5CfzN9c@){xS?3}Bums)+zeD(q%djXdwzq~4cy6M8nDMgZD1yt|F=JU<( zYK7dnpDq#R)$_UaqdU0myS&Z3akpUH%^7$5i)O-dtNi9u*IeU8wPQuKU$pT>%Y~xl z-ueO6W$oqUbFB~3O#>UfeVjg@te;{|i;<}iZVAz;5E;+`qaQ{C$IX;Dm`mrKrK=*gB%Qn69W7fQB4QE|63Mt67M4&6@H;v;pH>v7>;Q$XG6I(CvyC~-5I?om_$CyN^i0X1sffa;X}SU`=(jq&A3$0`hT(}F<`YJ$Se zDLOBso1S>w1*V3fGnI@LkIgVvh#W`(FN33WCj*Y)k{t~)jWpnyD>zI7(=@Eg@HCCi z(b7f6P!*gii>Jcpol1IUXgfE2 zj{4N>c(NYDb`zz23nUe@^B1-ttR8L}D_hNHt`RcVfXtb5uUaozeY@!ab1r?p{L;_= ze`DL=l9IL!UEWjE4{e{2=||c!L_6Yc?8jou5CU$*Mz;*OF43L%uo9Z}a}5*X`puKu zW!3Pqv9gtX<|-j`)qiojOu*y++x3}g`io?DG!gqs^!Q2aIC1BVPwa=dhelg=;u>t+ z4C5vadCf^ng-+7>NQ-WUhG0L32y8RnVT-dTv2mjM9~^ANGwXCP3Y|GTHFw&UmT?Ya zf_BJc`d%n)?eWYFW0@O9JB7wQTts- zS|QVSDkWyFBf6MOd#X_?761q>E>9zYSY;Ms)8f#Ca}%np!Rdgigh=h5DxX$Vz=#*& z`#q-C{j-9ydc3T5tgLppoiAG_l!>Q9Kq&rka~-}&<^Iou>nA#X?NfrHI1hQ&TYHtW!;>0#$#t&}5e5#H4zz*ReoTODi{f^G!82u>h4 zg`f|?3kYxrlngGUBH25LBJjb#n*kF^=#(zJUPb*Br+#Wmy(=kO`#_&H&_1QXTUe$W zvId%_bOg~;#6XZ_g&`Z*QV5cvFysuTPGu6rq%f3lGZ#-~5iDC_DCCM3OqmI0Q5cH2 z;zd(A1j`k%83fA{Ir9ltpfD70u%s^}SSih*j3BdU*0Tvxt}tW_#?4wa)k^Vgq44b#-w_ILqxjBH_%4d?4&i$!z87#P{F?hfRdK#q&O@@Xsg;N4&rCX;KguCcgjw2*g(UYSwD4@;>Q3d z={-wv7I5>x(J41Y!2^egQ!hn9kV)G-Pf-wQf}W-*>?P4!e1YQYk}$Y>sxFB(0-z`b z^%RAdwiC38qVQgJ6v*nSW{Sfb)=Ao1DJrcJcx^gKx0MFLo6)H#Z>KoCVQ5QFM1Gcn`&4bCHze07c>b-vqT%^cg^jWOPs*UgJ%Q)k#r!qc+HA(UhH{V6mmO zf0W|zVr=X!)l)qbhu2Dz*lvo#d$DNZy%dM{InyUOL2-DuGLin%6or>0lLvW$qS7i^ zr=mf1Dq7WmlB%ZYCP1-Kn<%;&(7eI5Q_U3J0w^ekB~x1|-U2vzeg(m)1!agn?++Om)++!+;ajWv4i7e^C4=#T}G?(Nqt` zoq%H{x+r=K(CmSIQ_oTqMg>FWz}zV}MV|xIJittyr06L?NgX~<@jk$@7oVo+85&$U z^#aA$sTGEqTP)&q`Oyq=;BfRawSf#QvTV|#9-=q5nPgEdinGvK7pH&dKe z$`-Yvq{g2=5020I^D6MhGSgpB=`X1jQFE~$u7de~(+q!h;gnq(nIzG_Wy+~mXfvmh z^cpj4;%dyk#Y0&`okO`d*AjH3lYDVI_mJ-h_^S${o!v(xW|Jh;5W8W>Jmem(AA0Uq zu`p{nY!G9zArOv(1Y>lzG@p_BNDkyK90kiqvzeuVZG#X9ha&-WJU&W_2KJ)?gu{@K zM}o|nSwrh@ZJAQQO$3A)ky=F0qnTR7OCU9jVD7_91wJ1lWel zp3DZ-Gdyd!ccg8!Y-I197lcI{BXvmb!X*gRETPf~2u?dX;XnKdEk%CBd0_NOJb3Yc zDx7MLN#~>%4U$Bf&yM`%$c2Jl2ROQlDeAF#iX7>V(4wgh$Dx{Ylz`Z zo0c~@hBtj$-jo>L48lvF-xw2`$>5e{S|D9E1MbB==jqQN!2zU}fEqpDql_PDY#??r z7%)P{k0f8Xk)$^t0`f_i0579lNUUY5(`u=ap-=rYFQd!>Z&;k;#(bjF?n~-Z;kNFD zMV|16FPa2y$s85YS2dA^Fgg~+H<|Qlj)XU{kAz)@fEvy>LcNs~y$uiBRjROqjpT*tlp|<{Cm&zfB`HL~ z@94OFI&0V|37nL2`;6ex)d`0;DzezwkQ#aCW#ny_77>Mm(+|b>i#h+$h1gZpIH? zJ8S9TgHslnwy2`?KSAnlSaU=+@4?Z+amn$9^Rg37^>Yb0*XUh8kn}_PjKR`(JNfid zA-xoK$W!O~3yZJseS7b(+OM?_Xb0Fgl79+jlnw3XGHU62oxq1xpE|D3AJgZp3{@yv@|qe-deJ6WGI8xU=tu62QZJwndS^i>YK@q5;4R`| zwervR-X^cQ2d&c7fC%nR!Q14k;6=C@w8MrrsO$`j1FFP3Jq2IcC_1LIsA7rk2yJ^^ z!_NIS0C2E^pW3B9Rnfa&B*ED3OMkUD08!nekZRcWRy1?GaN$_t!r=5ln!sMZNe+J$@eq>dhz-uLEq#(5j2B&?u~ty_l=v&$IRtE_s!$OId}5L;0<5q zRf2hyH`$+-dC_&=HF$(is}RyEIDMS!2Ym*Yd1yo@;LC{);kDywVaI;)KJyT-?-cZ% zoW3*CVWQmJ!xIx&REK(r$_I}c`-ztO@WkZrc}q6F*E*4j3Ha2;nV6_zCMGZ)_%EE8 zfJHm*)I>YY)C5W&`2lI^K#Lam_)`;@6k%_P`NYvWk%^AJPw&Qwj_gGPFDd)<@h2(p z9N~!-qm}=5a(qAP=oZC3&l*89geNk6ddYM}?+Z^=q>mDvhO|Z?Jw50UJ$VEF!*6a+ zTo!)=gW9d=8~EARuy1++{9}_2nl36CVkv`@ELVLKpGGMwni0-R*mICiMaH!A5%w2Y z7_uEqj4Wt#r{^2c@Xo%D;Fk#AK=3Al%K$@DN zC#Nss^(zGZigA78n7)zMZxZyIIQ=FvyHG~WF8;1L1q-{diYPs)1(_$aiAKibmk-xfHn zJ~xfkKe}I@))?TI38pz;Cd&`{Z9>aE=7rBW)4l|+1v;%gqdlQKp?FU9wBm%4ls>HA5>3fjT!zo`cgG9mDm}r%=sakj z6+iNm959E{!^|+M-Cz#(GciCj8t8A6kTZC6@g$vN;ZA{qy#z^8O|u{FVtjA$pvaFb z0Q`vy@>>lxr(rF8E-AW#eZUd>0pV1@kP7)oR={Dglk!KW_D0f$S}1;!s85q3iV@so zOCTP;b{O4e$z;x=`{;8~J&MXUNi1TzeYn8k5#u%z-*_1xiEzFG$r5CLqO6;ZY>yMFww=%^q0z zfG*1x`De|&xz)RIU<+@YDZPJSnsK%IQnjywH_aDJ^I?)3ZVkBE-w8F%n;5~wcy+Ky z95>A#GtK5ra|F{IuWl+yXDIa>vo5xtZynso8^Q5^7H6CVyVF;zE>-cSQo&Tp8RK6+ z{eWI-Nd|D~4*`fbZYmu!mGY)pf(cWKz5J#u@5Z0tGo^uO_^nc;mce+Gm3OuBQso=f zm#aDB%(&N219P6p^3zXb`n~-N)$qp8jdzS(=5~7V`W=FP2dCfh1NhD_J=b>uz31Jy z&GK!2_4*XWpOTgJX2qXUlGiU${mERgexd453pIGZLG>1brJ>jrH)l!IG}kXQ`d-pef(X%Xe|NSMY9U> zg&{E+hQIJI0CX*(E8{C>Mi;F{&ud|b3-1$#o&>+6^E<7AK|&ROs_j*HwXk!oKPx-Z z2Ln&|@E;6&s`yju=zOB%cp~1gigU7~g{ReIP?Q`$BZHy_@(J$(PX9d7`SfXEP&Ay@ zoY6!LimC)B<&vi)gW^9BFFYtp#V!Eo0Jj)wfE>|yeEC<8 zKKl%mFrDvGg;nzC!xtw$f9cWJZ^BQUQ6J&pH?KUt@m27`E5U(JO6K%pY36_v@zux< zUx#O!5fgV5$cTxaBX1?ak3uPYa2NYIr-TtSYFvchDN?q;skiJyy^f<+@n;;s1sI)6 z;B*KaG$T8TbQB=UT>4`V9dr=vr-R_X=vO&9dJZuiYfe?kM?&etOGiS=KM%*j94If0 zgTDRavm3`|H;%gRH}kXi3bXh2QzsPZ_(p3zJibkkpFb>NKY}3u)?R(xFcH%PmU0B5W0Ed-weh#k&`ff`5+ZNbv> zGoK^-T>z3HZ5`cCHA<@?c1V+k=3QN_>5qDhBH1x#=u~l9m@eDy8q(f2>#Yl1EQtys_T-=XXh;o1j_>OY>plFl3~~j z4Oae)y`1Fwt12lg?QaSIFtDXQ321TL`uVZ)hk{~X3J)&d1@paIy<7h^$IZNd#c?x; z+g3Yp{=(V* zXyp|b94i0O{74H`MCkR0+4g)&GcfLk*WHZjmh2q~@uz9gwF>KJOQhx_?}$ibFSAc;jqGA7 zVOLXmBs}GUu<%YMls;^9jpQYtT1uZ9rit1%MI3YUzfmoKx%nTj1zl7vpt(6x3p#7W ztQ<{`)Q;{Ss2wynYkKEEZH5-tSEyo=E~e4&^ZHw`!Uh8m*#d46Hxo=x*qjB2MFllf zlg-)4mTY9YLcHRmcWCK0%3BaOj_o!>TtCCFInfEqsS^3DOwO?m_1U|STv(s|n{USE z6^bXrdvBc)s@L;*^+H}fmo2~GFcO}7*BT08-*((MYs@&yx9ev6NG&XmdE+|4xNh9I zWy}cc`WC_1!WmnBD6bOAmyVaO94lYRm#-GeS9_ZTW0^la3vJJf{d4BsJUw6#GGLva zPtCo&X|7KqC`>5r>^pb|TA#Xwvo9fv!vXti zyz4?hT)@L)D*GMM{yl;M__}?4}hR;~;7XgxQfmUF32w9DpF~TC^ z$!09o^kFwhGoCHV3Q4Fx?6AYl&|9 zL!vn1k)Ii*zqV--1FJ+g7Z>X$zctFXVllEvGVUteD0DgT*GnN$+(Cz*9mRWh6?2GE z-7F4f0n;)3*;3$VZB-6eH@^IYZXssV02cNZs}NYx|BMKBz)XXBJ$6SGc^wJ)IZOHi zl{6Z5!gi<%U0qll4~Sn5J;6W(Ia;`32j4dY@VeezNf)msdIT{9j4n o-=r*kdPP$1R8o7A61qfu$XOse4Eh^^&_|MT1z?YfMEc494SnSU_W%F@ literal 0 HcmV?d00001 diff --git a/models/__pycache__/govs_order_master.cpython-311.pyc b/models/__pycache__/govs_order_master.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b96891fc270566109eeeac4d78db6cf842aea0d1 GIT binary patch literal 32476 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 m0j;d(x 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 qg8L8xIUd1(K`;|>)i`tysZMwF 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 E1C-U;pa1{> literal 0 HcmV?d00001 diff --git a/models/__pycache__/govs_order_user.cpython-311.pyc b/models/__pycache__/govs_order_user.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5cf9162667105e4aece1e1d42555ddcec3b1d9f GIT binary patch literal 21102 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 16: + _inner_dic[_key] = str(_val) + + # 处理列表 + if isinstance(_val, list): + _tmp_list = [] + for _v in _val: + if isinstance(_v, dict): + # 递归处理字典 + _tmp_list.append(cls.trans_format( + _v, skip_keys=skip_keys, trans_datetime=trans_datetime, long_to_str=long_to_str + )) + else: + _tmp_list.append(_v) + _inner_dic[_key] = _tmp_list + + # 递归处理字典 + if isinstance(_val, dict): + _inner_dic[_key] = cls.trans_format( + _val, skip_keys=skip_keys, trans_datetime=trans_datetime, long_to_str=long_to_str + ) + + return _inner_dic + + def to_dict(self, skip_fields: list[str] = None, trans_datetime: bool = False, long_to_str: bool = False): + """ + 数据模型转字典。 + + :param skip_fields: 跳过的,不需要转换的字段。跳过的字段会一直延续到内部对象 + :param trans_datetime: 是否转换日期时间字段中的特殊日期。 + :param long_to_str: 当 bigint 长度大于 16 位时转为 str + :return: 转换后的字典数据 + """ + model_dict = super().to_dict() + + # 没有需要处理的行为,直接返回 + _has_action = skip_fields or trans_datetime or long_to_str + if not _has_action: + return model_dict + + # 对特殊日期和长整数进行格式转换,跳过的字段会一直延续到内部对象 + model_dict = self.trans_format( + model_dict, skip_keys=skip_fields, trans_datetime=trans_datetime, long_to_str=long_to_str + ) + + return model_dict diff --git a/models/db_models.py b/models/db_models.py new file mode 100644 index 0000000..145973c --- /dev/null +++ b/models/db_models.py @@ -0,0 +1,1700 @@ +# coding: utf-8 +from sqlalchemy import Column, DECIMAL, DateTime, ForeignKey, Index, String, Text, text, Float +from sqlalchemy.dialects.mysql import BIGINT, BIT, INTEGER, TINYINT, MEDIUMTEXT +from sqlalchemy.orm import relationship + +from paste.db.basemodel import BaseModel + + +class TD3iDcmApplyPostpone(BaseModel): + __tablename__ = 't_d3i_dcm_apply_postpone' + __table_args__ = {'comment': '数字城管-申请延期接口表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + task_number = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='任务号') + apply_act_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='工单流程ID') + reply_part_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='回复环节ID') + ard_level = Column(String(32, 'utf8mb4_unicode_ci'), nullable=False, comment='固定值') + ard_type_id = Column(String(32, 'utf8mb4_unicode_ci'), nullable=False, comment='固定值(延期类型)') + apply_memo = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='申请意见') + time_num = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='延期时长') + postpone_date = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='延期日期') + time_unit = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='时间单位') + attachments = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件') + delay_multiple = Column(INTEGER(11), comment='延期倍数') + apply_type = Column(String(64, 'utf8mb4_unicode_ci'), comment='申请类型') + status = Column(BIGINT(20), nullable=False, comment='提交状态') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmDispose(BaseModel): + __tablename__ = 't_d3i_dcm_dispose' + __table_args__ = {'comment': '数字城管-批转接口表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + task_number = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='任务号') + act_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='工单ID') + task_list_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='任务列表ID') + trans_info = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='批转对象(固定:市受理员)') + opinion = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='批转意见') + add_num = Column(String(32, 'utf8mb4_unicode_ci'), nullable=False, comment='批转意见') + attachments = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件') + send_message = Column(String(32, 'utf8mb4_unicode_ci'), nullable=False, comment='发送短信(1:发送,0:不发送)') + undertake_user_name = Column(String(64, 'utf8mb4_unicode_ci'), comment='承办人员') + undertake_phone = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系电话') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmRollback(BaseModel): + __tablename__ = 't_d3i_dcm_rollback' + __table_args__ = {'comment': '数字城管-回退接口表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + task_number = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='任务号') + act_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='工单ID') + trans_info = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='回退流向(固定:市受理员)') + save_old_act_flag = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='是否保留旧流程') + opinion = Column(String(500, 'utf8mb4_unicode_ci'), nullable=False, comment='回退意见') + rollback_reason_id = Column(String(500, 'utf8mb4_unicode_ci'), nullable=False, comment='回退原因ID') + attachments = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件(多个用逗号分隔)') + send_message = Column(String(32, 'utf8mb4_unicode_ci'), nullable=False, comment='发送短信(1:发送,0:不发送)') + not_assigned = Column(String(16, 'utf8mb4_unicode_ci'), comment='申请不交办(0:不打勾,1:打勾)') + not_assigned_reason = Column(Text(collation='utf8mb4_unicode_ci'), comment='申请不交办原因') + undertake_user_name = Column(String(64, 'utf8mb4_unicode_ci'), comment='承办人员') + undertake_phone = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系电话') + status = Column(BIGINT(20), nullable=False, comment='提交状态') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmStageReply(BaseModel): + __tablename__ = 't_d3i_dcm_stage_reply' + __table_args__ = {'comment': '数字城管-阶段回复接口表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + task_number = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='任务号') + rec_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='记录ID') + act_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='工单ID') + item_type = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='固定值') + content = Column(String(1000, 'utf8mb4_unicode_ci'), nullable=False, comment='回复内容') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmTask(BaseModel): + __tablename__ = 't_d3i_dcm_task' + __table_args__ = ( + Index('idx_read_flag_deadline_time', 'read_flag', 'deadline_time'), + Index('idx_biz_sys_read_flag', 'biz_id', 'sys_id', 'read_flag'), + {'comment': '数字城管-部门待办'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + rec_id = Column(BIGINT(20), index=True, comment='记录ID') + rec_disp_num = Column(String(50), comment='显示编号(可空)') + rec_type_id = Column(INTEGER(11), comment='类型ID') + rec_type_name = Column(String(100), comment='案件类型') + act_id = Column(BIGINT(20), index=True, comment='任务ID') + act_deadline_time = Column(BIGINT(20), comment='任务截止时间戳(毫秒)') + act_warning_time = Column(BIGINT(20), comment='预警时间戳(毫秒)') + act_property_id = Column(INTEGER(11), comment='任务属性ID') + act_ard_state_name = Column(String(50), comment='阶段授权状态') + act_time_state_id = Column(TINYINT(4), comment='阶段状态ID') + biz_id = Column(INTEGER(11), index=True, comment='业务ID') + sys_id = Column(INTEGER(11), index=True, comment='系统ID') + task_num = Column(String(50), comment='任务号') + other_task_num = Column(String(100), comment='第三方任务号') + bundle_remain_char = Column(String(20), comment='剩余时间描述(如“1天”)') + bundle_deadline_time = Column(BIGINT(20), comment='捆绑截止时间戳') + bundle_deadline_char = Column(String(20), comment='捆绑截止时间描述') + bundle_warning_time = Column(BIGINT(20), comment='捆绑预警时间戳') + bundle_time_state_id = Column(TINYINT(4), comment='捆绑阶段红绿灯状态') + rollback_deadline = Column(BIGINT(20), comment='拒绝超时截止时间戳') + event_type_id = Column(INTEGER(11), index=True, comment='问题类型ID') + max_event_type_id = Column(INTEGER(11), comment='最大事件类型ID') + event_type_name = Column(String(100), comment='问题类型') + event_src_name = Column(String(100), comment='问题来源') + event_desc = Column(Text, comment='问题描述') + urgency_level = Column(TINYINT(4), index=True, comment='紧急程度(0正常,1紧急)') + main_type_id = Column(INTEGER(11), comment='大类ID') + main_type_name = Column(String(100), comment='大类名称') + sub_type_id = Column(INTEGER(11), comment='小类ID') + sub_type_name = Column(String(100), comment='小类名称') + address = Column(Text, comment='地址描述') + district_name = Column(String(50), index=True, comment='所属区域') + coordinate_x = Column(DECIMAL(10, 6), comment='经度') + coordinate_y = Column(DECIMAL(10, 6), comment='纬度') + proc_time_state_id = Column(TINYINT(4), comment='处理流程状态ID') + deadline_time = Column(BIGINT(20), index=True, comment='处理截止时间戳') + warning_time = Column(BIGINT(20), comment='处理预警时间戳') + processing_deadline = Column(String(50), comment='处置时限描述') + new_inst_cond_name = Column(String(200), comment='立案条件') + case_closure_condition = Column(String(200), comment='结案条件') + reply_intime = Column(TINYINT(4), comment='是否两小时回复(0无需回复,1待回复,2已回复,3超时,4无需回复已恢复)') + return_visit_flag = Column(TINYINT(4), comment='回访标识(0无需,1待回访,2已回访)') + first_depart_name = Column(String(100), comment='一级专业部门') + second_depart_name = Column(String(100), comment='二级专业部门') + reporter_name = Column(String(100), comment='举报人姓名') + reporter_contact = Column(String(50), comment='举报电话') + read_flag = Column(TINYINT(4), index=True, comment='是否已读(0未读,1已读)') + back_color_bit_id = Column(INTEGER(11), comment='背景色ID(可空)') + font_color_bit_id = Column(INTEGER(11), comment='字体色ID(可空)') + part_code = Column(String(100), comment='部件编码') + display_style_id = Column(INTEGER(11), comment='显示样式ID') + func_forbid_reporter_info_flag = Column(TINYINT(4), comment='是否禁止举报人信息') + operation = Column(String(256), comment='操作(工单上的操作按钮)') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + +class TD3iDcmTaskProcessInfo(BaseModel): + __tablename__ = 't_d3i_dcm_task_process_info' + __table_args__ = ( + Index('idx_item_id_action_time', 'item_id', 'action_time'), + Index('idx_unit_name_action_time', 'unit_name', 'action_time'), + Index('idx_act_def_name_action_time', 'act_def_name', 'action_time'), + {'comment': '数字城管-部门待办办理经过'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='部门待办任务ID') + raw_id = Column(BIGINT(20), comment='原始主键ID') + rec_id = Column(BIGINT(20), comment='记录ID') + act_id = Column(BIGINT(20), nullable=False, index=True, comment='任务ID') + act_def_id = Column(INTEGER(11), comment='流程节点定义ID') + act_def_name = Column(String(100), index=True, comment='流程节点名称') + act_time_state_id = Column(INTEGER(11), comment='操作时间状态ID') + act_limit_info = Column(String(255), comment='操作时限信息') + act_used_time_char = Column(String(50), comment='已用时间(字符串)') + act_remain_time_char = Column(String(50), comment='剩余时间(字符串)') + act_deadline_time = Column(DateTime, comment='操作截止时间') + act_property_id = Column(INTEGER(11), comment='操作属性ID') + action_name = Column(String(100), comment='操作动作名称(如批转、回退)') + action_time = Column(DateTime, nullable=False, index=True, comment='操作时间') + title = Column(String(100), comment='操作标题') + detail = Column(Text, comment='操作详细意见') + backup_detail = Column(Text, comment='备用意见') + medias = Column(Text, comment='附件信息') + unit_name = Column(String(100), index=True, comment='当前操作单位') + unit_contact = Column(String(255), comment='单位联系方式') + human_id = Column(BIGINT(20), index=True, comment='操作人ID,-1为系统') + human_name = Column(String(255), comment='操作人名称(含单位)') + role_name = Column(String(100), index=True, comment='当前角色名称') + item_id = Column(BIGINT(20), nullable=False, index=True, comment='项目ID') + item_type_id = Column(INTEGER(11), comment='任务类型ID') + item_content = Column(Text, comment='任务内容摘要') + item_process_info_list = Column(Text, comment='子流程列表') + sub_process_info = Column(Text, comment='子流程信息') + bundle_time_state_id = Column(INTEGER(11), comment='组合时间状态ID') + bundle_limit_info = Column(String(255), comment='组合时限信息') + bundle_used_char = Column(String(50), comment='组合已用时间') + bundle_remain_char = Column(String(50), comment='组合剩余时间') + bundle_deadline_time = Column(DateTime, comment='组合截止时间') + show_unit_contact = Column(TINYINT(1), comment='是否显示单位联系方式') + pre_unit_name = Column(String(100), comment='上一单位') + pre_action_name = Column(String(100), comment='上一操作名称') + pre_human_name = Column(String(255), comment='上一操作人') + pre_act_opinion = Column(Text, comment='上一操作意见') + next_act_def_name = Column(String(100), comment='下一节点名称') + next_role_part_name = Column(String(255), comment='下一角色/单位') + next_role_name = Column(String(100), comment='下一角色名称') + next_act_property_id = Column(INTEGER(11), comment='下一操作属性ID') + last_act_flag = Column(TINYINT(1), index=True, comment='是否为最后一节点(0否,1是)') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmTaskAttachment(BaseModel): + __tablename__ = 't_d3i_dcm_task_attachment' + __table_args__ = ( + Index('idx_relation_id_delete_flag', 'relation_id', 'delete_flag'), + Index('idx_relation_type_relation_id', 'relation_type_id', 'relation_id'), + {'comment': '数字城管-部门待办附件'} + ) + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='部门待办任务ID') + rec_id = Column(BIGINT(20), comment='记录ID') + relation_type_id = Column(INTEGER(11), nullable=False, index=True, comment='关联类型ID') + relation_id = Column(BIGINT(20), nullable=False, index=True, comment='主关联ID') + relation_main_id = Column(BIGINT(20), comment='主关联ID(可为空)') + relation_sub_id = Column(BIGINT(20), comment='子关联ID(可为空)') + act_def_name = Column(String(255), comment='流程节点名称') + media_id = Column(BIGINT(20), nullable=False, unique=True, comment='媒体唯一ID') + media_path = Column(String(512), nullable=False, comment='服务器存储路径') + media_type = Column(String(50), nullable=False, index=True, comment='媒体类型:IMAGE, VIDEO, etc.') + media_name = Column(String(255), nullable=False, comment='原始文件名') + media_usage = Column(String(100), comment='使用场景,如“上报”、“回退”') + media_server_name = Column(String(100), nullable=False, comment='媒体服务器名称') + media_property = Column(INTEGER(11), comment='媒体属性') + media_uploaded_name = Column(String(255), comment='上传时的原始文件名') + media_shot = Column(String(255), comment='截图标识或路径') + media_label_type_id = Column(INTEGER(11), comment='标签类型ID') + media_url = Column(String(512), comment='内部访问URL') + media_default_url = Column(String(512), comment='外部可访问URL') + display_order = Column(INTEGER(11), comment='显示顺序') + store_type_id = Column(INTEGER(11), nullable=False, comment='存储类型ID') + special_item_image_type = Column(String(100), comment='特殊图片类型') + height = Column(INTEGER(11), comment='图片高度') + width = Column(INTEGER(11), comment='图片宽度') + send_flag = Column(TINYINT(4), comment='发送标志') + public_flag = Column(TINYINT(4), nullable=False, index=True, server_default=text("0"), + comment='公开标志:0=私有,1=公开') + unit_name = Column(String(255), index=True, comment='所属单位') + gen_thumb = Column(TINYINT(4), nullable=False, server_default=text("0"), comment='是否生成缩略图:0=否,1=是') + can_delete = Column(TINYINT(4), nullable=False, server_default=text("0"), comment='是否可删除:0=否,1=是') + upload_time = Column(DateTime, index=True, comment='上传时间') + create_human_id = Column(BIGINT(20), nullable=False, index=True, comment='创建人ID') + human_name = Column(String(255), comment='创建人姓名') + create_time = Column(DateTime, nullable=False, comment='创建时间') + update_time = Column(DateTime, comment='更新时间') + delete_reason = Column(Text, comment='删除原因') + delete_flag = Column(TINYINT(4), nullable=False, index=True, server_default=text("0"), + comment='删除标记:0=未删,1=已删') + delete_human_id = Column(BIGINT(20), comment='删除人ID') + delete_time = Column(DateTime, comment='删除时间') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmTaskFormDatum(BaseModel): + __tablename__ = 't_d3i_dcm_task_form_data' + __table_args__ = {'comment': '数字化城市管理信息系统人工任务表单数据表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='部门待办任务ID') + rec_id = Column(BIGINT(20), index=True, comment='记录ID') + act_property_id = Column(INTEGER(11), comment='任务属性ID') + address = Column(Text, comment='地址描述') + archive_time = Column(BIGINT(20), comment='归档时间戳') + cancel_time = Column(BIGINT(20), comment='取消时间戳') + biz_id = Column(INTEGER(11), index=True, comment='业务ID') + biz_name = Column(String(200), comment='业务名称') + card_num = Column(String(100), comment='证件号码') + cell_id = Column(INTEGER(11), comment='单元格ID') + cell_name = Column(String(200), comment='单元格名称') + check_msg_state_id = Column(INTEGER(11), comment='核查消息状态ID') + check_pic_num = Column(INTEGER(11), comment='核查图片数量') + check_pic_total_num = Column(INTEGER(11), comment='核查图片总数') + check_video_num = Column(INTEGER(11), comment='核查视频数量') + check_video_total_num = Column(INTEGER(11), comment='核查视频总数') + check_wav_num = Column(INTEGER(11), comment='核查音频数量') + check_wav_total_num = Column(INTEGER(11), comment='核查音频总数') + community_id = Column(INTEGER(11), comment='社区ID') + community_name = Column(String(200), comment='社区名称') + coordinate_x = Column(DECIMAL(10, 6), comment='经度') + coordinate_y = Column(DECIMAL(10, 6), comment='纬度') + create_time = Column(BIGINT(20), comment='创建时间戳') + damage_grade_id = Column(INTEGER(11), comment='损毁等级ID') + damage_grade_name = Column(String(100), comment='损毁等级名称') + deadline_char = Column(String(50), comment='时限描述') + deadline_time = Column(BIGINT(20), index=True, comment='处理截止时间戳') + dispatch_opinion = Column(String(500), comment='派遣意见') + dispatch_time = Column(BIGINT(20), comment='派遣时间戳') + display_property = Column(String(200), comment='显示属性') + display_style_id = Column(INTEGER(11), comment='显示样式ID') + district_id = Column(INTEGER(11), comment='区域ID') + district_name = Column(String(50), index=True, comment='所属区域') + duration_unit = Column(INTEGER(11), comment='时长单位') + duty_grid_id = Column(INTEGER(11), comment='责任网格ID') + duty_grid_name = Column(String(200), comment='责任网格名称') + event_desc = Column(Text, comment='问题描述') + event_grade_id = Column(INTEGER(11), comment='事件等级ID') + event_grade_name = Column(String(100), comment='事件等级名称') + event_level_id = Column(INTEGER(11), comment='事件级别ID') + event_level_name = Column(String(100), comment='事件级别名称') + event_src_id = Column(INTEGER(11), comment='问题来源ID') + event_src_name = Column(String(100), comment='问题来源') + event_type_code = Column(String(50), comment='问题类型编码') + event_type_id = Column(INTEGER(11), index=True, comment='问题类型ID') + event_type_name = Column(String(100), comment='问题类型') + fifth_type_id = Column(INTEGER(11), comment='第五级类型ID') + fifth_type_name = Column(String(100), comment='第五级类型名称') + forth_type_id = Column(INTEGER(11), comment='第四级类型ID') + forth_type_name = Column(String(100), comment='第四级类型名称') + func_deadline = Column(BIGINT(20), comment='职能部门截止时间戳') + func_deal_time = Column(BIGINT(20), comment='职能部门处理时间戳') + func_limit_char = Column(String(50), comment='职能部门时限描述') + func_part_id = Column(INTEGER(11), comment='职能部门ID') + func_part_name = Column(String(200), comment='职能部门名称') + func_time_state_id = Column(INTEGER(11), comment='职能部门时间状态ID') + gather_flag = Column(String(50), comment='汇总标识') + link_field_display_value = Column(String(500), comment='关联字段显示值') + link_field_value = Column(String(500), comment='关联字段值') + main_type_id = Column(INTEGER(11), comment='大类ID') + main_type_name = Column(String(100), comment='大类名称') + media_check_num = Column(INTEGER(11), comment='媒体核查数量') + media_check_total_num = Column(INTEGER(11), comment='媒体核查总数') + media_lost_flag = Column(INTEGER(11), comment='媒体丢失标识') + media_upload_num = Column(INTEGER(11), comment='媒体上传数量') + media_upload_state = Column(String(50), comment='媒体上传状态') + media_upload_total_num = Column(INTEGER(11), comment='媒体上传总数') + media_url = Column(String(512), comment='内部访问URL') + media_verify_total_num = Column(INTEGER(11), comment='媒体核实总数') + mms_pic_path = Column(String(500), comment='彩信图片路径') + new_inst_cond_id = Column(INTEGER(11), comment='立案条件ID') + new_inst_cond_name = Column(String(200), comment='立案条件') + occur_time = Column(BIGINT(20), comment='发生时间戳') + part_code = Column(String(100), comment='部件编码') + patrol_deal_flag = Column(INTEGER(11), comment='巡查处置标识') + patrol_id = Column(INTEGER(11), comment='巡查员ID') + patrol_name = Column(String(200), comment='巡查员名称') + pos_type = Column(String(50), comment='位置类型') + proc_ard_state_id = Column(INTEGER(11), comment='处理仲裁状态ID') + proc_enq_state_id = Column(INTEGER(11), comment='处理询问状态ID') + proc_start_time = Column(BIGINT(20), comment='处理开始时间戳') + proc_sup_state_id = Column(INTEGER(11), comment='处理监督状态ID') + proc_time_state_id = Column(TINYINT(4), comment='处理流程状态ID') + rec_deadline = Column(Float(asdecimal=True), comment='记录时限') + rec_disp_num = Column(String(50), comment='显示编号') + rec_remain = Column(Float(asdecimal=True), comment='记录剩余时间') + rec_remain_char = Column(String(50), comment='记录剩余时间描述') + rec_type_id = Column(INTEGER(11), comment='类型ID') + rec_type_name = Column(String(100), comment='案件类型') + rec_used = Column(Float(asdecimal=True), comment='记录已用时间') + rec_used_char = Column(String(50), comment='记录已用时间描述') + rec_warning = Column(Float(asdecimal=True), comment='记录预警时间') + refresh_flag = Column(INTEGER(11), comment='刷新标识') + refresh_start_time = Column(BIGINT(20), comment='刷新开始时间戳') + refresh_time = Column(BIGINT(20), comment='刷新时间戳') + report_id = Column(BIGINT(20), comment='上报ID') + report_pic_num = Column(INTEGER(11), comment='上报图片数量') + report_pic_total_num = Column(INTEGER(11), comment='上报图片总数') + report_video_num = Column(INTEGER(11), comment='上报视频数量') + report_video_total_num = Column(INTEGER(11), comment='上报视频总数') + report_wav_num = Column(INTEGER(11), comment='上报音频数量') + report_wav_total_num = Column(INTEGER(11), comment='上报音频总数') + street_id = Column(INTEGER(11), comment='街道ID') + street_name = Column(String(200), comment='街道名称') + sub_type_id = Column(INTEGER(11), comment='小类ID') + sub_type_name = Column(String(100), comment='小类名称') + task_num = Column(String(50), comment='任务号') + third_type_id = Column(INTEGER(11), comment='第三级类型ID') + third_type_name = Column(String(100), comment='第三级类型名称') + time_area_id = Column(INTEGER(11), comment='时段ID') + time_area_name = Column(String(100), comment='时段名称') + unique_id = Column(String(100), comment='唯一标识') + urgent_flag = Column(INTEGER(11), comment='紧急标识') + urgent_memo = Column(String(500), comment='紧急备注') + verify_msg_state_id = Column(INTEGER(11), comment='核实消息状态ID') + verify_pic_total_num = Column(INTEGER(11), comment='核实图片总数') + verify_video_total_num = Column(INTEGER(11), comment='核实视频总数') + verify_wav_total_num = Column(INTEGER(11), comment='核实音频总数') + video_device_id = Column(BIGINT(20), comment='视频设备ID') + video_param = Column(String(500), comment='视频参数') + view_angle = Column(String(100), comment='视角') + view_image_name = Column(String(200), comment='视图图片名称') + view_image_x = Column(Float(asdecimal=True), comment='视图图片X坐标') + view_image_y = Column(Float(asdecimal=True), comment='视图图片Y坐标') + view_pos_x = Column(Float(asdecimal=True), comment='视图位置X坐标') + view_pos_y = Column(Float(asdecimal=True), comment='视图位置Y坐标') + warning_time = Column(BIGINT(20), comment='处理预警时间戳') + sys_id = Column(INTEGER(11), index=True, comment='系统ID') + form_id = Column(INTEGER(11), comment='表单ID') + verify_pic_num = Column(INTEGER(11), comment='核实图片数量') + verify_wav_num = Column(INTEGER(11), comment='核实音频数量') + verify_video_num = Column(INTEGER(11), comment='核实视频数量') + media_verify_num = Column(INTEGER(11), comment='媒体核实数量') + road_type_id = Column(INTEGER(11), comment='道路类型ID') + road_name = Column(String(200), comment='道路名称') + road_id = Column(INTEGER(11), comment='道路ID') + archive_cond_id = Column(INTEGER(11), comment='归档条件ID') + archive_cond = Column(String(100), comment='归档条件') + road_type_name = Column(String(100), comment='道路类型名称') + area_type_id = Column(INTEGER(11), comment='区域类型ID') + equal_group_id = Column(BIGINT(20), comment='等值组ID') + regather_msg_state_id = Column(INTEGER(11), comment='重新采集消息状态ID') + new_inst_advise = Column(String(500), comment='立案建议') + event_marks = Column(String(500), comment='事件标记') + archive_type_id = Column(INTEGER(11), comment='归档类型ID') + report_time_segment_id = Column(INTEGER(11), comment='上报时段ID') + enable_check_msg = Column(INTEGER(11), comment='启用核查消息') + revise_opinion = Column(String(500), comment='修订意见') + report_area_limit_id = Column(INTEGER(11), comment='上报区域限制ID') + deduction = Column(String(100), comment='扣减') + attach_rec_flag = Column(String(50), comment='附件记录标识') + sixth_type_id = Column(INTEGER(11), comment='第六级类型ID') + sixth_type_name = Column(String(100), comment='第六级类型名称') + seventh_type_id = Column(INTEGER(11), comment='第七级类型ID') + seventh_type_name = Column(String(100), comment='第七级类型名称') + max_event_type_id = Column(INTEGER(11), comment='最大事件类型ID') + max_event_type_name = Column(String(200), comment='最大事件类型名称') + occur_num = Column(INTEGER(11), comment='发生次数') + check_send_time = Column(BIGINT(20), comment='核查发送时间戳') + check_reply_time = Column(BIGINT(20), comment='核查回复时间戳') + duty_region_id = Column(INTEGER(11), comment='责任区域ID') + duty_region_name = Column(String(200), comment='责任区域名称') + lonlat_x = Column(Float(asdecimal=True), comment='经纬度X') + lonlat_y = Column(Float(asdecimal=True), comment='经纬度Y') + func_bundle_deadline = Column(BIGINT(20), comment='职能捆绑截止时间戳') + third_unique_id = Column(String(100), comment='第三方唯一标识') + event_property_id = Column(INTEGER(11), comment='事件属性ID') + event_property_name = Column(String(200), comment='事件属性名称') + city_village_flag = Column(String(50), comment='城乡标识') + specify_func_id = Column(INTEGER(11), comment='指定职能部门ID') + specify_competent_func_id = Column(INTEGER(11), comment='指定主管职能部门ID') + specify_func_name = Column(String(200), comment='指定职能部门名称') + specify_competent_func_name = Column(String(200), comment='指定主管职能部门名称') + super_rec_id = Column(BIGINT(20), comment='上级记录ID') + split_rec_flag = Column(INTEGER(11), comment='拆分记录标识') + site_num = Column(String(50), comment='站点编号') + difficult_type_id = Column(INTEGER(11), comment='困难类型ID') + event_district_grade_id = Column(INTEGER(11), comment='事件区域等级ID') + event_district_grade_name = Column(String(100), comment='事件区域等级名称') + duty_district_id = Column(INTEGER(11), comment='责任区域ID') + duty_street_id = Column(INTEGER(11), comment='责任街道ID') + duty_community_id = Column(INTEGER(11), comment='责任社区ID') + duty_district_name = Column(String(200), comment='责任区域名称') + duty_street_name = Column(String(200), comment='责任街道名称') + duty_community_name = Column(String(200), comment='责任社区名称') + cus_grid_code = Column(String(100), comment='自定义网格编码') + accepter_id = Column(INTEGER(11), comment='受理人ID') + accepter_name = Column(String(100), comment='受理人姓名') + auto_check_count = Column(INTEGER(11), comment='自动核查次数') + other_task_num = Column(String(100), comment='第三方任务号') + force_handle_flag = Column(String(50), comment='强制处理标识') + func_part_list_id = Column(String(100), comment='职能部门列表ID') + func_part_list_name = Column(String(200), comment='职能部门列表名称') + custom_deadline = Column(BIGINT(20), comment='自定义截止时间戳') + act_record_id = Column(BIGINT(20), comment='操作记录ID') + tell_num = Column(String(50), comment='联系电话') + reply_opinion = Column(String(500), comment='回复意见') + send_from_type = Column(String(50), comment='发送来源类型') + func_forbid_reporter_info_flag = Column(TINYINT(4), comment='是否禁止举报人信息') + property_company_id = Column(BIGINT(20), comment='物业公司ID') + accept_status = Column(String(50), comment='受理状态') + shop_name = Column(String(200), comment='商铺名称') + func_custom_limit = Column(String(50), comment='职能部门自定义时限') + squadron_id = Column(BIGINT(20), comment='中队ID') + squadron_name = Column(String(200), comment='中队名称') + reply_intime = Column(TINYINT(4), comment='是否两小时回复(0无需回复,1待回复,2已回复,3超时,4无需回复已恢复)') + locked_flag = Column(INTEGER(11), comment='锁定标识') + check_type_id = Column(INTEGER(11), comment='核查类型ID') + transited_flag = Column(INTEGER(11), comment='转交标识') + rec_analysis_type_id = Column(INTEGER(11), comment='记录分析类型ID') + deal_evaluate_ids = Column(String(200), comment='处置评价ID列表') + newinst_no_transit = Column(String(50), comment='立案不转交') + no_return_visit_flag = Column(INTEGER(11), comment='无需回访标识') + common_rec_type_flag = Column(String(50), comment='通用记录类型标识') + common_rec_attr_flag = Column(String(50), comment='通用记录属性标识') + main_rec_id = Column(BIGINT(20), comment='主记录ID') + send_pub_check_task_flag = Column(INTEGER(11), comment='发送公共核查任务标识') + patroltask_deadline_time = Column(BIGINT(20), comment='巡查任务截止时间戳') + shop_id = Column(BIGINT(20), comment='商铺ID') + spec_type_name = Column(String(100), comment='特殊类型名称') + law_duty_grid_id = Column(INTEGER(11), comment='法律责任网格ID') + law_duty_grid_name = Column(String(200), comment='法律责任网格名称') + proc_account_state_id = Column(INTEGER(11), comment='处理账户状态ID') + spec_type_id = Column(INTEGER(11), comment='特殊类型ID') + reply_flag = Column(String(50), comment='回复标识') + first_depart_name = Column(String(100), comment='一级专业部门') + second_depart_name = Column(String(100), comment='二级专业部门') + reply_intime_deadline = Column(BIGINT(20), comment='两小时回复截止时间戳') + supervision_check_state_id = Column(INTEGER(11), comment='监督核查状态ID') + urgent_level = Column(TINYINT(4), comment='紧急程度(0正常,1紧急)') + self_deal_msg_state_id = Column(INTEGER(11), comment='自行处置消息状态ID') + duty_grid_type_id = Column(INTEGER(11), comment='责任网格类型ID') + deal_duty_grid_type_id = Column(INTEGER(11), comment='处置责任网格类型ID') + deal_duty_grid_id = Column(INTEGER(11), comment='处置责任网格ID') + deal_duty_grid_name = Column(String(200), comment='处置责任网格名称') + site_id = Column(BIGINT(20), comment='站点ID') + media_self_deal_total_num = Column(INTEGER(11), comment='自行处置媒体总数') + media_self_deal_num = Column(INTEGER(11), comment='自行处置媒体数量') + self_deal_pic_total_num = Column(INTEGER(11), comment='自行处置图片总数') + self_deal_pic_num = Column(INTEGER(11), comment='自行处置图片数量') + self_deal_wav_total_num = Column(INTEGER(11), comment='自行处置音频总数') + self_deal_wav_num = Column(INTEGER(11), comment='自行处置音频数量') + self_deal_video_total_num = Column(INTEGER(11), comment='自行处置视频总数') + self_deal_video_num = Column(INTEGER(11), comment='自行处置视频数量') + review_msg_state_id = Column(INTEGER(11), comment='复核消息状态ID') + media_review_total_num = Column(INTEGER(11), comment='复核媒体总数') + media_review_num = Column(INTEGER(11), comment='复核媒体数量') + review_pic_total_num = Column(INTEGER(11), comment='复核图片总数') + review_pic_num = Column(INTEGER(11), comment='复核图片数量') + review_wav_total_num = Column(INTEGER(11), comment='复核音频总数') + review_wav_num = Column(INTEGER(11), comment='复核音频数量') + review_video_total_num = Column(INTEGER(11), comment='复核视频总数') + review_video_num = Column(INTEGER(11), comment='复核视频数量') + public_flag = Column(TINYINT(4), comment='公开标志') + whistle_flag = Column(String(50), comment='吹哨标识') + jx_id = Column(BIGINT(20), comment='警讯ID') + jx_jxmc = Column(String(200), comment='警讯名称') + jx_design_type = Column(String(100), comment='警讯设计类型') + rec_category_id = Column(INTEGER(11), comment='记录类别ID') + repeat_state = Column(String(50), comment='重复状态') + cg_area = Column(String(100), comment='城管区域') + hw_area = Column(String(100), comment='环卫区域') + sz_area = Column(String(100), comment='市政区域') + device_guid = Column(String(100), comment='设备GUID') + proc_press_state_id = Column(INTEGER(11), comment='处理压力状态ID') + hot_area = Column(String(100), comment='热点区域') + report_state = Column(String(50), comment='上报状态') + dispose_state = Column(INTEGER(11), comment='处置状态') + pre_dispose_state = Column(String(50), comment='预处置状态') + undertake_user_name = Column(String(50), comment='承办人员') + undertake_phone = Column(String(50), comment='联系电话') + deal_person_org = Column(String(50), comment='承办部门') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmTaskExtendedInfo(BaseModel): + __tablename__ = 't_d3i_dcm_task_extended_info' + __table_args__ = {'comment': '扩展信息'} + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + subtype_field_name = Column(String(100), comment='子类型字段名称') + content_range = Column(String(255), server_default=text("''"), comment='内容范围') + control_type = Column(String(50), comment='控件类型') + display_name = Column(String(100), comment='显示名称') + data_type_id = Column(String(50), comment='数据类型ID') + null_flag = Column(String(20), comment='是否可空标识(0:不可空,1:可空)') + list_content = Column(Text, comment='下拉框选项内容') + subtype_id = Column(String(50), comment='子类型ID') + field_value = Column(String(255), comment='字段值') + rec_id = Column(BIGINT(20), nullable=False, comment='记录ID') + field_id = Column(String(50), comment='字段ID') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmTaskMoreInfo(BaseModel): + __tablename__ = 't_d3i_dcm_task_more_info' + __table_args__ = {'comment': '更多信息'} + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + msg_id = Column(BIGINT(20), comment='消息ID') + rec_id = Column(BIGINT(20), comment='记录ID') + msg_type_id = Column(BIGINT(20), comment='消息类型ID') + msg_type = Column(String(50), comment='消息类型名称') + msg_info = Column(String(255), comment='消息详情') + create_time = Column(String(64), comment='创建时间') + human_id = Column(INTEGER(11), comment='人员ID') + human_name = Column(String(50), comment='人员姓名') + role_name = Column(String(50), comment='角色名称') + ex_info_id = Column(BIGINT(20), comment='扩展信息ID') + ex_info_msg = Column(String(255), comment='扩展信息内容') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmTaskFileUpload(BaseModel): + __tablename__ = 't_d3i_dcm_task_file_upload' + __table_args__ = {'comment': '文件上传关联表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + dcm_task_attachment_id = Column(BIGINT(20), nullable=False, comment='附件ID') + dcm_media_id = Column(BIGINT(20), nullable=False, comment='附件ID(数字城管)') + oa_media_id = Column(String(50), nullable=False, server_default=text(""), comment='附件ID(OA)') + file_hash = Column(String(256), nullable=False, comment='文件has值') + status = Column(INTEGER(11), nullable=False, server_default=text("0"), comment='0:没有上传或失败,1 上传成功') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iDcmPushStatu(BaseModel): + __tablename__ = 't_d3i_dcm_push_status' + __table_args__ = {'comment': '推送OA状态记录表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + push_task_status = Column(INTEGER(11), nullable=False, server_default=text("0"), comment='推送待办工单状态') + push_task_detail_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单详情状态') + push_task_attachment_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单附件状态') + push_task_extend_info_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单扩展信息状态') + push_task_file_upload_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='上传待办工单文件状态') + push_task_more_info_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单更多信息状态') + push_task_process_info_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单处理过程状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TToken(BaseModel): + __tablename__ = 't_token' + __table_args__ = {'comment': '认证token'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + platform = Column(String(20, 'utf8mb4_unicode_ci'), comment='平台') + token = Column(String(500, 'utf8mb4_unicode_ci'), comment='令牌') + deleted = Column(BIT(1)) + creator = Column(String(64, 'utf8mb4_unicode_ci'), comment='创建者') + create_time = Column(DateTime, server_default=text("current_timestamp()"), comment='创建时间') + updater = Column(String(64, 'utf8mb4_unicode_ci'), comment='更新者') + update_time = Column(DateTime, server_default=text("current_timestamp() ON UPDATE current_timestamp()"), + comment='更新时间') + + +class TD3iDcmApplyRollback(BaseModel): + __tablename__ = 't_d3i_dcm_apply_rollback' + __table_args__ = {'comment': '申请回退表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键ID') + dcm_task_id = Column(ForeignKey('t_d3i_dcm_task.id'), nullable=False, index=True, comment='唯一标志') + task_number = Column(String(64), nullable=False, comment='任务号') + act_id = Column(String(64), nullable=False, comment='工单ID') + reply_part_id = Column(BIGINT(20), comment='回复部门ID') + ard_level = Column(BIGINT(20), comment='延期等级') + ard_type_id = Column(BIGINT(20), comment='延期类型ID') + opinion = Column(Text, comment='申请意见') + apply_type = Column(String(64), comment='申请类型(拒签、处置阶段照片未公开)') + trans_info = Column(String(255), comment='流转信息') + flow_token = Column(String(256), comment='流令牌') + attachments = Column(Text, comment='附件(多个用逗号分隔)') + status = Column(BIGINT(20), nullable=False, comment='提交状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + dcm_task = relationship('TD3iDcmTask') + + +class TD3iGovsOrderMaster(BaseModel): + __tablename__ = 't_d3i_govs_order_master' + __table_args__ = {'comment': '省12345工单'} + + id = Column(BIGINT(20), primary_key=True, index=True, comment='工单唯一ID') + belong_dept = Column(Text, comment='所属部门') + order_id = Column(String(50), index=True, comment='工单编号') + order_no = Column(String(50), index=True, comment='工单号') + order_type = Column(String(50), comment='表单类型') + order_source = Column(Text, comment='诉求来源') + order_source_detail = Column(Text, comment='诉求来源详情') + order_status = Column(String(50), index=True, comment='工单状态') + order_user_id = Column(String(64), comment='用户ID') + order_user_name = Column(String(50), comment='来电人姓名') + order_user_sex = Column(String(50), comment='来电人性别') + order_user_phone2 = Column(String(20), comment='备用联系电话') + order_handle_way = Column(Text, comment='处理方式') + order_invalid_type = Column(Text, comment='工单作废原因') + master_id = Column(BIGINT(20), index=True, comment='工单主表ID') + call_number = Column(String(20), index=True, comment='来电号码') + contact_number = Column(String(20), index=True, comment='联系电话') + title = Column(Text, comment='工单标题') + call_time = Column(DateTime, index=True, comment='来电时间') + first_order_status = Column(String(10), comment='一级状态编码') + secord_order_status = Column(String(10), comment='二级状态编码') + atomic_order_status = Column(String(10), comment='原子状态编码') + area_code = Column(String(10), comment='区域代码') + area_code_city = Column(String(50), comment='市区域代码') + area_code_area = Column(String(50), comment='区区域代码') + area_code_street = Column(String(50), comment='街道区域代码') + address_detail = Column(Text, comment='详细地址') + case_lnglat = Column(String(50), comment='地理坐标') + case_accord_type_one_name = Column(String(50), comment='诉求归口一级') + case_accord_type_two_name = Column(String(50), comment='诉求归口二级') + case_accord_type_three_name = Column(String(50), comment='诉求归口三级') + case_accord_type_four_name = Column(String(50), comment='四级事项分类') + case_accord_type_five_name = Column(String(50), comment='五级事项分类') + case_accord_ext = Column(Text, comment='扩展分类说明') + case_content = Column(Text, comment='诉求内容') + case_goal = Column(Text, comment='诉求目的') + case_labels = Column(Text, comment='工单标签列表') + case_public = Column(String(10), comment='是否公开') + case_type = Column(Text, comment='案件类型') + case_is_urgent = Column(String(10), comment='紧急程度') + case_comple_time = Column(DateTime, comment='案件办结时间') + first_level_affiliation = Column(Text, comment='一级归属单位') + second_level_affiliation = Column(Text, comment='二级归属单位') + third_level_affiliation = Column(Text, comment='三级归属单位') + fourth_level_affiliation = Column(Text, comment='四级归属单位') + fifth_level_affiliation = Column(Text, comment='五级归属单位') + sixth_level_affiliation = Column(Text, comment='六级归属单位') + seventh_level_affiliation = Column(Text, comment='七级归属单位') + case_accord_code = Column(String(50), comment='事项编码') + info_protect = Column(String(10), comment='信息保护') + case_is_visit = Column(String(10), comment='是否回访') + service_object_type = Column(String(50), comment='服务对象类型') + hotspot = Column(String(10), comment='是否热点事件') + result_satisfied = Column(Text, comment='结果满意度') + first_vist_satisfied = Column(Text, comment='首次走访满意度') + contact_timely = Column(String(50), comment='是否及时联系') + distribute_type = Column(String(50), comment='分派类型') + dept_type = Column(Text, comment='部门类型') + dept_name = Column(Text, comment='部门名称') + active_dept_ids = Column(Text, comment='当前处理部门ID列表') + active_dept_name = Column(String(50), comment='当前处理部门名称') + case_solve = Column(Text, comment='处理结果') + supervise_type = Column(Text, comment='监督类型') + leader_indicate = Column(Text, comment='领导批示') + extension = Column(Text, comment='扩展字段') + org_id = Column(String(50), comment='组织ID') + org_name = Column(Text, comment='组织名称') + knowledge_quote = Column(Text, comment='知识引用') + special_type = Column(Text, comment='特殊类型') + attachment_ids = Column(Text, comment='附件ID列表') + attachment_list = Column(Text, comment='附件列表JSON') + file_exist = Column(Text, comment='是否存在附件') + record_id = Column(String(50), comment='通话记录ID') + call_end_time = Column(DateTime, comment='通话结束时间') + call_total_time = Column(String(20), comment='通话总时长') + plan_finish_time = Column(DateTime, comment='计划完成时间') + remark = Column(Text, comment='备注') + tenant_id = Column(BIGINT(20), index=True, comment='租户ID') + erge_revoke_plug = Column(Text, comment='撤销插件') + exist_quoto_info = Column(Text, comment='是否存在引用信息') + process_instance_id = Column(String(100), index=True, comment='流程实例ID') + sound_recording_address_list = Column(Text, comment='录音文件路径列表JSON') + visit_count = Column(INTEGER(11), comment='走访次数') + visit_adv_content = Column(Text, comment='走访建议内容') + return_visit_reason = Column(Text, comment='回访原因') + residue_date = Column(Text, comment='剩余天数') + whether_approval = Column(String(10), comment='是否审批') + over_time_warning_flag = Column(String(10), comment='超时预警标志') + create_no = Column(String(20), comment='创建编号') + belong_platform = Column(String(50), comment='所属平台') + back_count = Column(String(100), comment='回退次数') + tripartite_call_record_info = Column(Text, comment='三方通话记录') + knowledge_references = Column(Text, comment='知识参考JSON') + current_processing_platform = Column(Text, comment='当前处理平台') + judgment_flag = Column(String(10), comment='判定标志') + thrid_order_id = Column(Text, comment='第三方工单ID') + is_dispatch_accurate = Column(String(10), comment='是否精准分派') + is_coordination = Column(String(10), comment='是否协调') + coordination_time = Column(DateTime, comment='协调时间') + creator_id = Column(BIGINT(20), comment='创建人ID') + create_by = Column(Text, comment='创建人姓名') + updator_id = Column(BIGINT(20), comment='更新人ID') + update_by = Column(Text, comment='更新人姓名') + plan_sign_time = Column(DateTime, comment='计划签收时间') + claim_status = Column(String(64), comment='签收状态') + plan_back_time = Column(DateTime, comment='退回截止时间') + handle_time = Column(DateTime, comment='交办下级时间') + back_time = Column(DateTime, comment='下级退回时间') + complete_time = Column(DateTime, comment='下级办结时间') + update_date = Column(DateTime, comment='原始更新时间') + next_task_id = Column(String(64), comment='下一个任务ID') + govs_sign = Column(TINYINT(1), comment='是否已在省12345签收,1:签收,0:未签收') + created_at = Column(DateTime, nullable=False, index=True, server_default=text("current_timestamp()"), + comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, index=True, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='更新者') + + +class TD3iGovsOrderProces(BaseModel): + __tablename__ = 't_d3i_govs_order_process' + __table_args__ = {'comment': '省12345工单处理流程'} + + id = Column(BIGINT(20), primary_key=True, comment='工单处理记录唯一ID') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), index=True, + comment='关联工单主表ID(t_d3i_govs_order_master.id)') + tenant_id = Column(BIGINT(20), index=True, comment='租户ID') + plan_sign_time = Column(DateTime, comment='计划签收时间') + plan_finish_time = Column(DateTime, comment='计划完成时间') + plan_back_time = Column(DateTime, comment='计划退回时间') + deal_date = Column(DateTime, index=True, comment='实际处理时间') + hand_over_time = Column(String(20, 'utf8mb4_unicode_ci'), comment='交接时间(0表示未交接)') + sign_over_time = Column(String(20, 'utf8mb4_unicode_ci'), comment='签收超时时间') + origin_plan_finish_time = Column(DateTime, comment='原始计划完成时间') + origin_plan_sign_time = Column(DateTime, comment='原始计划签收时间') + order_id = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='工单编号') + order_no = Column(String(100, 'utf8mb4_unicode_ci'), index=True, comment='工单流水号(含子单标识)') + process_instance_id = Column(String(64, 'utf8mb4_unicode_ci'), index=True, comment='流程实例ID') + order_status = Column(String(10, 'utf8mb4_unicode_ci'), index=True, comment='工单状态编码') + is_over_time = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否超期(0-否,1-是)') + is_sign_over_time = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否签收超时(0-否,1-是)') + action_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='当前操作动作名称') + deal_type = Column(String(100, 'utf8mb4_unicode_ci'), comment='处理类型') + task_id = Column(String(64, 'utf8mb4_unicode_ci'), index=True, comment='当前任务ID(UUID)') + next_task_id = Column(String(64, 'utf8mb4_unicode_ci'), index=True, comment='下一任务ID(流程节点)') + next_action_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='下一处理动作名称') + next_handle = Column(String(50, 'utf8mb4_unicode_ci'), comment='下一处理动作名称') + next_handle_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='下一处理动作详细名称') + handler_user_ids = Column(String(500, 'utf8mb4_unicode_ci'), comment='当前处理人ID列表') + handler_user_names = Column(String(500, 'utf8mb4_unicode_ci'), comment='当前处理人姓名列表') + handler_org_ids = Column(String(1000, 'utf8mb4_unicode_ci'), comment='当前处理部门ID列表') + handler_org_names = Column(String(500, 'utf8mb4_unicode_ci'), comment='当前处理部门名称列表') + next_handler_user_ids = Column(String(500, 'utf8mb4_unicode_ci'), comment='下一处理人ID列表') + next_handler_user_names = Column(String(500, 'utf8mb4_unicode_ci'), comment='下一处理人姓名列表') + next_org_ids = Column(String(500, 'utf8mb4_unicode_ci'), comment='下一处理部门ID列表') + next_org_names = Column(String(500, 'utf8mb4_unicode_ci'), comment='下一处理部门名称列表') + dispatch_order_id = Column(String(100, 'utf8mb4_unicode_ci'), comment='派发工单ID') + to_master_id = Column(BIGINT(20), comment='目标主表ID') + to_tenant_id = Column(BIGINT(20), comment='目标租户ID') + to_area_code = Column(String(20, 'utf8mb4_unicode_ci'), comment='目标区域代码') + to_dept_id = Column(BIGINT(20), comment='目标部门ID') + dispatch_value = Column(String(20, 'utf8mb4_unicode_ci'), comment='派发值(XP表示下派)') + has_dispatch_process = Column(TINYINT(4), comment='是否有派发流程(0-否,1-是)') + contact_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='联系人姓名') + contact_time = Column(DateTime, comment='联系时间') + contact_type = Column(String(20, 'utf8mb4_unicode_ci'), comment='联系类型(电话/短信等)') + adv_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='处理建议/提醒内容') + remarks = Column(Text(collation='utf8mb4_unicode_ci'), comment='备注信息') + formal_reply = Column(Text(collation='utf8mb4_unicode_ci'), comment='正式回复内容') + reply_to_people = Column(String(100, 'utf8mb4_unicode_ci'), comment='回复对象') + return_reason = Column(String(500, 'utf8mb4_unicode_ci'), comment='退回原因') + notice_org_id = Column(BIGINT(20), comment='通知组织ID') + line_key = Column(String(100, 'utf8mb4_unicode_ci'), comment='线路标识') + current_task_status = Column(String(50, 'utf8mb4_unicode_ci'), comment='当前任务状态') + visit_type = Column(String(50, 'utf8mb4_unicode_ci'), comment='访问类型(如上门、电话)') + attachment_dto_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件列表(JSON数组)') + child_order_processes = Column(MEDIUMTEXT, comment='子流程处理记录(JSON数组,支持递归嵌套)') + created_at = Column(DateTime, nullable=False, index=True, server_default=text("current_timestamp()"), + comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, index=True, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsOrderAttachment(BaseModel): + __tablename__ = 't_d3i_govs_order_attachment' + __table_args__ = {'comment': '省12345工单附件'} + + id = Column(BIGINT(20), primary_key=True, comment='附件唯一ID') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), index=True, + comment='关联工单主表ID(t_d3i_govs_order_master.id)') + order_id = Column(String(50), index=True, comment='工单编号') + file_path = Column(String(500), comment='文件路径(内网地址)') + out_file_path = Column(String(500), comment='外网文件路径') + attach_name = Column(String(200), comment='附件名称') + to_tenant_id = Column(String(50), comment='目标租户ID') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsOrderDetail(BaseModel): + __tablename__ = 't_d3i_govs_order_detail' + __table_args__ = {'comment': '省12345工单详情(接口3:工单详情接口完整数据)'} + + id = Column(BIGINT(20), primary_key=True, comment='详情记录唯一ID') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), index=True, comment='关联工单主表ID') + order_id = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='工单编号') + order_no = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='工单号') + tenant_id = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='租户ID') + order_status = Column(String(10, 'utf8mb4_unicode_ci'), index=True, comment='工单状态码') + order_status_for_view = Column(String(50, 'utf8mb4_unicode_ci'), comment='工单状态显示值') + first_order_status = Column(String(10, 'utf8mb4_unicode_ci'), comment='一级状态编码') + secord_order_status = Column(String(10, 'utf8mb4_unicode_ci'), comment='二级状态编码') + atomic_order_status = Column(String(10, 'utf8mb4_unicode_ci'), comment='原子状态编码') + order_invalid_type = Column(Text(collation='utf8mb4_unicode_ci'), comment='工单作废原因') + order_finish_time = Column(DateTime, comment='工单完成时间') + case_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='诉求内容') + case_goal = Column(Text(collation='utf8mb4_unicode_ci'), comment='诉求目的') + title = Column(String(500, 'utf8mb4_unicode_ci'), comment='工单标题') + case_labels = Column(Text(collation='utf8mb4_unicode_ci'), comment='工单标签列表') + case_public = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否公开') + hotspot = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否热点事件') + case_is_urgent = Column(String(10, 'utf8mb4_unicode_ci'), index=True, comment='紧急程度(一般/紧急/特急)') + case_is_visit = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否回访(是/否)') + info_protect = Column(String(10, 'utf8mb4_unicode_ci'), comment='信息保护(是/否)') + case_accord_type_one_name = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='诉求归口一级') + case_accord_type_two_name = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='诉求归口二级') + case_accord_type_three_name = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='诉求归口三级') + case_accord_type_four_name = Column(String(50, 'utf8mb4_unicode_ci'), comment='诉求归口四级') + case_accord_type_five_name = Column(String(50, 'utf8mb4_unicode_ci'), comment='诉求归口五级') + case_accord_code = Column(String(50, 'utf8mb4_unicode_ci'), comment='事项编码') + first_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='一级归属单位') + second_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='二级归属单位') + third_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='三级归属单位') + fourth_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='四级归属单位') + fifth_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='五级归属单位') + sixth_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='六级归属单位') + seventh_level_affiliation = Column(Text(collation='utf8mb4_unicode_ci'), comment='七级归属单位') + appeal_dept = Column(String(100, 'utf8mb4_unicode_ci'), comment='诉求部门') + order_source = Column(String(50, 'utf8mb4_unicode_ci'), comment='诉求来源(电话/互联网)') + order_source_detail = Column(String(50, 'utf8mb4_unicode_ci'), comment='诉求来源详情(12345/随手拍)') + order_source_for_view = Column(String(50, 'utf8mb4_unicode_ci'), comment='诉求来源显示值') + belong_platform = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='所属平台代码') + belong_platform_name = Column(String(50, 'utf8mb4_unicode_ci'), comment='受理平台名称') + current_processing_platform = Column(Text(collation='utf8mb4_unicode_ci'), comment='当前处理平台') + service_object_type = Column(String(50, 'utf8mb4_unicode_ci'), comment='服务对象类型(投诉举报/咨询/建议等)') + order_type = Column(String(50, 'utf8mb4_unicode_ci'), comment='表单类型(个人/企业/其他)') + form_type = Column(String(50, 'utf8mb4_unicode_ci'), comment='表单类型代码') + area_code_city = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='市区域代码') + area_code_area = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='区区域代码') + area_code_street = Column(String(50, 'utf8mb4_unicode_ci'), comment='街道区域代码') + address_detail = Column(String(500, 'utf8mb4_unicode_ci'), comment='详细地址') + case_lnglat = Column(String(100, 'utf8mb4_unicode_ci'), comment='地理坐标') + call_number = Column(String(20, 'utf8mb4_unicode_ci'), comment='来电号码') + call_number_for_dh = Column(String(20, 'utf8mb4_unicode_ci'), comment='来电号码(脱敏)') + raw_call_numer = Column(String(20, 'utf8mb4_unicode_ci'), comment='原始来电号码') + contact_number = Column(String(20, 'utf8mb4_unicode_ci'), comment='联系电话') + raw_contact_number = Column(String(20, 'utf8mb4_unicode_ci'), comment='原始联系电话') + contact_number_for_dh = Column(String(20, 'utf8mb4_unicode_ci'), comment='联系电话(脱敏)') + call_time = Column(DateTime, index=True, comment='来电时间') + order_sound_record_id = Column(String(50, 'utf8mb4_unicode_ci'), comment='通话记录ID') + create_date = Column(DateTime, index=True, comment='创建日期') + update_date = Column(DateTime, index=True, comment='更新日期') + plan_finish_time = Column(DateTime, comment='计划完成时间') + plan_sign_time = Column(DateTime, comment='计划签收时间') + judgment_flag = Column(String(10, 'utf8mb4_unicode_ci'), comment='判定标志') + is_coordination = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否协调') + coordination_time = Column(DateTime, comment='协调时间') + thrid_order_id = Column(Text(collation='utf8mb4_unicode_ci'), comment='第三方工单ID') + relate_order_ids = Column(Text(collation='utf8mb4_unicode_ci'), comment='关联工单ID列表') + relate_order_count = Column(INTEGER(11), server_default=text("0"), comment='关联工单数量') + order_user_id = Column(String(50, 'utf8mb4_unicode_ci'), index=True, comment='用户ID(身份证号)') + user_word = Column(Text(collation='utf8mb4_unicode_ci'), comment='用户反馈') + show_flag = Column(String(10, 'utf8mb4_unicode_ci'), comment='显示标志') + origin_show = Column(TINYINT(4), server_default=text("0"), comment='原始显示标志') + order_user = Column(Text(collation='utf8mb4_unicode_ci'), comment='诉求人信息(JSON对象)') + order_phone_dto = Column(Text(collation='utf8mb4_unicode_ci'), comment='电话号码信息(JSON对象)') + order_attachment_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件列表(JSON数组)') + pre_process_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='预处理流程列表(JSON数组)') + tripartite_call_records = Column(Text(collation='utf8mb4_unicode_ci'), comment='三方通话记录(JSON对象)') + tripartite_call_records_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='三方通话记录列表(JSON数组)') + order_custom_form_fields = Column(Text(collation='utf8mb4_unicode_ci'), comment='自定义表单字段(JSON数组)') + knowledge_references = Column(Text(collation='utf8mb4_unicode_ci'), comment='知识参考(JSON对象)') + sound_recording_address_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='录音文件路径列表(JSON数组)') + active_dept_ids = Column(Text(collation='utf8mb4_unicode_ci'), comment='当前处理部门ID列表') + attachment_ids = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件ID列表') + attachment_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件列表JSON') + contactor_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='联系人列表(JSON数组)') + tsjb_entry_info = Column(Text(collation='utf8mb4_unicode_ci'), comment='投诉举报入口信息(JSON对象)') + order_erge_revoke_plug_dto_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='撤销插件信息(JSON数组)') + order_environmental = Column(Text(collation='utf8mb4_unicode_ci'), comment='环境信息(JSON对象)') + order_demands_dto = Column(Text(collation='utf8mb4_unicode_ci'), comment='诉求DTO(JSON对象)') + order_appeal_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='申诉列表(JSON数组)') + torder_process_list = Column(Text(collation='utf8mb4_unicode_ci'), comment='流程列表(JSON数组)') + pre_process = Column(Text(collation='utf8mb4_unicode_ci'), comment='预处理信息(JSON对象)') + extension = Column(Text(collation='utf8mb4_unicode_ci'), comment='扩展字段') + remark = Column(Text(collation='utf8mb4_unicode_ci'), comment='备注') + file_exist = Column(INTEGER(11), server_default=text("0"), comment='是否存在附件(0-无,1-有)') + exist_quoto_info = Column(Text(collation='utf8mb4_unicode_ci'), comment='是否存在引用信息') + residue_date = Column(Text(collation='utf8mb4_unicode_ci'), comment='剩余天数') + whether_approval = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否审批') + over_time_warning_flag = Column(String(10, 'utf8mb4_unicode_ci'), comment='超时预警标志') + create_no = Column(String(20, 'utf8mb4_unicode_ci'), comment='创建编号') + return_visit_reason = Column(Text(collation='utf8mb4_unicode_ci'), comment='回访原因') + back_count = Column(String(100, 'utf8mb4_unicode_ci'), comment='回退次数') + visit_adv_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='走访建议内容') + is_dispatch_accurate = Column(String(10, 'utf8mb4_unicode_ci'), comment='是否精准分派') + process_instance_id = Column(String(100, 'utf8mb4_unicode_ci'), index=True, comment='流程实例ID') + knowledge_quote = Column(Text(collation='utf8mb4_unicode_ci'), comment='知识引用') + special_type = Column(Text(collation='utf8mb4_unicode_ci'), comment='特殊类型') + supervise_type = Column(Text(collation='utf8mb4_unicode_ci'), comment='监督类型') + leader_indicate = Column(Text(collation='utf8mb4_unicode_ci'), comment='领导批示') + case_solve = Column(Text(collation='utf8mb4_unicode_ci'), comment='处理结果') + result_satisfied = Column(Text(collation='utf8mb4_unicode_ci'), comment='结果满意度') + first_vist_satisfied = Column(Text(collation='utf8mb4_unicode_ci'), comment='首次走访满意度') + contact_timely = Column(String(50, 'utf8mb4_unicode_ci'), comment='是否及时联系') + distribute_type = Column(String(50, 'utf8mb4_unicode_ci'), comment='分派类型') + dept_type = Column(Text(collation='utf8mb4_unicode_ci'), comment='部门类型') + dept_name = Column(Text(collation='utf8mb4_unicode_ci'), comment='部门名称') + active_dept_name = Column(String(50, 'utf8mb4_unicode_ci'), comment='当前处理部门名称') + org_id = Column(String(50, 'utf8mb4_unicode_ci'), comment='组织ID') + org_name = Column(Text(collation='utf8mb4_unicode_ci'), comment='组织名称') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsOrderUser(BaseModel): + __tablename__ = 't_d3i_govs_order_user' + __table_args__ = {'comment': '省12345服务对象信息'} + + id = Column(BIGINT(20), primary_key=True, comment='服务对象唯一ID') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), index=True, + comment='关联工单主表ID(t_d3i_govs_order_master.id)') + order_id = Column(String(50), index=True, comment='工单编号') + tenant_id = Column(String(50), comment='租户ID') + area_code = Column(String(20), comment='区域代码') + customer_name = Column(String(50), index=True, comment='姓名') + raw_customer_name = Column(String(50), comment='原始姓名') + customer_sex = Column(String(10), comment='性别(男/女/未知)') + customer_type = Column(String(20), comment='客户类型(个人/企业)') + customer_age_range = Column(String(20), comment='年龄段') + customer_connect_phone = Column(String(20), index=True, comment='联系电话') + raw_customer_connect_phone = Column(String(20), comment='原始联系电话') + customer_incoming_phone = Column(String(20), comment='来电号码') + raw_customer_incoming_phone = Column(String(20), comment='原始来电号码') + customer_phone_backup = Column(String(20), comment='备用电话') + raw_customer_phone_backup = Column(String(20), comment='原始备用电话') + customer_phone_backup_for_dh = Column(String(20), comment='备用电话(脱敏)') + customer_internet_nickname = Column(String(100), comment='网名') + customer_email = Column(String(100), comment='电子邮箱') + customer_credentials_type = Column(String(20), comment='证件类型(如:身份证、护照)') + customer_credentials_no = Column(String(50), index=True, comment='证件号码') + raw_customer_credentials_no = Column(String(50), comment='原始证件号码') + enterprise_type = Column(String(50), comment='企业类型') + enterprise_name = Column(String(200), comment='企业名称') + enterprise_register_address = Column(String(500), comment='企业注册地址') + enterprise_address = Column(String(500), comment='企业地址') + enterprise_credit_code = Column(String(50), comment='企业信用代码') + delete_flag = Column(TINYINT(4), server_default=text("0"), comment='删除标志(0-未删除,1-已删除)') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsPushStatu(BaseModel): + __tablename__ = 't_d3i_govs_push_status' + __table_args__ = {'comment': '推送OA状态记录表(省12345)'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), nullable=False, index=True, comment='唯一标志') + push_order_status = Column(INTEGER(11), nullable=False, server_default=text("0"), comment='推送待办工单状态') + push_order_detail_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单详情状态') + push_order_attachment_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单附件状态') + push_order_process_status = Column(INTEGER(11), nullable=False, server_default=text("0"), + comment='推送待办工单处理流程状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='创建者') + updated_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='修改时间') + updated_by = Column(String(64), nullable=False, server_default=text("'D3I'"), comment='修改者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovcTask(BaseModel): + __tablename__ = 't_d3i_govc_task' + __table_args__ = {'comment': '市12345工单主表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + evl_result = Column(String(64, 'utf8mb4_unicode_ci'), comment='结果满意度') + finish_result = Column(Text(collation='utf8mb4_unicode_ci'), comment='办结结果') + serial_num = Column(String(64, 'utf8mb4_unicode_ci'), comment='工单编号') + t_status = Column(String(64, 'utf8mb4_unicode_ci'), comment='任务单状态') + accord_type = Column(String(255, 'utf8mb4_unicode_ci'), comment='归口类型') + create_date = Column(DateTime, comment='交办时间') + back_time_bf = Column(DateTime, comment='拒绝时限') + sub_handle_ou_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='子处办单位') + sign_time_bf = Column(BIGINT(20), comment='签收时限时间戳') + is_leaf = Column(String(32, 'utf8mb4_unicode_ci'), comment='是否叶子节点') + row_guid = Column(String(64, 'utf8mb4_unicode_ci'), comment='rowguid') + c_guid = Column(String(64, 'utf8mb4_unicode_ci'), comment='查询详情使用guid') + finish_time = Column(BIGINT(20), comment='办结时间戳') + sign_time = Column(BIGINT(20), comment='签收时间戳') + is_secret = Column(String(32, 'utf8mb4_unicode_ci'), comment='是否保密') + finish_time_bf = Column(DateTime, comment='办结时限') + link_number = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系号码') + pvi_guid = Column(String(64, 'utf8mb4_unicode_ci'), comment='查询详情使用pviguid') + rqst_type = Column(String(64, 'utf8mb4_unicode_ci'), comment='诉求类型') + rqst_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='诉求内容') + handle_ou_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='处办单位') + rqst_title = Column(String(500, 'utf8mb4_unicode_ci'), comment='标题') + sign_person = Column(String(128, 'utf8mb4_unicode_ci'), comment='签收人') + rqst_person = Column(String(128, 'utf8mb4_unicode_ci'), comment='诉求人') + rqs_channel = Column(String(64, 'utf8mb4_unicode_ci'), comment='渠道来源') + t_type = Column(String(64, 'utf8mb4_unicode_ci'), comment='工单类型') + solve_situation = Column(String(64, 'utf8mb4_unicode_ci'), comment='解决情况') + evl_style = Column(String(64, 'utf8mb4_unicode_ci'), comment='态度满意度') + send_opinion = Column(Text(collation='utf8mb4_unicode_ci'), comment='派送意见') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + +class TD3iGovcTaskContact(BaseModel): + __tablename__ = 't_d3i_govc_task_contact' + __table_args__ = {'comment': '市12345工单联系信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + link_person = Column(String(128, 'utf8mb4_unicode_ci'), comment='联系人') + link_status = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系类型') + link_date = Column(DateTime, comment='联系时间') + link_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='联系内容') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskDelay(BaseModel): + __tablename__ = 't_d3i_govc_task_delay' + __table_args__ = {'comment': '市12345工单延迟信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + delay_status = Column(String(64, 'utf8mb4_unicode_ci'), comment='审核状态') + delay_num_unit = Column(String(64, 'utf8mb4_unicode_ci'), comment='通过时长') + delay_type = Column(String(64, 'utf8mb4_unicode_ci'), comment='申请类型') + delay_num = Column(INTEGER(11), comment='延迟时长') + apply_ou = Column(String(255, 'utf8mb4_unicode_ci'), comment='申请部门') + apply_time = Column(DateTime, comment='申请时间') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskDepartmentFeedback(BaseModel): + __tablename__ = 't_d3i_govc_task_department_feedback' + __table_args__ = {'comment': '市12345部门处置信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + zxhf_info = Column(Text(collation='utf8mb4_unicode_ci'), comment='专项回复信息') + back_info = Column(Text(collation='utf8mb4_unicode_ci'), comment='退回信息') + sign_time_bf = Column(DateTime, comment='签收时限') + operation_text = Column(String(255, 'utf8mb4_unicode_ci'), comment='操作描述') + opinion = Column(Text(collation='utf8mb4_unicode_ci'), comment='反馈意见') + unit = Column(String(255, 'utf8mb4_unicode_ci'), comment='承办单位') + finish_time_bf = Column(DateTime, comment='反馈时限') + person = Column(String(128, 'utf8mb4_unicode_ci'), comment='承办人') + sign_time = Column(DateTime, comment='签收时间') + name = Column(String(128, 'utf8mb4_unicode_ci'), comment='负责人') + tel = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系电话') + time = Column(DateTime, comment='反馈时间') + department = Column(String(255, 'utf8mb4_unicode_ci'), comment='部门') + status = Column(INTEGER(11), comment='状态') + back_time_bf = Column(DateTime, comment='拒绝时限') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskDetail(BaseModel): + __tablename__ = 't_d3i_govc_task_detail' + __table_args__ = {'comment': '市12345工单详情表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + note = Column(Text(collation='utf8mb4_unicode_ci'), comment='备注') + purpose = Column(String(255, 'utf8mb4_unicode_ci'), comment='诉求目的') + type_level = Column(String(64, 'utf8mb4_unicode_ci'), comment='诉求类型等级') + type = Column(String(64, 'utf8mb4_unicode_ci'), comment='诉求类型') + sign_time_bf = Column(DateTime, comment='签收时限') + matter = Column(String(32, 'utf8mb4_unicode_ci'), comment='窗口进驻事项') + case_form_type = Column(String(64, 'utf8mb4_unicode_ci'), comment='个性化表单类型') + content = Column(Text(collation='utf8mb4_unicode_ci'), comment='诉求内容') + handle_ou = Column(String(255, 'utf8mb4_unicode_ci'), comment='处办单位') + urgency = Column(TINYINT(4), comment='是否紧急') + sj_handle_ou = Column(String(255, 'utf8mb4_unicode_ci'), comment='涉及单位') + ccb_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='催补撤内容') + is_secret = Column(String(32, 'utf8mb4_unicode_ci'), comment='是否保密') + theme = Column(String(32, 'utf8mb4_unicode_ci'), comment='主题工单') + attribute = Column(String(255, 'utf8mb4_unicode_ci'), comment='归口类型') + zqt = Column(String(255, 'utf8mb4_unicode_ci'), comment='企业名称') + address = Column(String(500, 'utf8mb4_unicode_ci'), comment='详细地址') + seng_again_num = Column(INTEGER(11), comment='再交办次数') + epidemic = Column(String(32, 'utf8mb4_unicode_ci'), comment='是否疫情工单') + has_ccb = Column(TINYINT(4), comment='是否有催补撤信息') + way = Column(String(64, 'utf8mb4_unicode_ci'), comment='受理方式') + return_visit = Column(String(64, 'utf8mb4_unicode_ci'), comment='回访类型') + finish_time_bf = Column(DateTime, comment='反馈时限') + is_email = Column(TINYINT(4), comment='是否邮箱提交') + time = Column(DateTime, comment='事发时间') + called_tx = Column(String(64, 'utf8mb4_unicode_ci'), comment='被叫号码') + back_time_bf = Column(DateTime, comment='拒绝时限') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskFinish(BaseModel): + __tablename__ = 't_d3i_govc_task_finish' + __table_args__ = {'comment': '市12345工单办结信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + bj_result = Column(Text(collation='utf8mb4_unicode_ci'), comment='办结意见') + evl_result = Column(String(64, 'utf8mb4_unicode_ci'), comment='结果满意度') + replay_person = Column(String(128, 'utf8mb4_unicode_ci'), comment='回访人') + processing_results = Column(String(255, 'utf8mb4_unicode_ci'), comment='处理结果') + solve_situation = Column(String(64, 'utf8mb4_unicode_ci'), comment='解决情况') + replay_time = Column(DateTime, comment='回访时间') + evl_style = Column(String(64, 'utf8mb4_unicode_ci'), comment='态度满意度') + is_citizen = Column(TINYINT(4), comment='是否市民') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskHistory(BaseModel): + __tablename__ = 't_d3i_govc_task_history' + __table_args__ = {'comment': '市12345历史工单表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + history_date = Column(String(32, 'utf8mb4_unicode_ci'), comment='日期') + serial_num = Column(String(64, 'utf8mb4_unicode_ci'), comment='历史工单号') + detail_url = Column(Text(collation='utf8mb4_unicode_ci'), comment='详情页URL') + rqst_title = Column(String(500, 'utf8mb4_unicode_ci'), comment='工单标题') + state = Column(String(64, 'utf8mb4_unicode_ci'), comment='状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskProces(BaseModel): + __tablename__ = 't_d3i_govc_task_process' + __table_args__ = {'comment': '市12345工单流程追踪表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + handle_time = Column(DateTime, comment='办理时间') + operate_status = Column(String(128, 'utf8mb4_unicode_ci'), comment='办理状态') + activity_guid = Column(String(255, 'utf8mb4_unicode_ci'), comment='办理环节名称') + handle_opinion = Column(Text(collation='utf8mb4_unicode_ci'), comment='办理意见') + is_finish = Column(TINYINT(4), comment='是否结束') + operator_ou_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='部门') + is_back = Column(TINYINT(4), comment='是否回退') + operator_name = Column(String(128, 'utf8mb4_unicode_ci'), comment='办理人') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskRequester(BaseModel): + __tablename__ = 't_d3i_govc_task_requester' + __table_args__ = {'comment': '市12345诉求人信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + card_num = Column(String(128, 'utf8mb4_unicode_ci'), comment='身份证号') + emotion = Column(String(64, 'utf8mb4_unicode_ci'), comment='诉求情绪') + name_scope = Column(String(64, 'utf8mb4_unicode_ci'), comment='年龄范围') + sex = Column(String(32, 'utf8mb4_unicode_ci'), comment='性别') + name = Column(String(128, 'utf8mb4_unicode_ci'), comment='诉求人') + secret_flag = Column(String(32, 'utf8mb4_unicode_ci'), comment='保密标识') + is_secret = Column(String(32, 'utf8mb4_unicode_ci'), comment='是否保密') + is_not_show_record = Column(TINYINT(4), comment='是否不展示记录') + phone_num = Column(String(64, 'utf8mb4_unicode_ci'), comment='来电号码') + limk_num = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系号码1') + c_guid = Column(String(64, 'utf8mb4_unicode_ci'), comment='cguid') + phone_num1 = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系号码2') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskReturnVisit(BaseModel): + __tablename__ = 't_d3i_govc_task_return_visit' + __table_args__ = {'comment': '市12345工单回访结果表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + evl_result = Column(String(64, 'utf8mb4_unicode_ci'), comment='结果满意度') + replay_person = Column(String(128, 'utf8mb4_unicode_ci'), comment='回访人') + is_rg_reply = Column(String(32, 'utf8mb4_unicode_ci'), comment='是否人工回访') + processing_results = Column(String(255, 'utf8mb4_unicode_ci'), comment='处理结果') + solve_situation = Column(String(64, 'utf8mb4_unicode_ci'), comment='解决情况') + replay_time = Column(DateTime, comment='回访时间') + evl_style = Column(String(64, 'utf8mb4_unicode_ci'), comment='态度满意度') + is_citizen = Column(TINYINT(4), comment='是否市民') + replay_content = Column(Text(collation='utf8mb4_unicode_ci'), comment='回访内容') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskStatu(BaseModel): + __tablename__ = 't_d3i_govc_task_status' + __table_args__ = {'comment': '市12345工单办理状态表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + shou_li = Column(String(32, 'utf8mb4_unicode_ci'), comment='受理状态') + jie_dan = Column(String(32, 'utf8mb4_unicode_ci'), comment='接单状态') + hui_fang = Column(String(32, 'utf8mb4_unicode_ci'), comment='回访状态') + ban_li = Column(String(32, 'utf8mb4_unicode_ci'), comment='办理状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskSupervision(BaseModel): + __tablename__ = 't_d3i_govc_task_supervision' + __table_args__ = {'comment': '市12345工单监察信息表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + supervision_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='监察点名称') + supervision_type = Column(String(255, 'utf8mb4_unicode_ci'), comment='监察点类型') + supervision_date = Column(DateTime, comment='监察点时间') + supervision_ou_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='部门') + hj_date = Column(DateTime, comment='核减时间') + supervise_type = Column(String(32, 'utf8mb4_unicode_ci'), comment='监察类别 zx/bm/bmhj') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskTitle(BaseModel): + __tablename__ = 't_d3i_govc_task_title' + __table_args__ = {'comment': '市12345工单标题表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + urgency = Column(TINYINT(4), comment='是否紧急') + order_num = Column(String(64, 'utf8mb4_unicode_ci'), comment='工单编号') + source = Column(String(64, 'utf8mb4_unicode_ci'), comment='来源') + title = Column(String(500, 'utf8mb4_unicode_ci'), comment='标题') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + task = relationship('TD3iGovcTask') + + +class TD3iGovcTaskAttachment(BaseModel): + __tablename__ = 't_d3i_govc_task_attachment' + __table_args__ = {'comment': '市12345工单附件表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + task_id = Column(ForeignKey('t_d3i_govc_task.id'), nullable=False, index=True, comment='关联工单主表ID') + detail_id = Column(ForeignKey('t_d3i_govc_task_detail.id'), nullable=False, index=True, comment='关联工单详情ID') + name = Column(String(500, 'utf8mb4_unicode_ci'), comment='附件名称') + attach_url = Column(Text(collation='utf8mb4_unicode_ci'), comment='附件地址') + type = Column(String(64, 'utf8mb4_unicode_ci'), comment='附件类型') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + detail = relationship('TD3iGovcTaskDetail') + task = relationship('TD3iGovcTask') + + +class TD3iGovsApplicationForDelay(BaseModel): + __tablename__ = 't_d3i_govs_application_for_delay' + __table_args__ = {'comment': '延时申请表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), nullable=False, index=True, comment='主表ID') + gd_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='代签收唯一标志(需要填写)') + finally_time_after_approve = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, + comment='延时申请通过后时间(需要填写)') + finally_time_before_approve = Column(String(64, 'utf8mb4_unicode_ci'), comment='计划完成时间(列表取)') + request_delay = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='申请延时时长(需要填写)') + is_nature_day = Column(String(10, 'utf8mb4_unicode_ci'), nullable=False, + comment='申请延时时长(0、工作日1、自然日)(需要填写)') + already_notify_order_user = Column(String(10, 'utf8mb4_unicode_ci'), nullable=False, + comment='是否已告知诉求人需要延时(默认是)') + request_reason = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='延时原因(需要填写)') + remarks = Column(String(500, 'utf8mb4_unicode_ci'), comment='备注(需要填写)') + contact_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='何人(需要填写)') + contact_time = Column(String(64, 'utf8mb4_unicode_ci'), comment='何时(需要填写)') + contact_type = Column(String(64, 'utf8mb4_unicode_ci'), comment='何方式(主键或编码)(需要填写)') + contact_type_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='何方式(需要填写)') + reply_script = Column(Text(collation='utf8mb4_unicode_ci'), comment='答复脚本(需要填写)') + file_id_str = Column(Text(collation='utf8mb4_unicode_ci'), comment='OA文件id,多个需要,拼接(需要填写)') + order_no = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_no(列表取)') + process_instance_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='process_instance_id(列表取)') + request_delay_time = Column(String(64, 'utf8mb4_unicode_ci'), comment='申请延时时长(字符串)(申请延时时长+天)') + save_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='提交数据为id(默认空字符)') + order_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_id(列表取)') + save_status = Column(TINYINT(4), server_default=text("0"), comment='提交状态(0.未提交1.提交中2.提交成功9.提交失败)') + oa_feedback_status = Column(TINYINT(4), server_default=text("0"), + comment='OA反馈状态(0.初始状态1.反馈中2.反馈成功9.反馈失败)') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsPhaseWiseCompletion(BaseModel): + __tablename__ = 't_d3i_govs_phase_wise_completion' + __table_args__ = {'comment': '阶段性办结表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), nullable=False, index=True, comment='主表ID') + gd_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='代签收唯一标志(需要填写)') + is_contact = Column(String(10, 'utf8mb4_unicode_ci'), nullable=False, comment='联系诉求人情况(默认是)(需要填写)') + contact_name = Column(String(100, 'utf8mb4_unicode_ci'), nullable=False, comment='联系人员(需要填写)') + contact_time = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='联系时间(需要填写)') + contact_type = Column(String(255, 'utf8mb4_unicode_ci'), nullable=False, comment='联系情况(需要填写)') + next_feedback_time = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='下一次反馈时间(需要填写)') + advice = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='处理意见(需要填写)') + reason = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='处理意见1(需要填写)') + remark = Column(String(500, 'utf8mb4_unicode_ci'), comment='备注') + file_id_str = Column(Text(collation='utf8mb4_unicode_ci'), comment='OA文件id,多个需要,拼接(需要填写)') + action_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='action_name(列表取nextActionName)') + case_accord_type_one_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_one_name(列表取caseAccordTypeOneName)') + case_accord_type_two_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_two_name(列表取caseAccordTypeTwoName)') + case_accord_type_three_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_three_name(列表取caseAccordTypeThreeName)') + order_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_id(列表取)') + task_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='task_id(列表取nextTaskId)') + save_status = Column(TINYINT(4), server_default=text("0"), comment='提交状态(0.未提交1.提交中2.提交成功9.提交失败)') + oa_feedback_status = Column(TINYINT(4), server_default=text("0"), + comment='OA反馈状态(0.初始状态1.反馈中2.反馈成功9.反馈失败)') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsReplyFormal(BaseModel): + __tablename__ = 't_d3i_govs_reply_formal' + __table_args__ = {'comment': '答复办结表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), nullable=False, index=True, comment='主表ID') + gd_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='代签收唯一标志(需要填写)') + is_contact = Column(String(10, 'utf8mb4_unicode_ci'), nullable=False, comment='是否联系服务对象(默认是)') + contact_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='联系人员(需要填写)') + contact_time = Column(String(64, 'utf8mb4_unicode_ci'), comment='联系时间(需要填写)') + contact_type = Column(String(255, 'utf8mb4_unicode_ci'), nullable=False, comment='联系情况(需要填写)') + advice = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='处理意见(面向群众公开)(需要填写)') + reason = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='处理意见(面向群众公开2)(需要填写)') + remarks = Column(String(500, 'utf8mb4_unicode_ci'), comment='备注(需要填写)') + file_id_str = Column(Text(collation='utf8mb4_unicode_ci'), comment='OA文件id,多个需要,拼接(需要填写)') + save_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='提交数据为id(列表取nextTaskId)') + process_instance_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='process_instance_id(列表取)') + business_key = Column(String(64, 'utf8mb4_unicode_ci'), comment='business_key(列表取orderId)') + order_no = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_no(列表取)') + action_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='action_name(列表取nextActionName)') + case_accord_type_one_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_one_name(列表取caseAccordTypeOneName)') + case_accord_type_two_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_two_name(列表取caseAccordTypeTwoName)') + case_accord_type_three_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_three_name(列表取caseAccordTypeThreeName)') + save_status = Column(TINYINT(4), server_default=text("0"), comment='提交状态(0.未提交1.提交中2.提交成功9.提交失败)') + oa_feedback_status = Column(TINYINT(4), server_default=text("0"), + comment='OA反馈状态(0.初始状态1.反馈中2.反馈成功9.反馈失败)') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsSaveSign(BaseModel): + __tablename__ = 't_d3i_govs_save_sign' + __table_args__ = {'comment': '工单签收表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), nullable=False, index=True, comment='主表ID') + gd_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='代签收唯一标志(需要填写)') + order_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_id(列表取)') + order_no = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_no(列表取)') + order_process_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_process_id(列表取,origin_id)') + task_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='task_id(列表取nextTaskId)') + flag = Column(String(64, 'utf8mb4_unicode_ci'), comment='签收') + save_status = Column(TINYINT(4), server_default=text("0"), comment='提交状态(0.未提交1.提交中2.提交成功9.提交失败)') + oa_feedback_status = Column(TINYINT(4), server_default=text("0"), + comment='OA反馈状态(0.初始状态1.反馈中2.反馈成功9.反馈失败)') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') + + +class TD3iGovsWorkOrderReturnFormal(BaseModel): + __tablename__ = 't_d3i_govs_work_order_return_formal' + __table_args__ = {'comment': '工单退回表'} + + id = Column(BIGINT(20), primary_key=True, comment='主键') + master_id = Column(ForeignKey('t_d3i_govs_order_master.id'), nullable=False, index=True, comment='主表ID') + gd_id = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, comment='代签收唯一标志(需要填写)') + return_reason = Column(String(255, 'utf8mb4_unicode_ci'), nullable=False, + comment='退回原因(非部门职能/申请主协办)(需要填写)') + return_reason_name = Column(String(255, 'utf8mb4_unicode_ci'), nullable=False, + comment='退回原因2(非部门职能/申请主协办)(需要填写)') + return_auditor_name = Column(String(100, 'utf8mb4_unicode_ci'), comment='退回审核人(需要填写)') + return_auditor_id = Column(String(64, 'utf8mb4_unicode_ci'), + comment='return_auditor_id(退回审核人存在时,默认1788283400345608193)') + deal_opinion = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='处理意见(需要填写)') + reason = Column(Text(collation='utf8mb4_unicode_ci'), nullable=False, comment='处理意见2(需要填写)') + remark = Column(String(500, 'utf8mb4_unicode_ci'), comment='备注(需要填写)') + file_id_str = Column(Text(collation='utf8mb4_unicode_ci'), comment='OA文件id,多个需要,拼接(需要填写)') + process_instance_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='process_instance_id(列表取)') + action_name = Column(String(255, 'utf8mb4_unicode_ci'), comment='action_name(列表取nextActionName)') + order_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_id(列表取)') + task_id = Column(String(64, 'utf8mb4_unicode_ci'), comment='task_id(列表取nextTaskId)') + order_no = Column(String(64, 'utf8mb4_unicode_ci'), comment='order_no(列表取)') + case_accord_type_one_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_one_name(列表取caseAccordTypeOneName)') + case_accord_type_two_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_two_name(列表取caseAccordTypeTwoName)') + case_accord_type_three_name = Column(String(255, 'utf8mb4_unicode_ci'), + comment='case_accord_type_three_name(列表取caseAccordTypeThreeName)') + save_status = Column(TINYINT(4), server_default=text("0"), comment='提交状态(0.未提交1.提交中2.提交成功9.提交失败)') + oa_feedback_status = Column(TINYINT(4), server_default=text("0"), + comment='OA反馈状态(0.初始状态1.反馈中2.反馈成功9.反馈失败)') + flow_token = Column(String(256, 'utf8mb4_unicode_ci'), comment='流令牌') + status = Column(BIGINT(20), nullable=False, server_default=text("0"), comment='提交状态') + created_at = Column(DateTime, nullable=False, server_default=text("current_timestamp()"), comment='创建时间') + created_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='创建者') + updated_at = Column(DateTime, nullable=False, + server_default=text("current_timestamp() ON UPDATE current_timestamp()"), comment='更新时间') + updated_by = Column(String(64, 'utf8mb4_unicode_ci'), nullable=False, server_default=text("'D3I'"), + comment='更新者') + + master = relationship('TD3iGovsOrderMaster') diff --git a/models/dcm_apply_delay.py b/models/dcm_apply_delay.py new file mode 100644 index 0000000..e79735a --- /dev/null +++ b/models/dcm_apply_delay.py @@ -0,0 +1,553 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmApplyPostpone +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmApplyPostponeForm(ModelForm): + """ + 专业表单验证类(已完全根据 TD3iDcmApplyDelay 字段重构)。 + + 用于验证和处理数字城管-申请延期接口的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_dcm_apply_delay 的字段结构。 + """ + + # 基础信息 + id = IntegerField('主键ID') + dcm_task_id = IntegerField('任务ID') + task_number = StringField('任务号', validators=[Length(max=64, message='任务号长度不能超过64字符')]) + apply_act_id = StringField('工单流程ID', validators=[Length(max=64, message='工单流程ID长度不能超过64字符')]) + reply_part_id = StringField('回复环节ID', validators=[Length(max=64, message='回复环节ID长度不能超过64字符')]) + ard_level = StringField('固定值(等级)', validators=[Length(max=32, message='固定值长度不能超过32字符')]) + ard_type_id = StringField('延期类型', validators=[Length(max=32, message='延期类型长度不能超过32字符')]) + apply_memo = TextAreaField('申请意见', validators=[Length(max=65535, message='申请意见长度不能超过65535字符')]) + time_num = StringField('延期时长', validators=[Length(max=64, message='延期时长长度不能超过64字符')]) + apply_type = StringField('申请类型', validators=[Length(max=64, message='申请类型长度不能超过64字符')]) + delay_multiple = IntegerField('延期倍数') + postpone_date = StringField('延期日期', validators=[Length(max=64, message='延期日期长度不能超过64字符')]) + time_unit = StringField('时间单位', validators=[Length(max=64, message='时间单位长度不能超过64字符')]) + attachments = TextAreaField('附件', validators=[Length(max=65535, message='附件长度不能超过65535字符')]) + status = IntegerField('提交状态') + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmApplyPostponeBase(TD3iDcmApplyPostpone, CommonModel): + """ + 专业基础类(已完全映射 TD3iDcmApplyDelay 字段)。 + + 继承自数据库模型 TD3iDcmApplyDelay 和通用模型 CommonModel。 + 封装所有与申请延期相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'dcm_task_id': 'dcm_task_id', + 'task_number': 'task_number', + 'apply_act_id': 'apply_act_id', + 'reply_part_id': 'reply_part_id', + 'ard_level': 'ard_level', + 'ard_type_id': 'ard_type_id', + 'apply_memo': 'apply_memo', + 'timeNum': 'timeNum', + 'postponeDate': 'postponeDate', + 'time_unit': 'time_unit', + 'attachments': 'attachments', + 'status': 'status', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + + @classmethod + async def exist_other(cls, id: Union[str, int], task_number: str): + """ + 检查是否存在除当前记录外的其他同任务号的延期申请。 + + :param id: 当前记录ID + :param task_number: 任务号 + :return: 存在返回记录对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.task_number == task_number) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找延期申请数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _record_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _record_list + + @classmethod + async def is_exist(cls, task_number: str): + """ + 检查延期申请是否已经存在(根据任务号)。 + + :param task_number: 任务号 + :return: 存在返回记录对象,不存在返回None + """ + _query = select(cls).where(cls.task_number == task_number) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索延期申请数据的基础方法。 + + 支持字段: + - task_number, ard_type_id, status, created_by 等 + - 支持模糊匹配:apply_memo, attachments + - 支持精确匹配:task_number, ard_type_id, status, dcm_task_id + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_number': 'asc'} + :key str task_number: 精确匹配任务号 + :key str ard_type_id: 精确匹配延期类型 + :key int status: 精确匹配提交状态 + :key str apply_memo: 模糊匹配申请意见 + :key str attachments: 模糊匹配附件内容 + :key str created_by: 精确匹配创建者 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.apply_memo.key: '%{}%', + cls.attachments.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_number) + + _record_df = await cls.query_as_df(_data_query) + if not _record_df.empty: + _record_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _record_df[cls.id.key] = _record_df[cls.id.key].astype(str) + + return _record_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索延期申请数据,返回分页格式数据。 + """ + _record_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _record_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_number(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_number 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_number 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_number 列表(去重) + task_numbers = data_df[cls.task_number.key].unique().tolist() + if not task_numbers: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_number + _query = select(cls.id, cls.task_number).where(cls.task_number.in_(task_numbers)) + task_numbers_df = await cls.query_as_df(_query) + + if task_numbers_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_number -> id 的映射字典 + task_number_to_id_map = dict(zip(task_numbers_df[cls.task_number.key], task_numbers_df[cls.id.key])) + + # 根据 task_number 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_number.key].isin(task_numbers_df[cls.task_number.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_number.key].map(task_number_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class DcmApplyPostpone(DcmApplyPostponeBase): + """ + 专业模型类(主业务类,完全继承 TD3iDcmApplyDelay 字段)。 + + --- + description: 数字城管-申请延期接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + dcm_task_id: + description: 关联的任务ID + type: integer + example: 2001 + task_number: + description: 任务号 + type: string + example: "TASK20240501001" + maxLength: 64 + apply_act_id: + description: 工单流程ID + type: string + example: "ACT20240501001" + maxLength: 64 + reply_part_id: + description: 回复环节ID + type: string + example: "PART_REPLY_001" + maxLength: 64 + ard_level: + description: 固定值(等级) + type: string + example: "LEVEL_1" + maxLength: 32 + ard_type_id: + description: 延期类型(固定值) + type: string + example: "DELAY_TYPE_1" + maxLength: 32 + apply_memo: + description: 申请意见 + type: string + example: "因天气原因,申请延期处理。" + maxLength: 65535 + timeNum: + description: 延期时长 + type: string + example: "3" + maxLength: 64 + postponeDate: + description: 延期日期 + type: string + example: "2024-06-01" + maxLength: 64 + time_unit: + description: 时间单位 + type: string + example: "天" + maxLength: 64 + attachments: + description: 附件(JSON格式或逗号分隔) + type: string + example: "file1.jpg,file2.pdf" + maxLength: 65535 + status: + description: 提交状态(0草稿,1已提交,2已审批) + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "D3I" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "D3I" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的延期申请。 + + 业务流程: + 1. 使用 DcmApplyDelayForm 验证表单数据完整性 + 2. 检查任务号是否已存在延期申请 + 3. 创建新延期申请对象 + 4. 设置创建者和更新者为当前用户(默认为 D3I) + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象(可选) + :param kwargs: 延期申请参数字典 + :return: 新建延期申请对象 + :rtype: DcmApplyPostpone + :raises AssertionError: 当任务号已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmApplyPostponeForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同任务号的延期申请 + _exist = await cls.is_exist(_form.task_number.data) + assert _exist is None, "该任务已存在延期申请,不能重复提交。" + + # 创建延期申请对象 + _record = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _record.created_by = user.username + _record.updated_by = user.username + else: + # 默认为 D3I + if not _record.created_by: + _record.created_by = "D3I" + if not _record.updated_by: + _record.updated_by = "D3I" + + await _record.async_save() + return _record + + @classmethod + async def delete(cls, id: Union[str, int]): + """ + 删除延期申请。 + + 业务流程: + 1. 根据ID查找延期申请 + 2. 验证存在性 + 3. 执行删除操作 + + :param id: 要删除的延期申请ID + :return: 删除的对象 + :rtype: DcmApplyPostpone + :raises AssertionError: 当记录不存在时抛出 + """ + _record: cls = await cls.async_find_by_id(id) + assert _record, f"根据 ID {id} 未找到延期申请记录。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _record.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除延期申请(任务号:{_record.task_number},ID:{_record.id}).') + return _record + + @classmethod + async def modify(cls, id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有延期申请信息。 + + 业务流程: + 1. 将 id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 DcmApplyDelayForm 验证表单数据 + 4. 检查是否有其他记录使用了相同的 task_number(排除自身) + 5. 查询原记录对象 + 6. 验证存在性 + 7. 更新字段并设置更新者 + 8. 保存到数据库 + 9. 返回更新后的对象 + + :param id: 要修改的延期申请ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的延期申请对象 + :rtype: DcmApplyPostpone + :raises AssertionError: 当记录不存在或任务号重复时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = DcmApplyPostponeForm(formdata=kwargs) + _form.validate_form() + + # 检查是否与其他记录重复(排除自身) + _other = await cls.exist_other(id, _form.task_number.data) + assert _other is None, "该任务号已存在其他延期申请,不能重复修改。" + + # 查询原记录 + _record: cls = await cls.async_find_by_id(id) + assert _record, f'查无此延期申请记录。' + + # 更新字段 + _record.copy_from_dict(_form.data, skip_none=True).before_save() + _record.updated_by = user.username if user else "D3I" + await _record.async_save() + return _record + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建新的延期申请(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含延期申请数据的 DataFrame,字段需与模型属性匹配(如 task_number, apply_act_id 等) + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 向量化设置用户字段(无循环) + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + else: + # 默认 D3I + data_df['created_by'] = "D3I" + data_df['updated_by'] = "D3I" + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + records = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条延期申请。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有延期申请。 + + :param data_df: 包含延期申请数据的 DataFrame,必须包含 id 列 + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + else: + data_df['updated_by'] = "D3I" + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条延期申请。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmApplyPostpone.exists_task_number(data_df) + # 保存到数据库 + _created_count = await DcmApplyPostpone.create_batch(_latest_df, user) + _updated_count = await DcmApplyPostpone.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/dcm_apply_rollback.py b/models/dcm_apply_rollback.py new file mode 100644 index 0000000..b889e39 --- /dev/null +++ b/models/dcm_apply_rollback.py @@ -0,0 +1,320 @@ +import datetime +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField +from wtforms.validators import Length + +from models.common_model import CommonModel +from models.db_models import TD3iDcmApplyRollback +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.web.form import ModelForm + + +class DcmApplyRollbackForm(ModelForm): + """ + 申请回退任务表单验证类(完全映射 TD3iDcmApplyRollback 字段)。 + + 用于验证和处理数字城管-申请回退操作的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_dcm_apply_rollback 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_number = StringField('任务号', validators=[Length(max=64, message='任务号长度不能超过64字符')]) + act_id = StringField('工单ID', validators=[Length(max=64, message='工单ID长度不能超过64字符')]) + reply_part_id = IntegerField('回复部门ID') + ard_level = IntegerField('回退流向') + ard_type_id = IntegerField('延期类型ID') + opinion = TextAreaField('申请说明') + apply_type = StringField('申请类型(拒签、处置阶段照片未公开)', + validators=[Length(max=64, message='申请类型长度不能超过64字符')]) + trans_info = StringField('流转信息', validators=[Length(max=255, message='流转信息长度不能超过255字符')]) + attachments = TextAreaField('附件', validators=[Length(max=65535, message='附件长度不能超过65535字符')]) + status = IntegerField('提交状态') + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmApplyRollbackBase(TD3iDcmApplyRollback, CommonModel): + """ + 申请回退任务基础类(完全映射 TD3iDcmApplyRollback 字段)。 + + 继承自数据库模型 TD3iDcmApplyRollback 和通用模型 CommonModel。 + 封装所有与回退操作相关的通用操作方法。 + """ + + @classmethod + async def is_exist(cls, act_id: str): + """ + 检查申请回退记录是否已存在(根据任务ID)。 + + :param act_id: 任务ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.act_id == act_id) + _rollback: cls = await cls.query_first(_query) + return _rollback + + @classmethod + async def exists_act_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 act_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 act_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 act_id 列表(去重) + act_ids = data_df[cls.act_id.key].unique().tolist() + if not act_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 act_id + _query = select(cls.id, cls.act_id).where(cls.act_id.in_(act_ids)) + act_ids_df = await cls.query_as_df(_query) + + if act_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 act_id -> id 的映射字典 + act_id_to_id_map = dict(zip(act_ids_df[cls.act_id.key], act_ids_df[cls.id.key])) + + # 根据 act_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.act_id.key].isin(act_ids_df[cls.act_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.act_id.key].map(act_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class DcmApplyRollback(DcmApplyRollbackBase): + """ + 申请回退任务模型类(主业务类,完全继承 TD3iDcmApplyRollback 字段)。 + + --- + description: 数字城管-申请回退接口 + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的申请回退记录。 + + 业务流程: + 1. 使用 DcmApplyRollbackForm 验证表单数据完整性 + 2. 检查是否已存在相同 act_id 的记录(避免重复提交) + 3. 创建新申请回退对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 回退参数字典 + :return: 新建申请回退对象 + :rtype: DcmApplyRollback + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmApplyRollbackForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 act_id 的记录 + _existing = await cls.is_exist(_form.act_id.data) + assert _existing is None, "该任务已存在申请回退记录,不能重复提交。" + + # 创建对象 + _rollback = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _rollback.created_by = user.username + _rollback.updated_by = user.username + await _rollback.async_save() + return _rollback + + @classmethod + async def delete(cls, rollback_id: Union[str, int]): + """ + 删除申请回退记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param rollback_id: 要删除的申请回退记录ID + :return: 删除的记录对象 + :rtype: DcmApplyRollback + :raises AssertionError: 当记录不存在时抛出 + """ + _rollback: cls = await cls.async_find_by_id(rollback_id) + assert _rollback, f"根据 ID {rollback_id} 未找到申请回退记录。" + + _del_query = delete(cls).where(cls.id == _rollback.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除回退记录(任务号:{_rollback.task_number},ID:{_rollback.id}).') + return _rollback + + @classmethod + async def modify(cls, rollback_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有申请回退记录。 + + 业务流程: + 1. 将 rollback_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 DcmApplyRollbackForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param rollback_id: 要修改的申请回退记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的申请回退对象 + :rtype: DcmApplyRollback + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = DcmApplyRollbackForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _rollback: cls = await cls.async_find_by_id(rollback_id) + assert _rollback, f'查无此申请回退信息。' + + # 更新字段 + _rollback.copy_from_dict(_form.data, skip_none=True).before_save() + _rollback.updated_by = user.username + await _rollback.async_save() + return _rollback + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建申请回退记录(传入数据应为全新记录)。 + + :param data_df: 包含回退数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + rollbacks = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(rollbacks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(rollbacks)} 条申请回退记录。") + return len(rollbacks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有申请回退记录。 + + :param data_df: 包含申请回退数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条申请回退记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存申请回退数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmApplyRollback.exists_act_id(data_df) + # 保存到数据库 + _created_count = await DcmApplyRollback.create_batch(_latest_df, user) + _updated_count = await DcmApplyRollback.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/dcm_dispose.py b/models/dcm_dispose.py new file mode 100644 index 0000000..c58f2b7 --- /dev/null +++ b/models/dcm_dispose.py @@ -0,0 +1,556 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmDispose +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmDisposeForm(ModelForm): + """ + 批转任务表单验证类(完全映射 TD3iDcmDispose 字段)。 + + 用于验证和处理数字城管-批转任务的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_dcm_dispose 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + rec_id = StringField('记录ID', validators=[Length(max=64, message='记录ID长度不能超过64字符')]) + task_number = StringField('任务号', validators=[Length(max=64, message='任务号长度不能超过64字符')]) + act_id = StringField('工单ID', validators=[Length(max=64, message='工单ID长度不能超过64字符')]) + task_list_id = StringField('任务列表ID', validators=[Length(max=64, message='任务列表ID长度不能超过64字符')]) + trans_info = StringField('批转对象', validators=[Length(max=64, message='批转对象长度不能超过64字符')]) + opinion = TextAreaField('批转意见', validators=[Length(max=65535, message='批转意见长度不能超过65535字符')]) + add_num = StringField('批转意见', validators=[Length(max=32, message='批转意见长度不能超过32字符')]) + attachments = TextAreaField('附件', validators=[Length(max=65535, message='附件长度不能超过65535字符')]) + send_message = StringField('发送短信', validators=[Length(max=32, message='发送短信标识长度不能超过32字符')]) + undertake_user_name = StringField('承办人员', validators=[Length(max=64, message='承办人员长度不能超过64字符')]) + undertake_phone = StringField('联系电话', validators=[Length(max=64, message='联系电话长度不能超过64字符')]) + status = IntegerField('提交状态') + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmDisposeBase(TD3iDcmDispose, CommonModel): + """ + 批转任务基础类(完全映射 TD3iDcmDispose 字段)。 + + 继承自数据库模型 TD3iDcmDispose 和通用模型 CommonModel。 + 封装所有与批转任务相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'rec_id': 'rec_id', + 'task_number': 'task_number', + 'act_id': 'act_id', + 'task_list_id': 'task_list_id', + 'trans_info': 'trans_info', + 'opinion': 'opinion', + 'add_num': 'add_num', + 'attachments': 'attachments', + 'send_message': 'send_message', + 'undertake_user_name': 'undertake_user_name', + 'undertake_phone': 'undertake_phone', + 'status': 'status', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 批转任务数据映射 + """ + + @classmethod + async def exist_other(cls, id: Union[str, int], rec_id: str, task_number: str): + """ + 检查是否存在除当前记录外的其他相同任务号或记录ID的批转记录。 + + :param id: 当前记录ID + :param rec_id: 记录ID + :param task_number: 任务号 + :return: 存在返回记录对象,不存在返回None + """ + _query = select(cls).where( + cls.id != id, + (cls.rec_id == rec_id) | (cls.task_number == task_number) + ) + _dispose: cls = await cls.query_first(_query) + return _dispose + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找批转任务数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _dispose_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _dispose_list + + @classmethod + async def is_exist(cls, rec_id: str, task_number: str): + """ + 检查批转记录是否已经存在(根据记录ID或任务号)。 + """ + _query = select(cls).where( + (cls.rec_id == rec_id) | (cls.task_number == task_number) + ) + _dispose: cls = await cls.query_first(_query) + return _dispose + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索批转任务数据的基础方法。 + + 支持字段: + - task_number, rec_id, act_id, trans_info, status + - 支持模糊匹配:trans_info, opinion + - 支持精确匹配:status, send_message + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_number': 'asc'} + :key str task_number: 精确匹配任务号 + :key str rec_id: 精确匹配记录ID + :key str act_id: 精确匹配工单ID + :key str trans_info: 模糊匹配批转对象 + :key str opinion: 模糊匹配批转意见 + :key int status: 精确匹配提交状态 + :key str send_message: 精确匹配发送短信标识 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.trans_info.key: '%{}%', + cls.opinion.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_number, cls.rec_id) + + _dispose_df = await cls.query_as_df(_data_query) + if not _dispose_df.empty: + _dispose_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _dispose_df[cls.id.key] = _dispose_df[cls.id.key].astype(str) + + return _dispose_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索批转任务数据,返回分页格式数据。 + """ + _dispose_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _dispose_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 rec_id 和 task_number 判断。 + + :param data_df: 输入的数据框架,必须包含 rec_id 和 task_number 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 rec_id 和 task_number 列表(去重组合) + rec_ids = data_df[cls.rec_id.key].unique().tolist() + task_numbers = data_df[cls.task_number.key].unique().tolist() + + if not rec_ids and not task_numbers: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录(任一匹配) + _query = select(cls.id, cls.rec_id, cls.task_number).where( + (cls.rec_id.in_(rec_ids)) | (cls.task_number.in_(task_numbers)) + ) + exists_df = await cls.query_as_df(_query) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 (rec_id, task_number) -> id 的映射字典 + exists_map = set(zip(exists_df[cls.rec_id.key], exists_df[cls.task_number.key])) + + # 标记是否存在 + mask_exists = data_df.apply( + lambda row: (row[cls.rec_id.key], row[cls.task_number.key]) in exists_map, + axis=1 + ) + exists_df = data_df[mask_exists].copy() + latest_df = data_df[~mask_exists].copy() + + # 为存在的记录补充 id 字段(可选) + exists_df[cls.id.key] = exists_df.apply( + lambda row: exists_df[ + (exists_df[cls.rec_id.key] == row[cls.rec_id.key]) & + (exists_df[cls.task_number.key] == row[cls.task_number.key]) + ][cls.id.key].iloc[0] if len(exists_df[ + (exists_df[cls.rec_id.key] == row[cls.rec_id.key]) & + (exists_df[cls.task_number.key] == row[cls.task_number.key]) + ]) > 0 else None, + axis=1 + ) + + return exists_df, latest_df + + +@register_swagger_model +class DcmDispose(DcmDisposeBase): + """ + 批转任务模型类(主业务类,完全继承 TD3iDcmDispose 字段)。 + + --- + description: 数字城管-批转接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + rec_id: + description: 记录ID + type: string + example: "20240501001" + maxLength: 64 + task_number: + description: 任务号 + type: string + example: "TASK20240501001" + maxLength: 64 + act_id: + description: 工单ID + type: string + example: "ACT20240501001" + maxLength: 64 + task_list_id: + description: 任务列表ID + type: string + example: "LIST20240501001" + maxLength: 64 + trans_info: + description: 批转对象(固定:市受理员) + type: string + example: "市受理员" + maxLength: 64 + opinion: + description: 批转意见 + type: string + example: "请转交至市容科处理" + maxLength: 65535 + add_num: + description: 批转意见(冗余字段) + type: string + example: "请转交" + maxLength: 32 + attachments: + description: 附件(JSON格式) + type: string + example: '["file1.jpg","file2.pdf"]' + maxLength: 65535 + send_message: + description: 发送短信(1:发送,0:不发送) + type: string + example: "1" + maxLength: 32 + status: + description: 提交状态 + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的批转任务。 + + 业务流程: + 1. 使用 TD3iDcmDisposeForm 验证表单数据完整性 + 2. 检查是否存在相同记录ID或任务号的批转记录 + 3. 创建新批转对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的批转对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 批转参数字典 + :return: 新建批转任务对象 + :rtype: TD3iDcmDispose + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _dispose_form = DcmDisposeForm(formdata=kwargs) + _dispose_form.validate_form() + + # 检查是否已存在相同记录ID或任务号 + _exist: cls = await cls.is_exist(_dispose_form.rec_id.data, _dispose_form.task_number.data) + assert _exist is None, "该记录ID或任务号已存在批转记录,不能重复创建。" + + # 创建批转对象 + _dispose = cls().copy_from_dict(_dispose_form.data, skip_none=True).before_save() + if user: + _dispose.created_by = user.username + _dispose.updated_by = user.username + await _dispose.async_save() + return _dispose + + @classmethod + async def delete(cls, dispose_id: Union[str, int]): + """ + 删除批转任务。 + + 业务流程: + 1. 根据ID查找批转任务 + 2. 验证任务存在性 + 3. 执行删除操作 + + :param dispose_id: 要删除的批转任务ID + :return: 删除的批转任务对象 + :rtype: TD3iDcmDispose + :raises AssertionError: 当任务不存在时抛出 + """ + _dispose: cls = await cls.async_find_by_id(dispose_id) + assert _dispose, f"根据 ID {dispose_id} 未找到批转任务。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _dispose.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除批转任务(任务号:{_dispose.task_number},ID:{_dispose.id}).') + return _dispose + + @classmethod + async def modify(cls, dispose_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有批转任务信息。 + + 业务流程: + 1. 将 dispose_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 TD3iDcmDisposeForm 验证表单数据 + 4. 检查是否有其他批转任务使用了相同的 rec_id 或 task_number + 5. 查询原批转任务对象 + 6. 验证任务存在性 + 7. 更新字段并设置更新者 + 8. 保存到数据库 + 9. 返回更新后的批转任务对象 + + :param dispose_id: 要修改的批转任务ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的批转任务对象 + :rtype: TD3iDcmDispose + :raises AssertionError: 当任务不存在或信息重复时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _dispose_form = DcmDisposeForm(formdata=kwargs) + _dispose_form.validate_form() + + # 检查是否与其他批转任务重复(排除自身) + _other = await cls.exist_other( + dispose_id, + _dispose_form.rec_id.data, + _dispose_form.task_number.data + ) + assert _other is None, "批转记录ID或任务号已存在,不能重复修改。" + + # 查询原批转任务 + _dispose: cls = await cls.async_find_by_id(dispose_id) + assert _dispose, f'查无此批转信息。' + + # 更新字段 + _dispose.copy_from_dict(_dispose_form.data, skip_none=True).before_save() + _dispose.updated_by = user.username + await _dispose.async_save() + return _dispose + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建新批转任务(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含批转任务数据的 DataFrame,字段需与模型属性匹配(如 rec_id, task_number 等) + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的任务数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 向量化设置用户字段(无循环) + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + disposals = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(disposals) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(disposals)} 条新批转记录。") + return len(disposals) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有批转任务。 + + :param data_df: 包含批转任务数据的 DataFrame,必须包含 id 列 + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的任务数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条批转记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmDispose.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmDispose.create_batch(_latest_df, user) + _updated_count = await DcmDispose.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/dcm_push_status.py b/models/dcm_push_status.py new file mode 100644 index 0000000..cb09a9e --- /dev/null +++ b/models/dcm_push_status.py @@ -0,0 +1,543 @@ +import random +from typing import Union, Optional, Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from paste.core.logging import echo_log +from paste.util.pagination import Pagination + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmPushStatu + + +class DcmPushStatuBase(TD3iDcmPushStatu, CommonModel): + """ + 推送状态基础类(完全映射 TD3iDcmPushStatu 字段)。 + + 封装所有与推送OA状态相关的通用操作方法。 + """ + + # 无字段名映射需求,保持原样 + FieldMapping = {} + + @classmethod + async def exist_other(cls, id: Union[str, int], dcm_task_id: Union[str, int]): + """ + 检查是否存在除当前记录外的其他同任务ID的推送状态记录。 + + :param id: 当前记录ID + :param dcm_task_id: 任务ID(唯一标志) + :return: 存在返回记录对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.dcm_task_id == dcm_task_id) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找推送状态数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _record_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _record_list + + @classmethod + async def is_exist(cls, dcm_task_id: Union[str, int]): + """ + 检查推送状态是否已经存在(根据任务ID)。 + """ + _query = select(cls).where(cls.dcm_task_id == dcm_task_id) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索推送状态数据的基础方法。 + + 支持字段: + - dcm_task_id, push_task_status, push_task_attachment_status, ... + - 不支持模糊匹配(均为整型状态码) + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'updated_at': 'desc'} + :key int dcm_task_id: 精确匹配任务ID + :key int push_task_status: 精确匹配推送待办工单状态 + :key int push_task_attachment_status: 精确匹配附件状态 + :key int push_task_extend_info_status: 精确匹配扩展信息状态 + :key int push_task_file_upload_status: 精确匹配文件上传状态 + :key int push_task_more_info_status: 精确匹配更多信息状态 + :key int push_task_process_info_status: 精确匹配处理过程状态 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 无模糊字段,仅精确匹配 + _query = select(cls).where( + *cls.search_wheres(**kwargs) + ) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.updated_at.desc()) + + _record_df = await cls.query_as_df(_data_query) + if not _record_df.empty: + _record_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + return _record_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索推送状态数据,返回分页格式数据。 + """ + _record_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _record_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_relation(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 dcm_task_id 判断。 + + :param data_df: 输入的数据框架,必须包含 dcm_task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 dcm_task_id 组合 + task_ids = data_df[cls.dcm_task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.dcm_task_id).where(cls.dcm_task_id.in_(task_ids)) + exists_df = await cls.query_as_df(_query) + exists_df[cls.dcm_task_id.key] = exists_df[cls.dcm_task_id.key].astype(str) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 dcm_task_id -> id 的映射 + key_to_id_map = dict(zip(exists_df[cls.dcm_task_id.key], exists_df[cls.id.key])) + + # 根据 dcm_task_id 是否在数据库中划分数据 + mask_exists = data_df[cls.dcm_task_id.key].isin(exists_df[cls.dcm_task_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.dcm_task_id.key].map(key_to_id_map) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def fill_attachment(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'push_status', is_full: bool = True, + preprocessing: Optional[Callable] = None): + """ + 填充推送状态数据到数据框架。 + + 用于在查询结果中添加关联的推送状态信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`push_status` + :param is_full: 是否填充完整数据(此处无关联表,忽略) + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 推送状态数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_id.in_(_task_ids)) + + _status_df: pd.DataFrame = await cls.query_as_df(_query) + if not _status_df.empty: + _status_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _status_df[cls.id.key] = _status_df[cls.id.key].astype(str) + _status_df[cls.dcm_task_id.key] = _status_df[cls.dcm_task_id.key].astype(str) + + # 设置索引 + _status_df['index_id'] = _status_df[cls.id.key] + _status_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _status_df = preprocessing(_status_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _status_df.query(f"{cls.dcm_task_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + + return _status_df + + +@register_swagger_model +class DcmPushStatus(DcmPushStatuBase): + """ + 推送状态业务模型类(主业务类,完全继承 TD3iDcmPushStatu 字段)。 + + --- + description: 推送OA状态记录 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + dcm_task_id: + description: 唯一标志(任务ID) + type: integer + example: 2001 + push_task_status: + description: 推送待办工单状态 + type: integer + example: 1 + push_task_attachment_status: + description: 推送待办工单附件状态 + type: integer + example: 1 + push_task_extend_info_status: + description: 推送待办工单扩展信息状态 + type: integer + example: 1 + push_task_file_upload_status: + description: 上传待办工单文件状态 + type: integer + example: 1 + push_task_more_info_status: + description: 推送待办工单更多信息状态 + type: integer + example: 1 + push_task_process_info_status: + description: 推送待办工单处理过程状态 + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "D3I" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "D3I" + readOnly: true + """ + + @classmethod + async def create(cls, **kwargs): + """ + 创建新的推送状态记录。 + + 业务流程: + 1. 使用 kwargs 直接构造对象(无需表单验证,因无前端交互) + 2. 检查是否已存在相同任务ID的记录(避免重复) + 3. 创建新记录对象 + 4. 设置创建者和更新者为 'D3I' + 5. 保存到数据库 + 6. 返回创建的对象 + + :param kwargs: 推送状态参数字典 + :return: 新建推送状态对象 + :rtype: DcmPushStatus + :raises AssertionError: 当记录已存在时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 检查是否已存在同任务ID的记录 + _record: cls = await cls.is_exist(kwargs.get('dcm_task_id')) + assert _record is None, "相同任务ID的推送状态已存在,不能重复创建。" + + # 创建记录对象 + _record = cls().copy_from_dict(kwargs, skip_none=True).before_save() + # 强制设置创建者和更新者为 'D3I' + _record.created_by = 'D3I' + _record.updated_by = 'D3I' + await _record.async_save() + return _record + + @classmethod + async def delete(cls, id: Union[str, int]): + """ + 删除推送状态记录(软删除,不实际删除,仅用于逻辑隔离)。 + + 注意:此系统建议保留历史记录,删除操作仅为标记。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行物理删除(因无软删除字段,此处直接删除) + + :param id: 要删除的记录ID + :return: 删除的记录ID + :rtype: int + :raises AssertionError: 当记录不存在时抛出 + """ + _record: cls = await cls.async_find_by_id(id) + assert _record, f"根据 ID {id} 未找到推送状态记录。" + + # 执行物理删除 + _del_query = delete(cls).where(cls.id == id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除推送状态记录(ID:{id}).') + return _del_count + + @classmethod + async def modify(cls, id: Union[str, int], **kwargs): + """ + 修改已有推送状态信息。 + + 注意:仅允许更新状态码字段,不允许修改 id、created_at、created_by 等系统字段。 + + 业务流程: + 1. 处理字符串字段去除空格 + 2. 查询原记录 + 3. 验证存在性 + 4. 更新允许字段 + 5. 设置 updated_by = 'D3I' + 6. 保存到数据库 + 7. 返回更新后的对象 + + :param id: 要修改的记录ID + :param kwargs: 需要更新的字段(仅限状态字段) + :return: 修改后的推送状态对象 + :rtype: DcmPushStatus + :raises AssertionError: 当记录不存在时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 查询原记录 + _record: cls = await cls.async_find_by_id(id) + assert _record, f"根据 ID {id} 未找到推送状态记录。" + + # 允许更新的字段(仅状态码) + allowed_fields = { + 'push_task_status', + 'push_task_attachment_status', + 'push_task_extend_info_status', + 'push_task_file_upload_status', + 'push_task_more_info_status', + 'push_task_process_info_status', + } + + # 过滤合法字段 + update_data = {k: v for k, v in kwargs.items() if k in allowed_fields and v is not None} + if not update_data: + return _record + + # 更新字段 + _record.copy_from_dict(update_data, skip_none=True) + _record.updated_by = 'D3I' + await _record.async_save() + return _record + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新推送状态记录(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含推送状态数据的 DataFrame,字段需与模型属性匹配(如 dcm_task_id, push_task_status 等) + :return: 成功创建的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + records = [ + cls().copy_from_dict(record, skip_none=True).before_save() + for record in records + ] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条推送状态记录。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有推送状态记录。 + + :param data_df: 包含推送状态数据的 DataFrame,必须包含 id 列 + :return: 成功更新的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条推送状态记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :return 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmPushStatus.exists_relation(data_df) + # 保存到数据库 + _created_count = await DcmPushStatus.create_batch(_latest_df) + _updated_count = await DcmPushStatus.modify_batch(_exists_df) + return _created_count, _updated_count + + @classmethod + async def set_push_task_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_status=status) + # 保存数据 + await dcm_task.async_save() + + @classmethod + async def set_push_task_detail_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_detail_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_detail_status=status) + # 保存数据 + await dcm_task.async_save() + + @classmethod + async def set_push_task_attachment_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_attachment_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_attachment_status=status) + # 保存数据 + await dcm_task.async_save() + + @classmethod + async def set_push_task_extend_info_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_extend_info_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_extend_info_status=status) + # 保存数据 + await dcm_task.async_save() + + @classmethod + async def set_push_task_file_upload_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_file_upload_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_file_upload_status=status) + # 保存数据 + await dcm_task.async_save() + + @classmethod + async def set_push_task_more_info_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_more_info_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_more_info_status=status) + # 保存数据 + await dcm_task.async_save() + + @classmethod + async def set_push_task_process_info_status(cls, dcm_task_id: Union[str, int], status: int = 1): + dcm_task: cls = await cls(dcm_task_id=dcm_task_id).async_find_first() + if dcm_task: + dcm_task.push_task_process_info_status = status + else: + dcm_task = cls(dcm_task_id=dcm_task_id, push_task_process_info_status=status) + # 保存数据 + await dcm_task.async_save() \ No newline at end of file diff --git a/models/dcm_rollback.py b/models/dcm_rollback.py new file mode 100644 index 0000000..66f8e53 --- /dev/null +++ b/models/dcm_rollback.py @@ -0,0 +1,504 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmRollback +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmRollbackForm(ModelForm): + """ + 回退任务表单验证类(完全映射 TD3iDcmRollback 字段)。 + + 用于验证和处理数字城管-回退操作的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_dcm_rollback 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + rec_id = StringField('记录ID', validators=[Length(max=64, message='记录ID长度不能超过64字符')]) + task_number = StringField('任务号', validators=[Length(max=64, message='任务号长度不能超过64字符')]) + act_id = StringField('工单ID', validators=[Length(max=64, message='工单ID长度不能超过64字符')]) + transInfo = StringField('回退流向', validators=[Length(max=64, message='回退流向长度不能超过64字符')]) + save_old_act_flag = StringField('是否保留旧流程', + validators=[Length(max=64, message='是否保留旧流程长度不能超过64字符')]) + opinion = StringField('回退意见', validators=[Length(max=500, message='回退意见长度不能超过500字符')]) + rollback_reason_id = StringField('回退原因ID', + validators=[Length(max=500, message='回退原因ID长度不能超过500字符')]) + attachments = TextAreaField('附件', validators=[Length(max=65535, message='附件长度不能超过65535字符')]) + send_message = StringField('发送短信', validators=[Length(max=32, message='发送短信长度不能超过32字符')]) + not_assigned = StringField('申请不交办(0:不打勾,1:打勾)', + validators=[Length(max=16, message='申请不交办长度不能超过16字符')]) + not_assigned_reason = TextAreaField('申请不交办原因') + undertake_user_name = StringField('承办人员', validators=[Length(max=64, message='承办人员长度不能超过64字符')]) + undertake_phone = StringField('联系电话', validators=[Length(max=64, message='联系电话长度不能超过64字符')]) + status = IntegerField('提交状态') + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmRollbackBase(TD3iDcmRollback, CommonModel): + """ + 回退任务基础类(完全映射 TD3iDcmRollback 字段)。 + + 继承自数据库模型 TD3iDcmRollback 和通用模型 CommonModel。 + 封装所有与回退操作相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'rec_id': 'rec_id', + 'task_number': 'task_number', + 'act_id': 'act_id', + 'transInfo': 'transInfo', + 'save_old_act_flag': 'save_old_act_flag', + 'opinion': 'opinion', + 'rollback_reason_id': 'rollback_reason_id', + 'attachments': 'attachments', + 'send_message': 'send_message', + 'not_assigned': 'not_assigned', + 'not_assigned_reason': 'not_assigned_reason', + 'undertake_user_name': 'undertake_user_name', + 'undertake_phone': 'undertake_phone', + 'status': 'status', + } + """ + 回退数据映射 + """ + + @classmethod + async def is_exist(cls, rec_id: str): + """ + 检查回退记录是否已存在(根据记录ID)。 + + :param rec_id: 记录ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.rec_id == rec_id) + _rollback: cls = await cls.query_first(_query) + return _rollback + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索回退数据的基础方法。 + + 支持字段: + - task_number, rec_id, act_id, transInfo, status + - 支持模糊匹配:opinion, attachments + - 支持精确匹配:status, send_message + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_number': 'asc'} + :key str task_number: 精确匹配任务号 + :key str rec_id: 精确匹配记录ID + :key str act_id: 精确匹配工单ID + :key str transInfo: 精确匹配回退流向 + :key str opinion: 模糊匹配回退意见 + :key str attachments: 模糊匹配附件 + :key int status: 精确匹配提交状态 + :key str send_message: 精确匹配发送短信标志 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.opinion.key: '%{}%', + cls.attachments.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_number, cls.rec_id) + + _rollback_df = await cls.query_as_df(_data_query) + if not _rollback_df.empty: + _rollback_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _rollback_df[cls.id.key] = _rollback_df[cls.id.key].astype(str) + + return _rollback_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索回退数据,返回分页格式数据。 + """ + _rollback_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _rollback_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 rec_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 rec_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 rec_id 列表(去重) + rec_ids = data_df[cls.rec_id.key].unique().tolist() + if not rec_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 rec_id + _query = select(cls.id, cls.rec_id).where(cls.rec_id.in_(rec_ids)) + rec_ids_df = await cls.query_as_df(_query) + + if rec_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 rec_id -> id 的映射字典 + rec_id_to_id_map = dict(zip(rec_ids_df[cls.rec_id.key], rec_ids_df[cls.id.key])) + + # 根据 rec_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.rec_id.key].isin(rec_ids_df[cls.rec_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.rec_id.key].map(rec_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class DcmRollback(DcmRollbackBase): + """ + 回退任务模型类(主业务类,完全继承 TD3iDcmRollback 字段)。 + + --- + description: 数字城管-回退接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + rec_id: + description: 记录ID + type: string + example: "R20240501001" + maxLength: 64 + task_number: + description: 任务号 + type: string + example: "TASK20240501001" + maxLength: 64 + act_id: + description: 工单ID + type: string + example: "ACT20240501001" + maxLength: 64 + transInfo: + description: 回退流向(固定:市受理员) + type: string + example: "市受理员" + maxLength: 64 + save_old_act_flag: + description: 是否保留旧流程 + type: string + example: "1" + maxLength: 64 + opinion: + description: 回退意见 + type: string + example: "流程错误,需退回重新处理" + maxLength: 500 + rollback_reason_id: + description: 回退原因ID + type: string + example: "REASON_001" + maxLength: 500 + attachments: + description: 附件(多个用逗号分隔) + type: string + example: "file1.jpg,file2.pdf" + maxLength: 65535 + send_message: + description: 发送短信(1:发送,0:不发送) + type: string + example: "1" + maxLength: 32 + status: + description: 提交状态 + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的回退记录。 + + 业务流程: + 1. 使用 DcmRollbackForm 验证表单数据完整性 + 2. 检查是否已存在相同 rec_id 的记录(避免重复提交) + 3. 创建新回退对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 回退参数字典 + :return: 新建回退对象 + :rtype: DcmRollback + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmRollbackForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 rec_id 的记录 + _existing = await cls.is_exist(_form.rec_id.data) + assert _existing is None, "该任务已存在回退记录,不能重复提交。" + + # 创建对象 + _rollback = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _rollback.created_by = user.username + _rollback.updated_by = user.username + await _rollback.async_save() + return _rollback + + @classmethod + async def delete(cls, rollback_id: Union[str, int]): + """ + 删除回退记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param rollback_id: 要删除的回退记录ID + :return: 删除的记录对象 + :rtype: DcmRollback + :raises AssertionError: 当记录不存在时抛出 + """ + _rollback: cls = await cls.async_find_by_id(rollback_id) + assert _rollback, f"根据 ID {rollback_id} 未找到回退记录。" + + _del_query = delete(cls).where(cls.id == _rollback.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除回退记录(任务号:{_rollback.task_number},ID:{_rollback.id}).') + return _rollback + + @classmethod + async def modify(cls, rollback_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有回退记录。 + + 业务流程: + 1. 将 rollback_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 DcmRollbackForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param rollback_id: 要修改的回退记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的回退对象 + :rtype: DcmRollback + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = DcmRollbackForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _rollback: cls = await cls.async_find_by_id(rollback_id) + assert _rollback, f'查无此回退信息。' + + # 更新字段 + _rollback.copy_from_dict(_form.data, skip_none=True).before_save() + _rollback.updated_by = user.username + await _rollback.async_save() + return _rollback + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建回退记录(传入数据应为全新记录)。 + + :param data_df: 包含回退数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + rollbacks = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(rollbacks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(rollbacks)} 条回退记录。") + return len(rollbacks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有回退记录。 + + :param data_df: 包含回退数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条回退记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存回退数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmRollback.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmRollback.create_batch(_latest_df, user) + _updated_count = await DcmRollback.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/dcm_stage_reply.py b/models/dcm_stage_reply.py new file mode 100644 index 0000000..0a06e90 --- /dev/null +++ b/models/dcm_stage_reply.py @@ -0,0 +1,507 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField, TextAreaField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmStageReply +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmStageReplyForm(ModelForm): + """ + 阶段回复表单验证类(完全映射 TD3iDcmStageReply 字段)。 + + 用于验证和处理数字城管-阶段回复的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_dcm_stage_reply 的字段结构。 + """ + + # 基础信息 + id = IntegerField('主键ID') + rec_id = StringField('记录ID', validators=[Length(max=64, message='记录ID长度不能超过64字符')]) + task_number = StringField('任务号', validators=[Length(max=64, message='任务号长度不能超过64字符')]) + act_id = StringField('工单ID', validators=[Length(max=64, message='工单ID长度不能超过64字符')]) + item_type = StringField('固定值', validators=[Length(max=64, message='固定值长度不能超过64字符')]) + content = TextAreaField('回复内容', validators=[Length(max=1000, message='回复内容长度不能超过1000字符')]) + status = IntegerField('提交状态') + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmStageReplyBase(TD3iDcmStageReply, CommonModel): + """ + 阶段回复基础类(完全映射 TD3iDcmStageReply 字段)。 + + 继承自数据库模型 TD3iDcmStageReply 和通用模型 CommonModel。 + 封装所有与阶段回复相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'rec_id': 'rec_id', + 'task_number': 'task_number', + 'act_id': 'act_id', + 'item_type': 'item_type', + 'content': 'content', + 'status': 'status', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 回复数据映射 + """ + + @classmethod + async def exist_other(cls, id: Union[str, int], rec_id: str, task_number: str): + """ + 检查是否存在除当前回复外的其他相同记录ID和任务号的回复。 + + :param id: 当前回复ID + :param rec_id: 记录ID + :param task_number: 任务号 + :return: 存在返回回复对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.rec_id == rec_id, cls.task_number == task_number) + _reply: cls = await cls.query_first(_query) + return _reply + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找回复数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _reply_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _reply_list + + @classmethod + async def is_exist(cls, rec_id: str, task_number: str): + """ + 检查回复是否已经存在(根据记录ID和任务号)。 + """ + _query = select(cls).where(cls.rec_id == rec_id, cls.task_number == task_number) + _reply: cls = await cls.query_first(_query) + return _reply + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索回复数据的基础方法。 + + 支持字段: + - rec_id, task_number, act_id, item_type, status + - 支持模糊匹配:content + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_number': 'asc'} + :key str rec_id: 精确匹配记录ID + :key str task_number: 精确匹配任务号 + :key str act_id: 精确匹配工单ID + :key str item_type: 精确匹配固定值 + :key int status: 精确匹配提交状态 + :key str content: 模糊匹配回复内容 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.content.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_number, cls.rec_id) + + _reply_df = await cls.query_as_df(_data_query) + if not _reply_df.empty: + _reply_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _reply_df[cls.id.key] = _reply_df[cls.id.key].astype(str) + + return _reply_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索回复数据,返回分页格式数据。 + """ + _reply_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _reply_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 rec_id + task_number 判断。 + + :param data_df: 输入的数据框架,必须包含 rec_id 和 task_number 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 (rec_id, task_number) 组合 + rec_task_pairs = data_df[[cls.rec_id.key, cls.task_number.key]].values.tolist() + if not rec_task_pairs: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.rec_id, cls.task_number).where( + (cls.rec_id.in_([r[0] for r in rec_task_pairs])) & + (cls.task_number.in_([r[1] for r in rec_task_pairs])) + ) + rec_task_df = await cls.query_as_df(_query) + + if rec_task_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 (rec_id, task_number) -> id 的映射字典 + rec_task_to_id_map = dict(zip( + zip(rec_task_df[cls.rec_id.key], rec_task_df[cls.task_number.key]), + rec_task_df[cls.id.key] + )) + + # 标记是否已存在 + mask_exists = data_df.apply( + lambda row: (row[cls.rec_id.key], row[cls.task_number.key]) in rec_task_to_id_map, axis=1 + ) + + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df.apply( + lambda row: rec_task_to_id_map[(row[cls.rec_id.key], row[cls.task_number.key])], axis=1 + ) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class DcmStageReply(DcmStageReplyBase): + """ + 阶段回复主业务类(完全继承 TD3iDcmStageReply 字段)。 + + --- + description: 数字城管-阶段回复接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + rec_id: + description: 记录ID + type: string + example: "REC20240501001" + maxLength: 64 + task_number: + description: 任务号 + type: string + example: "TASK20240501001" + maxLength: 64 + act_id: + description: 工单ID + type: string + example: "ACT20240501001" + maxLength: 64 + item_type: + description: 固定值 + type: string + example: "STAGE_REPLY" + maxLength: 64 + content: + description: 回复内容 + type: string + example: "已处理完毕,请查收。" + maxLength: 1000 + status: + description: 提交状态 + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的阶段回复。 + + 业务流程: + 1. 使用 DcmStageReplyForm 验证表单数据完整性 + 2. 检查是否已存在相同 rec_id + task_number 的回复 + 3. 创建新回复对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的回复对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 回复参数字典 + :return: 新建回复对象 + :rtype: DcmStageReply + :raises AssertionError: 当回复已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _reply_form = DcmStageReplyForm(formdata=kwargs) + _reply_form.validate_form() + + # 检查是否存在相同记录ID和任务号的回复 + _reply: cls = await cls.is_exist(_reply_form.rec_id.data, _reply_form.task_number.data) + assert _reply is None, "相同记录ID和任务号的回复已存在,不能重复创建。" + + # 创建回复对象 + _reply = cls().copy_from_dict(_reply_form.data, skip_none=True).before_save() + if user: + _reply.created_by = user.username + _reply.updated_by = user.username + await _reply.async_save() + return _reply + + @classmethod + async def delete(cls, reply_id: Union[str, int]): + """ + 删除阶段回复。 + + 业务流程: + 1. 根据ID查找回复 + 2. 验证回复存在性 + 3. 执行删除操作 + + :param reply_id: 要删除的回复ID + :return: 删除的回复对象 + :rtype: DcmStageReply + :raises AssertionError: 当回复不存在时抛出 + """ + _reply: cls = await cls.async_find_by_id(reply_id) + assert _reply, f"根据 ID {reply_id} 未找到阶段回复。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _reply.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除阶段回复(ID:{_reply.id})。') + return _reply + + @classmethod + async def modify(cls, reply_id: Union[str, int], user: RbacUser=None, **kwargs): + """ + 修改已有阶段回复信息。 + + 业务流程: + 1. 将 reply_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 DcmStageReplyForm 验证表单数据 + 4. 检查是否与其他回复冲突(排除自身) + 5. 查询原回复对象 + 6. 验证回复存在性 + 7. 更新字段并设置更新者 + 8. 保存到数据库 + 9. 返回更新后的回复对象 + + :param reply_id: 要修改的回复ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的回复对象 + :rtype: DcmStageReply + :raises AssertionError: 当回复不存在或信息冲突时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _reply_form = DcmStageReplyForm(formdata=kwargs) + _reply_form.validate_form() + + # 检查是否与其他回复重复(排除自身) + _other = await cls.exist_other(reply_id, _reply_form.rec_id.data, _reply_form.task_number.data) + assert _other is None, "相同记录ID和任务号的回复已存在,不能重复修改。" + + # 查询原回复 + _reply: cls = await cls.async_find_by_id(reply_id) + assert _reply, f'查无此阶段回复信息。' + + # 更新字段 + _reply.copy_from_dict(_reply_form.data, skip_none=True).before_save() + _reply.updated_by = user.username + await _reply.async_save() + return _reply + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建新回复(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含回复数据的 DataFrame,字段需与模型属性匹配(如 rec_id, task_number 等) + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的回复数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 向量化设置用户字段(无循环) + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + replies = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(replies) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(replies)} 条新阶段回复。") + return len(replies) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有回复。 + + :param data_df: 包含回复数据的 DataFrame,必须包含 id 列 + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的回复数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条阶段回复。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmStageReply.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmStageReply.create_batch(_latest_df, user) + _updated_count = await DcmStageReply.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task.py b/models/dcm_task.py new file mode 100644 index 0000000..7eda14e --- /dev/null +++ b/models/dcm_task.py @@ -0,0 +1,829 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, FloatField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTask +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmTaskForm(ModelForm): + """ + 专业表单验证类(已完全根据 TD3iDcmTask 字段重构)。 + + 用于验证和处理数字城管-部门待办任务的创建/修改表单数据。 + 字段完全映射数据库表 t_dcm_department_task 的字段结构。 + """ + + # 基础信息 + rec_id = IntegerField('记录ID') + rec_disp_num = StringField('显示编号', validators=[Length(max=50, message='显示编号长度不能超过50字符')]) + rec_type_id = IntegerField('类型ID') + rec_type_name = StringField('案件类型', validators=[Length(max=100, message='案件类型长度不能超过100字符')]) + + # 任务信息 + act_id = IntegerField('任务ID') + act_deadline_time = IntegerField('任务截止时间戳(毫秒)') + act_warning_time = IntegerField('预警时间戳(毫秒)') + act_property_id = IntegerField('任务属性ID') + act_ard_state_name = StringField('阶段授权状态', validators=[Length(max=50, message='阶段授权状态长度不能超过50字符')]) + act_time_state_id = IntegerField('阶段状态ID') + + # 业务信息 + biz_id = IntegerField('业务ID') + sys_id = IntegerField('系统ID') + task_num = StringField('任务号', validators=[Length(max=50, message='任务号长度不能超过50字符')]) + other_task_num = StringField('第三方任务号', validators=[Length(max=100, message='第三方任务号长度不能超过100字符')]) + bundle_remain_char = StringField('剩余时间描述', validators=[Length(max=20, message='剩余时间描述长度不能超过20字符')]) + bundle_deadline_time = IntegerField('捆绑截止时间戳') + bundle_deadline_char = StringField('捆绑截止时间描述', validators=[Length(max=20, message='捆绑截止时间描述长度不能超过20字符')]) + bundle_warning_time = IntegerField('捆绑预警时间戳') + bundle_time_state_id = IntegerField('捆绑阶段红绿灯状态') + + # 事件信息 + event_type_id = IntegerField('问题类型ID') + max_event_type_id = IntegerField('最大事件类型ID') + event_type_name = StringField('问题类型', validators=[Length(max=100, message='问题类型长度不能超过100字符')]) + event_src_name = StringField('问题来源', validators=[Length(max=100, message='问题来源长度不能超过100字符')]) + event_desc = TextAreaField('问题描述', validators=[Length(max=65535, message='问题描述长度不能超过65535字符')]) + + # 紧急程度与分类 + urgency_level = IntegerField('紧急程度(0正常,1紧急)') + main_type_id = IntegerField('大类ID') + main_type_name = StringField('大类名称', validators=[Length(max=100, message='大类名称长度不能超过100字符')]) + sub_type_id = IntegerField('小类ID') + sub_type_name = StringField('小类名称', validators=[Length(max=100, message='小类名称长度不能超过100字符')]) + + # 地址与坐标 + address = TextAreaField('地址描述', validators=[Length(max=65535, message='地址描述长度不能超过65535字符')]) + district_name = StringField('所属区域', validators=[Length(max=50, message='所属区域长度不能超过50字符')]) + coordinate_x = FloatField('经度') + coordinate_y = FloatField('纬度') + + # 处理流程 + proc_time_state_id = IntegerField('处理流程状态ID') + deadline_time = IntegerField('处理截止时间戳') + warning_time = IntegerField('处理预警时间戳') + processing_deadline = StringField('处置时限描述', validators=[Length(max=50, message='处置时限描述长度不能超过50字符')]) + new_inst_cond_name = StringField('立案条件', validators=[Length(max=200, message='立案条件长度不能超过200字符')]) + case_closure_condition = StringField('结案条件', validators=[Length(max=200, message='结案条件长度不能超过200字符')]) + + # 回复与回访 + reply_intime = IntegerField('是否两小时回复(0无需回复,1待回复,2已回复,3超时,4无需回复已恢复)') + return_visit_flag = IntegerField('回访标识(0无需,1待回访,2已回访)') + + # 部门信息 + first_depart_name = StringField('一级专业部门', validators=[Length(max=100, message='一级专业部门长度不能超过100字符')]) + second_depart_name = StringField('二级专业部门', validators=[Length(max=100, message='二级专业部门长度不能超过100字符')]) + + # 举报人信息 + reporter_name = StringField('举报人姓名', validators=[Length(max=100, message='举报人姓名长度不能超过100字符')]) + reporter_contact = StringField('举报电话', validators=[Length(max=50, message='举报电话长度不能超过50字符')]) + + # 阅读与颜色 + read_flag = IntegerField('是否已读(0未读,1已读)') + back_color_bit_id = IntegerField('背景色ID') + font_color_bit_id = IntegerField('字体色ID') + + # 部件与显示 + part_code = StringField('部件编码', validators=[Length(max=100, message='部件编码长度不能超过100字符')]) + display_style_id = IntegerField('显示样式ID') + + # 功能控制 + func_forbid_reporter_info_flag = IntegerField('是否禁止举报人信息') + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskBase(TD3iDcmTask, CommonModel): + """ + 专业基础类(已完全映射 TD3iDcmTask 字段)。 + + 继承自数据库模型 TD3iDcmTask 和通用模型 CommonModel。 + 封装所有与部门待办任务相关的通用操作方法。 + """ + + + FieldMapping = { + 'id': 'id', + 'rec_id': 'rec_id', + 'rec_disp_num': 'rec_disp_num', + 'rec_type_id': 'rec_type_id', + 'rec_type_name': 'rec_type_name', + 'act_id': 'act_id', + 'act_deadline_time': 'act_deadline_time', + 'act_warning_time': 'act_warning_time', + 'act_property_id': 'act_property_id', + 'act_ard_state_name': 'act_ard_state_name', + 'act_time_state_id': 'act_time_state_id', + 'biz_id': 'biz_id', + 'sys_id': 'sys_id', + 'task_num': 'task_num', + 'other_task_num': 'other_task_num', + 'bundle_remain_char': 'bundle_remain_char', + 'bundle_deadline_time': 'bundle_deadline_time', + 'bundle_deadline_char': 'bundle_deadline_char', + 'bundle_warning_time': 'bundle_warning_time', + 'bundle_time_state_id': 'bundle_time_state_id', + 'rollback_deadline': 'rollback_deadline', + 'event_type_id': 'event_type_id', + 'max_event_type_id': 'max_event_type_id', + 'event_type_name': 'event_type_name', + 'event_src_name': 'event_src_name', + 'event_desc': 'event_desc', + 'urgency_level': '紧急程度', + 'main_type_id': 'main_type_id', + 'main_type_name': 'main_type_name', + 'sub_type_id': 'sub_type_id', + 'sub_type_name': 'sub_type_name', + 'address': 'address', + 'district_name': 'district_name', + 'coordinate_x': 'coordinate_x', + 'coordinate_y': 'coordinate_y', + 'proc_time_state_id': 'proc_time_state_id', + 'deadline_time': 'deadline_time', + 'warning_time': 'warning_time', + 'processing_deadline': '处置时限', + 'new_inst_cond_name': 'new_inst_cond_name', + 'case_closure_condition': '结案条件', + 'reply_intime': 'reply_intime', + 'return_visit_flag': 'return_visit_flag', + 'first_depart_name': 'first_depart_name', + 'second_depart_name': 'second_depart_name', + 'reporter_name': 'reporter_name', + 'reporter_contact': 'reporter_contact', + 'read_flag': 'read_flag', + 'back_color_bit_id': 'back_color_bit_id', + 'font_color_bit_id': 'font_color_bit_id', + 'part_code': 'part_code', + 'display_style_id': 'display_style_id', + 'func_forbid_reporter_info_flag': 'func_forbid_reporter_info_flag', + } + """ + 任务数据映射 + """ + + @classmethod + async def exist_other(cls, id: Union[str, int], rec_id: str): + """ + 检查是否存在除当前任务外的其他同任务号或同显示编号的任务。 + + :param id: 当前任务ID + :param task_num: 任务号 + :param rec_disp_num: 显示编号 + :return: 存在返回任务对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.rec_id == rec_id) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找任务数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _task_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _task_list + + @classmethod + async def is_exist(cls, rec_id: str): + """ + 检查任务是否已经存在(根据任务号或显示编号)。 + """ + _query = select(cls).where(cls.rec_id == rec_id) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索任务数据的基础方法。 + + 支持字段: + - task_num, rec_disp_num, event_type_name, district_name, urgency_level, read_flag 等 + - 支持模糊匹配:event_type_name, rec_type_name, event_src_name, first_depart_name, second_depart_name + - 支持精确匹配:biz_id, sys_id, urgency_level, read_flag, rec_type_id, act_time_state_id 等 + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_num': 'asc'} + :key str task_num: 精确匹配任务号 + :key str rec_disp_num: 精确匹配显示编号 + :key str event_type_name: 模糊匹配问题类型 + :key str district_name: 精确匹配区域 + :key int urgency_level: 精确匹配紧急程度 + :key int read_flag: 精确匹配是否已读 + :key int biz_id: 精确匹配业务ID + :key int sys_id: 精确匹配系统ID + :key int rec_type_id: 精确匹配类型ID + :key int act_time_state_id: 精确匹配阶段状态ID + :key int deadline_time: 精确匹配处理截止时间戳 + :key int act_deadline_time: 精确匹配任务截止时间戳 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.event_type_name.key: '%{}%', + cls.rec_type_name.key: '%{}%', + cls.event_src_name.key: '%{}%', + cls.first_depart_name.key: '%{}%', + cls.second_depart_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_num, cls.rec_disp_num) + + _task_df = await cls.query_as_df(_data_query) + if not _task_df.empty: + _task_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _task_df[cls.id.key] = _task_df[cls.id.key].astype(str) + + return _task_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索任务数据,返回分页格式数据。 + """ + _task_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _task_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 rec_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 rec_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 rec_id 列表(去重) + rec_ids = data_df[cls.rec_id.key].unique().tolist() + if not rec_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 rec_id + _query = select(cls.id, cls.rec_id).where(cls.rec_id.in_(rec_ids)) + rec_ids_df = await cls.query_as_df(_query) + + if rec_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 rec_id -> id 的映射字典 + rec_id_to_id_map = dict(zip(rec_ids_df[cls.rec_id.key], rec_ids_df[cls.id.key])) + + # 根据 rec_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.rec_id.key].isin(rec_ids_df[cls.rec_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.rec_id.key].map(rec_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class DcmTask(DcmTaskBase): + """ + 部门待办任务类(主业务类,完全继承 TD3iDcmTask 字段)。 + + --- + description: 数字城管-部门待办任务 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + rec_id: + description: 记录ID + type: integer + example: 2001 + rec_disp_num: + description: 显示编号 + type: string + example: "D20240501001" + maxLength: 50 + rec_type_id: + description: 类型ID + type: integer + example: 101 + rec_type_name: + description: 案件类型 + type: string + example: "市容环境" + maxLength: 100 + act_id: + description: 任务ID + type: integer + example: 3001 + act_deadline_time: + description: 任务截止时间戳(毫秒) + type: integer + example: 1714567890000 + act_warning_time: + description: 预警时间戳(毫秒) + type: integer + example: 1714560000000 + act_property_id: + description: 任务属性ID + type: integer + example: 5 + act_ard_state_name: + description: 阶段授权状态 + type: string + example: "已授权" + maxLength: 50 + act_time_state_id: + description: 阶段状态ID + type: integer + example: 1 + biz_id: + description: 业务ID + type: integer + example: 10 + sys_id: + description: 系统ID + type: integer + example: 1 + task_num: + description: 任务号 + type: string + example: "TASK20240501001" + maxLength: 50 + other_task_num: + description: 第三方任务号 + type: string + example: "THIRD-2024-001" + maxLength: 100 + bundle_remain_char: + description: 剩余时间描述 + type: string + example: "3天" + maxLength: 20 + bundle_deadline_time: + description: 捆绑截止时间戳 + type: integer + example: 1714578000000 + bundle_deadline_char: + description: 捆绑截止时间描述 + type: string + example: "3天" + maxLength: 20 + bundle_warning_time: + description: 捆绑预警时间戳 + type: integer + example: 1714570000000 + bundle_time_state_id: + description: 捆绑阶段红绿灯状态 + type: integer + example: 0 + rollback_deadline: + description: 拒绝超时截止时间戳 + type: integer + example: 1714580000000 + event_type_id: + description: 问题类型ID + type: integer + example: 1001 + max_event_type_id: + description: 最大事件类型ID + type: integer + example: 1002 + event_type_name: + description: 问题类型 + type: string + example: "道路破损" + maxLength: 100 + event_src_name: + description: 问题来源 + type: string + example: "市民举报" + maxLength: 100 + event_desc: + description: 问题描述 + type: string + example: "中山路与解放路交叉口路面大面积破损" + maxLength: 65535 + urgency_level: + description: 紧急程度(0正常,1紧急) + type: integer + example: 1 + main_type_id: + description: 大类ID + type: integer + example: 101 + main_type_name: + description: 大类名称 + type: string + example: "市容环境" + maxLength: 100 + sub_type_id: + description: 小类ID + type: integer + example: 10101 + sub_type_name: + description: 小类名称 + type: string + example: "道路破损" + maxLength: 100 + address: + description: 地址描述 + type: string + example: "中山路与解放路交叉口" + maxLength: 65535 + district_name: + description: 所属区域 + type: string + example: "鼓楼区" + maxLength: 50 + coordinate_x: + description: 经度 + type: number + format: decimal + example: 118.789012 + coordinate_y: + description: 纬度 + type: number + format: decimal + example: 32.045678 + proc_time_state_id: + description: 处理流程状态ID + type: integer + example: 2 + deadline_time: + description: 处理截止时间戳 + type: integer + example: 1714578000000 + warning_time: + description: 处理预警时间戳 + type: integer + example: 1714570000000 + processing_deadline: + description: 处置时限描述 + type: string + example: "24小时" + maxLength: 50 + new_inst_cond_name: + description: 立案条件 + type: string + example: "破损面积大于0.5㎡" + maxLength: 200 + case_closure_condition: + description: 结案条件 + type: string + example: "修复完成并验收" + maxLength: 200 + reply_intime: + description: 是否两小时回复(0无需回复,1待回复,2已回复,3超时,4无需回复已恢复) + type: integer + example: 2 + return_visit_flag: + description: 回访标识(0无需,1待回访,2已回访) + type: integer + example: 1 + first_depart_name: + description: 一级专业部门 + type: string + example: "市政工程处" + maxLength: 100 + second_depart_name: + description: 二级专业部门 + type: string + example: "道路养护科" + maxLength: 100 + reporter_name: + description: 举报人姓名 + type: string + example: "张三" + maxLength: 100 + reporter_contact: + description: 举报电话 + type: string + example: "13800138000" + maxLength: 50 + read_flag: + description: 是否已读(0未读,1已读) + type: integer + example: 1 + back_color_bit_id: + description: 背景色ID + type: integer + example: 10 + font_color_bit_id: + description: 字体色ID + type: integer + example: 20 + part_code: + description: 部件编码 + type: string + example: "P0012345" + maxLength: 100 + display_style_id: + description: 显示样式ID + type: integer + example: 5 + func_forbid_reporter_info_flag: + description: 是否禁止举报人信息 + type: integer + example: 0 + operation: + description: 操作(工单上的操作按钮) + type: string + example: "批转" + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的任务。 + + 业务流程: + 1. 使用 DcmDepartmentTaskForm 验证表单数据完整性 + 2. 检查任务是否已存在(根据 task_num 或 rec_disp_num) + 3. 创建新任务对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的任务对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 任务参数字典 + :return: 新建任务对象 + :rtype: DcmTask + :raises AssertionError: 当任务已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _task_form = DcmTaskForm(formdata=kwargs) + _task_form.validate_form() + + # 检查是否存在同任务号或同显示编号的任务 + _task: cls = await cls.is_exist(_task_form.rec_id.data) + assert _task is None, "任务号或显示编号已存在,不能重复创建。" + + # 创建任务对象 + _task = cls().copy_from_dict(_task_form.data, skip_none=True).before_save() + if user: + _task.created_by = user.username + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def delete(cls, task_id: Union[str, int]): + """ + 删除任务。 + + 注意:任务删除需根据业务规则判断是否允许(如是否已处理、是否有附件等)。 + + 业务流程: + 1. 根据ID查找任务 + 2. 验证任务存在性 + 3. 执行删除操作 + + :param task_id: 要删除的任务ID + :return: 删除的任务对象 + :rtype: DcmTask + :raises AssertionError: 当任务不存在时抛出 + """ + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f"根据 ID {task_id} 未找到任务。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _task.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除任务(任务号:{_task.task_num},ID:{_task.id}).') + return _task + + @classmethod + async def modify(cls, task_id: Union[str, int], user: RbacUser=None, **kwargs): + """ + 修改已有任务信息。 + + 注意:修改任务号或显示编号时需检查是否与其他任务重复。 + + 业务流程: + 1. 将 task_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 DcmDepartmentTaskForm 验证表单数据 + 4. 检查是否有其他任务使用了相同的 task_num 或 rec_disp_num + 5. 查询原任务对象 + 6. 验证任务存在性 + 7. 更新字段并设置更新者 + 8. 保存到数据库 + 9. 返回更新后的任务对象 + + :param task_id: 要修改的任务ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的任务对象 + :rtype: DcmTask + :raises AssertionError: 当任务不存在或信息重复时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _task_form = DcmTaskForm(formdata=kwargs) + _task_form.validate_form() + + # 检查是否与其他任务重复(排除自身) + _other = await cls.exist_other(task_id, _task_form.rec_id.data) + assert _other is None, "待办任务号或显示编号已存在,不能重复修改。" + + # 查询原任务 + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f'查无此待办信息。' + + # 更新字段 + _task.copy_from_dict(_task_form.data, skip_none=True).before_save() + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建新任务(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含任务数据的 DataFrame,字段需与模型属性匹配(如 rec_id, task_num 等) + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的任务数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 向量化设置用户字段(无循环) + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + tasks = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(tasks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(tasks)} 条新待办。") + return len(tasks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有任务。 + + :param data_df: 包含任务数据的 DataFrame + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的任务数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条待办。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmTask.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmTask.create_batch(_latest_df, user) + _updated_count = await DcmTask.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task_attachment.py b/models/dcm_task_attachment.py new file mode 100644 index 0000000..0e58de0 --- /dev/null +++ b/models/dcm_task_attachment.py @@ -0,0 +1,734 @@ +import random +from typing import Union, Optional, Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTaskAttachment +from paste.core.logging import echo_log +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmTaskAttachmentForm(ModelForm): + """ + 附件表单验证类(完全映射 TD3iDcmTaskAttachment 字段)。 + + 用于验证和处理数字城管-部门待办任务附件的上传/修改表单数据。 + 字段完全映射数据库表 t_d3i_dcm_task_attachment 的字段结构。 + """ + + # 关联信息 + relation_type_id = IntegerField('关联类型ID') + relation_id = IntegerField('主关联ID') + relation_main_id = IntegerField('主关联ID(可空)') + relation_sub_id = IntegerField('子关联ID(可空)') + + # 媒体信息 + act_def_name = StringField('流程节点名称', validators=[Length(max=255, message='流程节点名称长度不能超过255字符')]) + media_id = IntegerField('媒体唯一ID') + media_path = StringField('服务器存储路径', validators=[Length(max=512, message='存储路径长度不能超过512字符')]) + media_type = StringField('媒体类型', validators=[Length(max=50, message='媒体类型长度不能超过50字符')]) + media_name = StringField('原始文件名', validators=[Length(max=255, message='原始文件名长度不能超过255字符')]) + media_usage = StringField('使用场景', validators=[Length(max=100, message='使用场景长度不能超过100字符')]) + media_server_name = StringField('媒体服务器名称', validators=[Length(max=100, message='媒体服务器名称长度不能超过100字符')]) + media_property = IntegerField('媒体属性') + media_uploaded_name = StringField('上传时的原始文件名', validators=[Length(max=255, message='上传文件名长度不能超过255字符')]) + media_shot = StringField('截图标识或路径', validators=[Length(max=255, message='截图路径长度不能超过255字符')]) + media_label_type_id = IntegerField('标签类型ID') + media_url = StringField('内部访问URL', validators=[Length(max=512, message='内部URL长度不能超过512字符')]) + media_default_url = StringField('外部可访问URL', validators=[Length(max=512, message='外部URL长度不能超过512字符')]) + display_order = IntegerField('显示顺序') + store_type_id = IntegerField('存储类型ID') + + # 图像信息 + special_item_image_type = StringField('特殊图片类型', validators=[Length(max=100, message='特殊图片类型长度不能超过100字符')]) + height = IntegerField('图片高度') + width = IntegerField('图片宽度') + + # 上传与状态 + send_flag = IntegerField('发送标志') + public_flag = IntegerField('公开标志(0私有,1公开)') + unit_name = StringField('所属单位', validators=[Length(max=255, message='单位名称长度不能超过255字符')]) + gen_thumb = IntegerField('是否生成缩略图(0否,1是)') + can_delete = IntegerField('是否可删除(0否,1是)') + + # 时间与人员 + upload_time = IntegerField('上传时间戳(毫秒)') + create_human_id = IntegerField('创建人ID') + human_name = StringField('创建人姓名', validators=[Length(max=255, message='创建人姓名长度不能超过255字符')]) + create_time = IntegerField('创建时间戳(毫秒)') + update_time = IntegerField('更新时间戳(毫秒)') + delete_reason = TextAreaField('删除原因', validators=[Length(max=65535, message='删除原因长度不能超过65535字符')]) + delete_flag = IntegerField('删除标记(0未删,1已删)') + delete_human_id = IntegerField('删除人ID') + delete_time = IntegerField('删除时间戳(毫秒)') + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskAttachmentBase(TD3iDcmTaskAttachment, CommonModel): + """ + 附件基础类(完全映射 TD3iDcmTaskAttachment 字段)。 + + 封装所有与任务附件相关的通用操作方法。 + """ + + FieldMapping = { + 'store_type_id': 'storeTypeID', + 'relation_type_id': 'relationTypeID', + 'relation_id': 'relationID', + 'relation_main_id': 'relationMainID', + 'relation_sub_id': 'relationSubID', + 'media_type': 'mediaType', + 'media_name': 'mediaName', + 'media_usage': 'mediaUsage', + 'create_time': 'createTime', + 'update_time': 'updateTime', + 'display_order': 'displayOrder', + 'delete_reason': 'deleteReason', + 'delete_flag': 'deleteFlag', + 'create_human_id': 'createHumanID', + 'delete_human_id': 'deleteHumanID', + 'delete_time': 'deleteTime', + 'media_path': 'mediaPath', + 'media_server_name': 'mediaServerName', + 'media_property': 'mediaProperty', + 'special_item_image_type': 'specialitemImageType', + 'media_uploaded_name': 'mediaUploadedName', + 'height': 'height', + 'width': 'width', + 'send_flag': 'sendFlag', + 'media_shot': 'mediaShot', + 'public_flag': 'publicFlag', + 'media_label_type_id': 'mediaLabelTypeID', + 'media_url': 'mediaURL', + 'media_default_url': 'mediaDefaultURL', + 'human_name': 'humanName', + 'unit_name': 'unitName', + 'act_def_name': 'actDefName', + 'upload_time': 'uploadTime', + 'gen_thumb': 'genThumb', + 'can_delete': 'canDelete', + 'media_id': 'mediaID', + } + """ + 附件数据映射 + """ + + @classmethod + async def exist_other(cls, id: Union[str, int], relation_id: Union[str, int], media_id: Union[str, int]): + """ + 检查是否存在除当前附件外的其他同关联ID和类型附件。 + + :param id: 当前附件ID + :param relation_id: 关联主ID + :param media_id: 媒体ID + :return: 存在返回附件对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.relation_id == relation_id, cls.media_id == media_id) + _attachment: cls = await cls.query_first(_query) + return _attachment + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找附件数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _attachment_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _attachment_list + + @classmethod + async def is_exist(cls, relation_id: Union[str, int], media_id: Union[str, int]): + """ + 检查附件是否已经存在(根据关联ID和类型)。 + """ + _query = select(cls).where(cls.relation_id == relation_id, cls.media_id == media_id) + _attachment: cls = await cls.query_first(_query) + return _attachment + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索附件数据的基础方法。 + + 支持字段: + - relation_type_id, relation_id, media_type, unit_name, delete_flag + - 支持模糊匹配:media_name, act_def_name, media_usage + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'display_order': 'asc'} + :key int relation_type_id: 精确匹配关联类型 + :key int relation_id: 精确匹配主关联ID + :key str media_type: 精确匹配媒体类型 + :key str unit_name: 精确匹配单位 + :key int delete_flag: 精确匹配删除标记 + :key str media_name: 模糊匹配原始文件名 + :key str act_def_name: 模糊匹配流程节点名称 + :key str media_usage: 模糊匹配使用场景 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.media_name.key: '%{}%', + cls.act_def_name.key: '%{}%', + cls.media_usage.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.display_order) + + _attachment_df = await cls.query_as_df(_data_query) + if not _attachment_df.empty: + _attachment_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + return _attachment_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索附件数据,返回分页格式数据。 + """ + _attachment_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _attachment_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_relation(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 relation_id + relation_type_id 判断。 + + :param data_df: 输入的数据框架,必须包含 relation_id 和 relation_type_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 (relation_id, media_id) 组合 + pairs = data_df[[cls.relation_id.key, cls.media_id.key]].drop_duplicates().values.tolist() + if not pairs: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.relation_id, cls.media_id).where( + (cls.relation_id.in_([p[0] for p in pairs])) & + (cls.media_id.in_([p[1] for p in pairs])) + ) + exists_df = await cls.query_as_df(_query) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 (relation_id, media_id) -> id 的映射 + key_to_id_map = dict(zip( + zip(exists_df[cls.relation_id.key], exists_df[cls.media_id.key]), + exists_df[cls.id.key]) + ) + + # 根据组合是否在数据库中划分数据 + mask_exists = data_df.apply(lambda row: (row[cls.relation_id.key], row[cls.media_id.key]) in key_to_id_map, axis=1) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df.apply(lambda row: key_to_id_map[(row[cls.relation_id.key], row[cls.media_id.key])], axis=1) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def fill_attachment(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'attachments', is_full: bool = True, + preprocessing: Optional[Callable] = None): + """ + 填充附件数据到数据框架。 + + 用于在查询结果中添加关联的附件信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`attachments` + :param is_full: 是否填充上传数据 + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 附件数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_id.in_(_task_ids)) + + if is_full: + # 默认加入文件上传得到的 OA MediaId 值 + from models.dcm_task_file_upload import DcmTaskFileUpload + _query = _query.add_columns( + DcmTaskFileUpload.oa_media_id + ).join( + DcmTaskFileUpload, DcmTaskFileUpload.dcm_task_attachment_id == cls.id + ) + + _atta_df: pd.DataFrame = await cls.query_as_df(_query) + if not _atta_df.empty: + _atta_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _atta_df[cls.id.key] = _atta_df[cls.id.key].astype(str) + _atta_df[cls.dcm_task_id.key] = _atta_df[cls.dcm_task_id.key].astype(str) + + # 设置索引 + _atta_df['index_id'] = _atta_df[cls.id.key] + _atta_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _atta_df = preprocessing(_atta_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _atta_df.query(f"{cls.dcm_task_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + + return _atta_df + + +@register_swagger_model +class DcmTaskAttachment(DcmTaskAttachmentBase): + """ + 附件业务模型类(主业务类,完全继承 TD3iDcmTaskAttachment 字段)。 + + --- + description: 数字城管-部门待办任务附件 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + relation_type_id: + description: 关联类型ID + type: integer + example: 1 + relation_id: + description: 主关联ID + type: integer + example: 2001 + relation_main_id: + description: 主关联ID(可空) + type: integer + example: 2001 + relation_sub_id: + description: 子关联ID(可空) + type: integer + example: 2002 + act_def_name: + description: 流程节点名称 + type: string + example: "受理" + maxLength: 255 + media_id: + description: 媒体唯一ID + type: integer + example: 3001 + media_path: + description: 服务器存储路径 + type: string + example: "/uploads/2024/05/01/photo.jpg" + maxLength: 512 + media_type: + description: 媒体类型(IMAGE, VIDEO等) + type: string + example: "IMAGE" + maxLength: 50 + media_name: + description: 原始文件名 + type: string + example: "IMG_20240501.jpg" + maxLength: 255 + media_usage: + description: 使用场景 + type: string + example: "上报" + maxLength: 100 + media_server_name: + description: 媒体服务器名称 + type: string + example: "oss-1" + maxLength: 100 + media_property: + description: 媒体属性 + type: integer + example: 1 + media_uploaded_name: + description: 上传时的原始文件名 + type: string + example: "DSC_001.jpg" + maxLength: 255 + media_shot: + description: 截图标识或路径 + type: string + example: "/thumbs/photo.jpg" + maxLength: 255 + media_label_type_id: + description: 标签类型ID + type: integer + example: 5 + media_url: + description: 内部访问URL + type: string + example: "http://internal/oss/123" + maxLength: 512 + media_default_url: + description: 外部可访问URL + type: string + example: "https://public.example.com/oss/123" + maxLength: 512 + display_order: + description: 显示顺序 + type: integer + example: 1 + store_type_id: + description: 存储类型ID + type: integer + example: 1 + special_item_image_type: + description: 特殊图片类型 + type: string + example: "现场照片" + maxLength: 100 + height: + description: 图片高度 + type: integer + example: 1080 + width: + description: 图片宽度 + type: integer + example: 1920 + send_flag: + description: 发送标志 + type: integer + example: 1 + public_flag: + description: 公开标志(0私有,1公开) + type: integer + example: 1 + unit_name: + description: 所属单位 + type: string + example: "市政工程处" + maxLength: 255 + gen_thumb: + description: 是否生成缩略图(0否,1是) + type: integer + example: 1 + can_delete: + description: 是否可删除(0否,1是) + type: integer + example: 1 + upload_time: + description: 上传时间戳(毫秒) + type: integer + example: 1714567890000 + create_human_id: + description: 创建人ID + type: integer + example: 101 + human_name: + description: 创建人姓名 + type: string + example: "张三" + maxLength: 255 + create_time: + description: 创建时间戳(毫秒) + type: integer + example: 1714567890000 + update_time: + description: 更新时间戳(毫秒) + type: integer + example: 1714567900000 + delete_reason: + description: 删除原因 + type: string + example: "重复上传" + maxLength: 65535 + delete_flag: + description: 删除标记(0未删,1已删) + type: integer + example: 0 + delete_human_id: + description: 删除人ID + type: integer + example: 102 + delete_time: + description: 删除时间戳(毫秒) + type: integer + example: 1714567910000 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, **kwargs): + """ + 创建新的附件。 + + 业务流程: + 1. 使用 D3iDcmTaskAttachmentForm 验证表单数据完整性 + 2. 检查是否已存在相同关联ID和类型的附件(避免重复) + 3. 创建新附件对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的附件对象 + + :param kwargs: 附件参数字典 + :return: 新建附件对象 + :rtype: DcmTaskAttachment + :raises AssertionError: 当附件已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmTaskAttachmentForm(formdata=kwargs) + _form.validate_form() + + # 检查是否存在同关联ID和类型的附件(排除自身) + _attachment: cls = await cls.is_exist(_form.relation_id.data, _form.relation_type_id.data) + assert _attachment is None, "相同关联ID和类型的附件已存在,不能重复创建。" + + # 创建附件对象 + _attachment = cls().copy_from_dict(_form.data, skip_none=True).before_save() + await _attachment.async_save() + return _attachment + + @classmethod + async def delete(cls, attachment_id: Union[str, int]): + """ + 删除附件(软删除,设置 delete_flag=1)。 + + 注意:物理删除需谨慎,建议使用软删除机制。 + + 业务流程: + 1. 根据ID查找附件 + 2. 验证附件存在性 + 3. 设置删除标记和删除信息 + 4. 保存更新 + + :param attachment_id: 要删除的附件ID + :return: 更新后的附件对象 + :rtype: DcmTaskAttachment + :raises AssertionError: 当附件不存在时抛出 + """ + _attachment: cls = await cls.async_find_by_id(attachment_id) + assert _attachment, f"根据 ID {attachment_id} 未找到附件。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _attachment.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除任务附件(记录ID:{_attachment.rec_id},ID:{_attachment.id}).') + return _attachment + + @classmethod + async def modify(cls, attachment_id: Union[str, int], **kwargs): + """ + 修改已有附件信息。 + + 注意:不允许修改 media_id、media_path 等核心存储字段,仅允许修改 metadata(如显示顺序、公开标志、备注等)。 + + 业务流程: + 1. 将 attachment_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 D3iDcmTaskAttachmentForm 验证表单数据 + 4. 检查是否有其他附件使用了相同的关联ID和类型(排除自身) + 5. 查询原附件对象 + 6. 验证附件存在性 + 7. 更新允许字段并设置更新者 + 8. 保存到数据库 + 9. 返回更新后的附件对象 + + :param attachment_id: 要修改的附件ID + :param kwargs: 需要更新的字段 + :return: 修改后的附件对象 + :rtype: DcmTaskAttachment + :raises AssertionError: 当附件不存在或信息重复时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = DcmTaskAttachmentForm(formdata=kwargs) + _form.validate_form() + + # 检查是否与其他附件重复(排除自身) + _other = await cls.exist_other(attachment_id, _form.relation_id.data, _form.relation_type_id.data) + assert _other is None, "相同关联ID和类型的附件已存在,不能重复修改。" + + # 查询原附件 + _attachment: cls = await cls.async_find_by_id(attachment_id) + assert _attachment, f'查无此附件信息。' + + # 仅允许更新非核心存储字段 + allowed_fields = { + 'display_order', 'public_flag', 'unit_name', 'media_usage', + 'media_label_type_id', 'media_shot', 'media_property', + 'gen_thumb', 'can_delete', 'delete_reason', 'delete_flag', + 'act_def_name', 'human_name' + } + + update_data = {k: v for k, v in _form.data.items() if k in allowed_fields and v is not None} + _attachment.copy_from_dict(update_data, skip_none=True).before_save() + await _attachment.async_save() + return _attachment + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新附件(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含附件数据的 DataFrame,字段需与模型属性匹配(如 relation_id, relation_type_id 等) + :return: 成功创建的附件数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + attachments = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(attachments) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(attachments)} 条附件。") + return len(attachments) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有附件。 + + :param data_df: 包含附件数据的 DataFrame,必须包含 id 列 + :return: 成功更新的附件数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条附件。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmTaskAttachment.exists_relation(data_df) + # 保存到数据库 + _created_count = await DcmTaskAttachment.create_batch(_latest_df) + _updated_count = await DcmTaskAttachment.modify_batch(_exists_df) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task_extend_info.py b/models/dcm_task_extend_info.py new file mode 100644 index 0000000..34f40b3 --- /dev/null +++ b/models/dcm_task_extend_info.py @@ -0,0 +1,236 @@ +from typing import Optional, Callable +from paste.web.form import ModelForm +from paste.core.logging import echo_log +from wtforms import StringField, IntegerField,TextAreaField +from wtforms.validators import Length +from tornado_swagger.model import register_swagger_model +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTaskExtendedInfo +import pandas as pd +from sqlalchemy import select + + +class DcmTaskExtendedInfoForm(ModelForm): + """ + 更多信息表单验证类(完全映射 TD3iDcmTaskExtendedInfo 字段)。 + + 用于验证和处理数字城管-部门待办任务扩展信息数据。 + 字段完全映射数据库表 t_d3i_dcm_task_extend_info 的字段结构。 + """ + rec_id=IntegerField('记录ID') + subtype_id=StringField('子类型ID',validators=[Length(max=50,message='子类型ID长度不能超过50个字符')]) + content_range=StringField('内容范围',validators=[Length(max=255,message='内容范围长度不能超过255个字符')]) + control_type=StringField('控件类型',validators=[Length(max=50,message='控件类型长度不能超过50个字符')]) + data_type_id=StringField('数据类型ID',validators=[Length(max=50,message='数据类型ID长度不能超过50个字符')]) + display_name=StringField('显示名称',validators=[Length(max=100,message='显示名称长度不能超过100个字符')]) + field_id=StringField('字段ID',validators=[Length(max=50,message='字段ID长度不能超过50个字符')]) + field_value=StringField('字段值',validators=[Length(max=255,message='字段值长度不能超过255个字符')]) + list_content=TextAreaField('下拉框选项内容') + null_flag=StringField('是否可空标识(0:不可空,1:可空)',validators=[Length(max=20,message='标识长度不能超过20个字符')]) + subtype_field_name=StringField('子类型字段名称',validators=[Length(max=100,message='子类型字段名称长度不能超过100个字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskExtendedInfoBase(TD3iDcmTaskExtendedInfo, CommonModel): + """ + 扩展信息基础类(完全映射 TD3iDcmTaskExtendedInfo 字段)。 + + 封装所有与扩展信息相关的通用操作方法。 + """ + FieldMapping = { + 'rec_id': 'recID', + 'subtype_id': 'subtypeID', + 'content_range': 'contentRange', + 'control_type': 'controlType', + 'data_type_id': 'dataTypeID', + 'display_name': 'displayName', + 'field_id': 'fieldID', + 'field_value': 'fieldValue', + 'list_content': 'listContent', + 'null_flag': 'nullFlag', + 'subtype_field_name': 'subtypeFieldName' + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。仅根据 rec_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 raw_id(rec_id)列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录(已匹配数据库id) + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 rec_id(去重) + rec_ids = data_df[cls.rec_id.key].drop_duplicates().tolist() + if not rec_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库仅根据 rec_id 匹配 + _query = select(cls.id, cls.rec_id).where( + cls.rec_id.in_(rec_ids) + ) + exists_df = await cls.query_as_df(_query) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 rec_id -> 数据库id 的映射(单字段) + key_to_id_map = dict(zip(exists_df[cls.rec_id.key], exists_df[cls.id.key])) + + # 根据 rec_id 判断是否存在 + mask_exists = data_df.apply(lambda row: row[cls.rec_id.key] in key_to_id_map, axis=1) + + # 拆分存在/不存在的数据 + exists_df = data_df[mask_exists].copy() + # 通过 rec_id 匹配数据库主键 + exists_df[cls.id.key] = exists_df.apply(lambda row: key_to_id_map[row[cls.rec_id.key]], axis=1) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def fill_extend_info(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'extend_infos', + preprocessing: Optional[Callable] = None): + """ + 填充扩展信息数据到数据框架。 + + 用于在查询结果中添加关联的扩展信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`extend_info` + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 扩展信息数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_id.in_(_task_ids)) + _extend_info_df: pd.DataFrame = await cls.query_as_df(_query) + if not _extend_info_df.empty: + _extend_info_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _extend_info_df[cls.id.key] = _extend_info_df[cls.id.key].astype(str) + _extend_info_df[cls.dcm_task_id.key] = _extend_info_df[cls.dcm_task_id.key].astype(str) + # 设置索引 + _extend_info_df['index_id'] = _extend_info_df[cls.dcm_task_id.key] + _extend_info_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _extend_info_df = preprocessing(_extend_info_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _extend_info_df.query(f"{cls.dcm_task_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + return _extend_info_df + + +@register_swagger_model +class DcmTaskExtendedInfo(DcmTaskExtendedInfoBase): + """ + 扩展信息模型类(主业务类,完全继承 TD3iDcmTaskExtendedInfo 字段)。 + """ + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新扩展信息(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含扩展信息数据的 DataFrame,字段需与模型属性匹配 + :return: 成功创建的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + records = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条任务扩展信息。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有扩展信息。 + + :param data_df: 包含扩展信息数据的 DataFrame + :return: 成功更新的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条任务扩展信息。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmTaskExtendedInfo.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmTaskExtendedInfo.create_batch(_latest_df) + _updated_count = await DcmTaskExtendedInfo.modify_batch(_exists_df) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task_file_upload.py b/models/dcm_task_file_upload.py new file mode 100644 index 0000000..0901179 --- /dev/null +++ b/models/dcm_task_file_upload.py @@ -0,0 +1,462 @@ +import random +from typing import Union, Optional, Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTaskFileUpload +from paste.core.logging import echo_log +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmTaskFileUploadForm(ModelForm): + """ + 文件上传关联表单验证类(完全映射 TD3iDcmTaskFileUpload 字段)。 + + 用于验证和处理文件上传关联数据的表单。 + 字段完全映射数据库表 t_d3i_dcm_task_file_upload 的字段结构。 + """ + + # 关联信息 + dcm_task_id = IntegerField('任务ID', validators=[Length(min=1, message='任务ID不能为空')]) + dcm_task_attachment_id = IntegerField('附件ID', validators=[Length(min=1, message='附件ID不能为空')]) + dcm_media_id = IntegerField('数字城管附件ID', validators=[Length(min=1, message='数字城管附件ID不能为空')]) + oa_media_id = IntegerField('OA附件ID', validators=[Length(min=1, message='OA附件ID不能为空')]) + + # 文件信息 + file_hash = StringField('文件哈希值', validators=[Length(max=256, message='文件哈希值长度不能超过256字符')]) + + # 状态与时间 + status = IntegerField('上传状态', validators=[Length(min=0, max=1, message='状态只能是0或1')]) + created_at = IntegerField('创建时间戳(毫秒)', validators=[Length(min=1, message='创建时间不能为空')]) + created_by = StringField('创建者', validators=[Length(max=64, message='创建者长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskFileUploadBase(TD3iDcmTaskFileUpload, CommonModel): + """ + 文件上传关联基础类(完全映射 TD3iDcmTaskFileUpload 字段)。 + + 封装所有与文件上传关联相关的通用操作方法。 + """ + + FieldMapping = { + 'dcm_task_id': 'dcmTaskId', + 'dcm_task_attachment_id': 'dcmTaskAttachmentId', + 'dcm_media_id': 'dcmMediaId', + 'oa_media_id': 'oaMediaId', + 'file_hash': 'fileHash', + 'status': 'status', + 'created_at': 'createdAt', + 'created_by': 'createdBy', + } + """ + 文件上传关联数据映射 + """ + + @classmethod + async def is_exist(cls, dcm_task_id: Union[str, int], dcm_task_attachment_id: Union[str, int]): + """ + 检查是否已存在相同任务ID和附件ID的记录。 + """ + _query = select(cls).where( + cls.dcm_task_id == dcm_task_id, cls.dcm_task_attachment_id == dcm_task_attachment_id + ) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找记录。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _records: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _records + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索文件上传记录的基础方法。 + + 支持字段: + - dcm_task_id, dcm_task_attachment_id, dcm_media_id, oa_media_id, status + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'created_at': 'asc'} + :key int dcm_task_id: 精确匹配任务ID + :key int dcm_task_attachment_id: 精确匹配附件ID + :key int dcm_media_id: 精确匹配数字城管附件ID + :key int oa_media_id: 精确匹配OA附件ID + :key int status: 精确匹配上传状态 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + _query = select(cls).where( + *cls.search_wheres(**kwargs) + ) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df[cls.id.key] = _df[cls.id.key].astype(str) + _df[cls.dcm_task_id.key] = _df[cls.dcm_task_id.key].astype(str) + _df[cls.dcm_task_attachment_id.key] = _df[cls.dcm_task_attachment_id.key].astype(str) + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + return _df, _paging + + @classmethod + async def search_by_attachment_ids(cls, attachment_ids: list[Union[str, int]]): + query = select( + cls.dcm_task_attachment_id, cls.oa_media_id, cls.updated_at + ).where( + cls.dcm_task_attachment_id.in_(attachment_ids) + ) + return await cls.query_as_df(query) + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索文件上传记录,返回分页格式数据。 + """ + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count if _paging else len(_df), + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number if _paging else 1, + 'page_count': _paging.page_count if _paging else 1, + 'page_size': _paging.page_size if _paging else 20, + }, + } + + @classmethod + async def exists_relation(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 dcm_task_id + dcm_task_attachment_id 判断。 + + :param data_df: 输入的数据框架,必须包含 dcm_task_id 和 dcm_task_attachment_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + pairs = data_df[[cls.dcm_task_id.key, cls.dcm_task_attachment_id.key]].drop_duplicates().values.tolist() + if not pairs: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.dcm_task_id, cls.dcm_task_attachment_id).where( + (cls.dcm_task_id.in_([p[0] for p in pairs])) & + (cls.dcm_task_attachment_id.in_([p[1] for p in pairs])) + ) + exists_df = await cls.query_as_df(_query) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + key_to_id_map = dict(zip(zip(exists_df[cls.dcm_task_id.key], exists_df[cls.dcm_task_attachment_id.key]), exists_df[cls.id.key])) + + mask_exists = data_df.apply(lambda row: (row[cls.dcm_task_id.key], row[cls.dcm_task_attachment_id.key]) in key_to_id_map, axis=1) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df.apply(lambda row: key_to_id_map[(row[cls.dcm_task_id.key], row[cls.dcm_task_attachment_id.key])], axis=1) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def fill_file_upload(cls, data_df: pd.DataFrame, index_field: str = 'dcm_task_attachment_id', + column_name: str = 'file_uploads', + preprocessing: Optional[Callable] = None): + """ + 填充文件上传数据到数据框架。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时新增列名称,默认为`file_uploads` + :param preprocessing: 预处理函数 + :return: 填充后的数据框架 + """ + if data_df.empty: + return pd.DataFrame() + + _task_attachment_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_attachment_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_attachment_id.in_(_task_attachment_ids)) + _upload_df: pd.DataFrame = await cls.query_as_df(_query) + if not _upload_df.empty: + _upload_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + _upload_df[cls.id.key] = _upload_df[cls.id.key].astype(str) + _upload_df[cls.dcm_task_id.key] = _upload_df[cls.dcm_task_id.key].astype(str) + _upload_df[cls.dcm_task_attachment_id.key] = _upload_df[cls.dcm_task_attachment_id.key].astype(str) + + _upload_df['index_id'] = _upload_df[cls.id.key] + _upload_df.set_index(['index_id'], inplace=True) + + if isinstance(preprocessing, Callable): + _upload_df = preprocessing(_upload_df) + + data_df[column_name] = data_df[index_field].apply( + lambda x: next(iter(_upload_df.query(f"{cls.dcm_task_attachment_id.key}=='{x}'").to_dict('records')), {}) + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + + return _upload_df + + +@register_swagger_model +class DcmTaskFileUpload(DcmTaskFileUploadBase): + """ + 文件上传关联业务模型类(主业务类,完全继承 TD3iDcmTaskFileUpload 字段)。 + + --- + description: 文件上传关联表 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + dcm_task_id: + description: 任务唯一标志 + type: integer + example: 2001 + dcm_task_attachment_id: + description: 附件ID(数字城管附件) + type: integer + example: 3001 + dcm_media_id: + description: 附件ID(数字城管媒体) + type: integer + example: 3002 + oa_media_id: + description: 附件ID(OA媒体) + type: integer + example: 3003 + file_hash: + description: 文件哈希值 + type: string + example: "a1b2c3d4e5..." + maxLength: 256 + status: + description: 上传状态(0:未上传/失败,1:成功) + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "D3I" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, **kwargs): + """ + 创建新的文件上传关联记录。 + + 业务流程: + 1. 使用 DcmTaskFileUploadForm 验证表单数据 + 2. 检查是否已存在相同任务ID和附件ID的记录 + 3. 创建新对象,设置创建者 + 4. 保存到数据库 + 5. 返回创建对象 + + :param kwargs: 附件参数字典 + :return: 新建的文件上传记录 + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmTaskFileUploadForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在 + _exist = await cls.is_exist(_form.dcm_task_id.data, _form.dcm_task_attachment_id.data) + assert _exist is None, "相同任务ID和附件ID的文件上传记录已存在,不能重复创建。" + + _record = cls().copy_from_dict(_form.data, skip_none=True).before_save() + await _record.async_save() + return _record + + @classmethod + async def delete(cls, id: Union[str, int]): + """ + 软删除(不推荐物理删除,但可设 status=-1 或删除记录) + + 本系统暂不支持软删除,建议直接物理删除。 + """ + _record: cls = await cls.async_find_by_id(id) + assert _record, f"根据 ID {id} 未找到文件上传记录。" + + _del_query = delete(cls).where(cls.id == _record.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除文件上传记录(ID:{_record.id}).') + return _record + + @classmethod + async def modify(cls, id: Union[str, int], **kwargs): + """ + 修改文件上传记录。 + + 注意:仅允许修改 status、created_by 等非核心关联字段。 + 核心字段(dcm_task_id, dcm_task_attachment_id, dcm_media_id, oa_media_id)不允许修改。 + + :param id: 记录ID + :param kwargs: 更新字段 + :return: 更新后的记录 + :raises AssertionError: 当记录不存在或字段被非法修改时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmTaskFileUploadForm(formdata=kwargs) + _form.validate_form() + + # 核心字段禁止修改 + protected_fields = {'dcm_task_id', 'dcm_task_attachment_id', 'dcm_media_id', 'oa_media_id'} + disallowed_fields = set(kwargs.keys()) & protected_fields + assert not disallowed_fields, f"禁止修改核心字段: {disallowed_fields}" + + _record: cls = await cls.async_find_by_id(id) + assert _record, f'查无此文件上传记录(ID:{id})。' + + # 允许更新的字段 + allowed_fields = {'status', 'created_by'} + update_data = {k: v for k, v in _form.data.items() if k in allowed_fields and v is not None} + + _record.copy_from_dict(update_data, skip_none=True).before_save() + await _record.async_save() + return _record + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建文件上传记录(传入数据应为全新记录)。 + + :param data_df: 包含字段 dcm_task_id, dcm_task_attachment_id, dcm_media_id, oa_media_id, file_hash, status, created_at, created_by 的 DataFrame + :return: 成功创建的数量 + """ + if data_df.empty: + return 0 + + records = data_df.to_dict('records') + records = [cls().copy_from_dict(r, skip_none=True).before_save() for r in records] + + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条文件上传记录。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改文件上传记录。 + + :param data_df: 必须包含 id 列的 DataFrame + :return: 成功更新的数量 + """ + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log("错误:modify_batch 要求输入数据必须包含 'id' 列(主键)") + return 0 + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条文件上传记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架,必须包含 dcm_task_id 和 dcm_task_attachment_id + :return: (新建数量, 更新数量) + """ + _exists_df, _latest_df = await DcmTaskFileUpload.exists_relation(data_df) + + _created_count = await DcmTaskFileUpload.create_batch(_latest_df) + _updated_count = await DcmTaskFileUpload.modify_batch(_exists_df) + + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task_form_datum.py b/models/dcm_task_form_datum.py new file mode 100644 index 0000000..be09b25 --- /dev/null +++ b/models/dcm_task_form_datum.py @@ -0,0 +1,2420 @@ +import random +from typing import Union, Optional, Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, FloatField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTaskFormDatum +from paste.core.logging import echo_log +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmTaskFormDatumForm(ModelForm): + """ + 企业待办表单数据验证类(完全映射 TD3iDcmTaskFormDatum 字段)。 + + 用于验证和处理数字化城市管理信息系统中企业待办的表单数据。 + 字段完全对应数据库表 t_d3i_dcm_task_form_data 的结构。 + """ + + # 基础信息 + id = IntegerField('主键ID') + rec_id = IntegerField('记录ID') + rec_disp_num = StringField('显示编号', validators=[Length(max=50, message='显示编号长度不能超过50字符')]) + rec_type_id = IntegerField('类型ID') + rec_type_name = StringField('案件类型', validators=[Length(max=100, message='案件类型长度不能超过100字符')]) + + # 任务信息 + task_num = StringField('任务号', validators=[Length(max=50, message='任务号长度不能超过50字符')]) + other_task_num = StringField('第三方任务号', validators=[Length(max=100, message='第三方任务号长度不能超过100字符')]) + act_property_id = IntegerField('任务属性ID') + + # 业务信息 + biz_id = IntegerField('业务ID') + biz_name = StringField('业务名称', validators=[Length(max=200, message='业务名称长度不能超过200字符')]) + sys_id = IntegerField('系统ID') + + # 地址与坐标 + address = TextAreaField('地址描述', validators=[Length(max=65535, message='地址描述长度不能超过65535字符')]) + district_name = StringField('所属区域', validators=[Length(max=50, message='所属区域长度不能超过50字符')]) + coordinate_x = FloatField('经度') + coordinate_y = FloatField('纬度') + lonlat_x = FloatField('经纬度X') + lonlat_y = FloatField('经纬度Y') + + # 事件信息 + event_type_id = IntegerField('问题类型ID') + event_type_name = StringField('问题类型', validators=[Length(max=100, message='问题类型长度不能超过100字符')]) + event_src_id = IntegerField('问题来源ID') + event_src_name = StringField('问题来源', validators=[Length(max=100, message='问题来源长度不能超过100字符')]) + event_desc = TextAreaField('问题描述', validators=[Length(max=65535, message='问题描述长度不能超过65535字符')]) + max_event_type_id = IntegerField('最大事件类型ID') + max_event_type_name = StringField('最大事件类型名称', validators=[Length(max=200, message='最大事件类型名称长度不能超过200字符')]) + + # 分类信息 + main_type_id = IntegerField('大类ID') + main_type_name = StringField('大类名称', validators=[Length(max=100, message='大类名称长度不能超过100字符')]) + sub_type_id = IntegerField('小类ID') + sub_type_name = StringField('小类名称', validators=[Length(max=100, message='小类名称长度不能超过100字符')]) + third_type_id = IntegerField('第三级类型ID') + third_type_name = StringField('第三级类型名称', validators=[Length(max=100, message='第三级类型名称长度不能超过100字符')]) + forth_type_id = IntegerField('第四级类型ID') + forth_type_name = StringField('第四级类型名称', validators=[Length(max=100, message='第四级类型名称长度不能超过100字符')]) + fifth_type_id = IntegerField('第五级类型ID') + fifth_type_name = StringField('第五级类型名称', validators=[Length(max=100, message='第五级类型名称长度不能超过100字符')]) + sixth_type_id = IntegerField('第六级类型ID') + sixth_type_name = StringField('第六级类型名称', validators=[Length(max=100, message='第六级类型名称长度不能超过100字符')]) + seventh_type_id = IntegerField('第七级类型ID') + seventh_type_name = StringField('第七级类型名称', validators=[Length(max=100, message='第七级类型名称长度不能超过100字符')]) + + # 时间与状态 + create_time = IntegerField('创建时间戳') + update_time = IntegerField('更新时间戳') + deadline_time = IntegerField('处理截止时间戳') + warning_time = IntegerField('处理预警时间戳') + occur_time = IntegerField('发生时间戳') + dispatch_time = IntegerField('派遣时间戳') + archive_time = IntegerField('归档时间戳') + cancel_time = IntegerField('取消时间戳') + refresh_time = IntegerField('刷新时间戳') + refresh_start_time = IntegerField('刷新开始时间戳') + check_send_time = IntegerField('核查发送时间戳') + check_reply_time = IntegerField('核查回复时间戳') + func_deadline = IntegerField('职能部门截止时间戳') + func_deal_time = IntegerField('职能部门处理时间戳') + proc_start_time = IntegerField('处理开始时间戳') + custom_deadline = IntegerField('自定义截止时间戳') + patroltask_deadline_time = IntegerField('巡查任务截止时间戳') + + # 时限描述 + deadline_char = StringField('时限描述', validators=[Length(max=50, message='时限描述长度不能超过50字符')]) + func_limit_char = StringField('职能部门时限描述', validators=[Length(max=50, message='职能部门时限描述长度不能超过50字符')]) + rec_remain_char = StringField('记录剩余时间描述', validators=[Length(max=50, message='记录剩余时间描述长度不能超过50字符')]) + rec_used_char = StringField('记录已用时间描述', validators=[Length(max=50, message='记录已用时间描述长度不能超过50字符')]) + + # 数值型字段 + rec_remain = FloatField('记录剩余时间') + rec_used = FloatField('记录已用时间') + rec_warning = FloatField('记录预警时间') + rec_deadline = FloatField('记录时限') + + # 部门与网格 + func_part_id = IntegerField('职能部门ID') + func_part_name = StringField('职能部门名称', validators=[Length(max=200, message='职能部门名称长度不能超过200字符')]) + func_part_list_id = StringField('职能部门列表ID', validators=[Length(max=100, message='职能部门列表ID长度不能超过100字符')]) + func_part_list_name = StringField('职能部门列表名称', validators=[Length(max=200, message='职能部门列表名称长度不能超过200字符')]) + specify_func_id = IntegerField('指定职能部门ID') + specify_func_name = StringField('指定职能部门名称', validators=[Length(max=200, message='指定职能部门名称长度不能超过200字符')]) + specify_competent_func_id = IntegerField('指定主管职能部门ID') + specify_competent_func_name = StringField('指定主管职能部门名称', validators=[Length(max=200, message='指定主管职能部门名称长度不能超过200字符')]) + first_depart_name = StringField('一级专业部门', validators=[Length(max=100, message='一级专业部门长度不能超过100字符')]) + second_depart_name = StringField('二级专业部门', validators=[Length(max=100, message='二级专业部门长度不能超过100字符')]) + + # 地理信息 + district_id = IntegerField('区域ID') + street_id = IntegerField('街道ID') + street_name = StringField('街道名称', validators=[Length(max=200, message='街道名称长度不能超过200字符')]) + community_id = IntegerField('社区ID') + community_name = StringField('社区名称', validators=[Length(max=200, message='社区名称长度不能超过200字符')]) + duty_grid_id = IntegerField('责任网格ID') + duty_grid_name = StringField('责任网格名称', validators=[Length(max=200, message='责任网格名称长度不能超过200字符')]) + duty_region_id = IntegerField('责任区域ID') + duty_region_name = StringField('责任区域名称', validators=[Length(max=200, message='责任区域名称长度不能超过200字符')]) + duty_district_id = IntegerField('责任区域ID') + duty_district_name = StringField('责任区域名称', validators=[Length(max=200, message='责任区域名称长度不能超过200字符')]) + duty_street_id = IntegerField('责任街道ID') + duty_street_name = StringField('责任街道名称', validators=[Length(max=200, message='责任街道名称长度不能超过200字符')]) + duty_community_id = IntegerField('责任社区ID') + duty_community_name = StringField('责任社区名称', validators=[Length(max=200, message='责任社区名称长度不能超过200字符')]) + law_duty_grid_id = IntegerField('法律责任网格ID') + law_duty_grid_name = StringField('法律责任网格名称', validators=[Length(max=200, message='法律责任网格名称长度不能超过200字符')]) + deal_duty_grid_id = IntegerField('处置责任网格ID') + deal_duty_grid_name = StringField('处置责任网格名称', validators=[Length(max=200, message='处置责任网格名称长度不能超过200字符')]) + + # 人员信息 + patrol_id = IntegerField('巡查员ID') + patrol_name = StringField('巡查员名称', validators=[Length(max=200, message='巡查员名称长度不能超过200字符')]) + accepter_id = IntegerField('受理人ID') + accepter_name = StringField('受理人姓名', validators=[Length(max=100, message='受理人姓名长度不能超过100字符')]) + human_id = IntegerField('操作人ID') + human_name = StringField('操作人名称', validators=[Length(max=255, message='操作人名称长度不能超过255字符')]) + reporter_name = StringField('举报人姓名', validators=[Length(max=100, message='举报人姓名长度不能超过100字符')]) + reporter_contact = StringField('举报电话', validators=[Length(max=50, message='举报电话长度不能超过50字符')]) + tell_num = StringField('联系电话', validators=[Length(max=50, message='联系电话长度不能超过50字符')]) + + # 状态与标识 + read_flag = IntegerField('是否已读(0未读,1已读)') + reply_intime = IntegerField('是否两小时回复(0无需回复,1待回复,2已回复,3超时,4无需回复已恢复)') + return_visit_flag = IntegerField('回访标识(0无需,1待回访,2已回访)') + urgency_level = IntegerField('紧急程度(0正常,1紧急)') + urgent_flag = IntegerField('紧急标识') + func_forbid_reporter_info_flag = IntegerField('是否禁止举报人信息') + public_flag = IntegerField('公开标志') + locked_flag = IntegerField('锁定标识') + transited_flag = IntegerField('转交标识') + split_rec_flag = IntegerField('拆分记录标识') + enable_check_msg = IntegerField('启用核查消息') + no_return_visit_flag = IntegerField('无需回访标识') + common_rec_type_flag = StringField('通用记录类型标识', validators=[Length(max=50, message='通用记录类型标识长度不能超过50字符')]) + common_rec_attr_flag = StringField('通用记录属性标识', validators=[Length(max=50, message='通用记录属性标识长度不能超过50字符')]) + send_pub_check_task_flag = IntegerField('发送公共核查任务标识') + reply_flag = StringField('回复标识', validators=[Length(max=50, message='回复标识长度不能超过50字符')]) + whistle_flag = StringField('吹哨标识', validators=[Length(max=50, message='吹哨标识长度不能超过50字符')]) + repeat_state = StringField('重复状态', validators=[Length(max=50, message='重复状态长度不能超过50字符')]) + report_state = StringField('上报状态', validators=[Length(max=50, message='上报状态长度不能超过50字符')]) + dispose_state = IntegerField('处置状态') + pre_dispose_state = StringField('预处置状态', validators=[Length(max=50, message='预处置状态长度不能超过50字符')]) + undertake_user_name = StringField('承办人员', validators=[Length(max=50, message='承办人员长度不能超过50字符')]) + undertake_phone = StringField('联系电话', validators=[Length(max=50, message='联系电话长度不能超过50字符')]) + deal_person_org = StringField('承办部门', validators=[Length(max=50, message='承办部门长度不能超过50字符')]) + + # 媒体信息 + media_upload_num = IntegerField('媒体上传数量') + media_upload_total_num = IntegerField('媒体上传总数') + media_upload_state = StringField('媒体上传状态', validators=[Length(max=50, message='媒体上传状态长度不能超过50字符')]) + media_check_num = IntegerField('媒体核查数量') + media_check_total_num = IntegerField('媒体核查总数') + media_verify_num = IntegerField('媒体核实数量') + media_verify_total_num = IntegerField('媒体核实总数') + media_self_deal_num = IntegerField('自行处置媒体数量') + media_self_deal_total_num = IntegerField('自行处置媒体总数') + media_review_num = IntegerField('复核媒体数量') + media_review_total_num = IntegerField('复核媒体总数') + report_pic_num = IntegerField('上报图片数量') + report_pic_total_num = IntegerField('上报图片总数') + report_video_num = IntegerField('上报视频数量') + report_video_total_num = IntegerField('上报视频总数') + report_wav_num = IntegerField('上报音频数量') + report_wav_total_num = IntegerField('上报音频总数') + check_pic_num = IntegerField('核查图片数量') + check_pic_total_num = IntegerField('核查图片总数') + check_video_num = IntegerField('核查视频数量') + check_video_total_num = IntegerField('核查视频总数') + check_wav_num = IntegerField('核查音频数量') + check_wav_total_num = IntegerField('核查音频总数') + verify_pic_num = IntegerField('核实图片数量') + verify_pic_total_num = IntegerField('核实图片总数') + verify_video_num = IntegerField('核实视频数量') + verify_video_total_num = IntegerField('核实视频总数') + verify_wav_num = IntegerField('核实音频数量') + verify_wav_total_num = IntegerField('核实音频总数') + self_deal_pic_num = IntegerField('自行处置图片数量') + self_deal_pic_total_num = IntegerField('自行处置图片总数') + self_deal_video_num = IntegerField('自行处置视频数量') + self_deal_video_total_num = IntegerField('自行处置视频总数') + self_deal_wav_num = IntegerField('自行处置音频数量') + self_deal_wav_total_num = IntegerField('自行处置音频总数') + review_pic_num = IntegerField('复核图片数量') + review_video_total_num = IntegerField('复核视频总数') + review_wav_num = IntegerField('复核音频数量') + review_wav_total_num = IntegerField('复核音频总数') + + # 媒体路径与属性 + media_url = StringField('内部访问URL', validators=[Length(max=512, message='内部访问URL长度不能超过512字符')]) + mms_pic_path = StringField('彩信图片路径', validators=[Length(max=500, message='彩信图片路径长度不能超过500字符')]) + media_path = StringField('服务器存储路径', validators=[Length(max=512, message='服务器存储路径长度不能超过512字符')]) + media_type = StringField('媒体类型', validators=[Length(max=50, message='媒体类型长度不能超过50字符')]) + media_usage = StringField('使用场景', validators=[Length(max=100, message='使用场景长度不能超过100字符')]) + media_server_name = StringField('媒体服务器名称', validators=[Length(max=100, message='媒体服务器名称长度不能超过100字符')]) + media_property = IntegerField('媒体属性') + media_uploaded_name = StringField('上传时的原始文件名', validators=[Length(max=255, message='上传时的原始文件名长度不能超过255字符')]) + media_shot = StringField('截图标识或路径', validators=[Length(max=255, message='截图标识或路径长度不能超过255字符')]) + media_label_type_id = IntegerField('标签类型ID') + media_default_url = StringField('外部可访问URL', validators=[Length(max=512, message='外部可访问URL长度不能超过512字符')]) + display_order = IntegerField('显示顺序') + store_type_id = IntegerField('存储类型ID') + special_item_image_type = StringField('特殊图片类型', validators=[Length(max=100, message='特殊图片类型长度不能超过100字符')]) + height = IntegerField('图片高度') + width = IntegerField('图片宽度') + send_flag = IntegerField('发送标志') + public_flag = IntegerField('公开标志') + gen_thumb = IntegerField('是否生成缩略图') + can_delete = IntegerField('是否可删除') + delete_flag = IntegerField('删除标记') + delete_reason = TextAreaField('删除原因', validators=[Length(max=65535, message='删除原因长度不能超过65535字符')]) + + # 地理与位置 + pos_type = StringField('位置类型', validators=[Length(max=50, message='位置类型长度不能超过50字符')]) + view_angle = StringField('视角', validators=[Length(max=100, message='视角长度不能超过100字符')]) + view_image_name = StringField('视图图片名称', validators=[Length(max=200, message='视图图片名称长度不能超过200字符')]) + view_image_x = FloatField('视图图片X坐标') + view_image_y = FloatField('视图图片Y坐标') + view_pos_x = FloatField('视图位置X坐标') + view_pos_y = FloatField('视图位置Y坐标') + + # 附件与标识 + attach_rec_flag = StringField('附件记录标识', validators=[Length(max=50, message='附件记录标识长度不能超过50字符')]) + gather_flag = StringField('汇总标识', validators=[Length(max=50, message='汇总标识长度不能超过50字符')]) + link_field_value = StringField('关联字段值', validators=[Length(max=500, message='关联字段值长度不能超过500字符')]) + link_field_display_value = StringField('关联字段显示值', validators=[Length(max=500, message='关联字段显示值长度不能超过500字符')]) + unique_id = StringField('唯一标识', validators=[Length(max=100, message='唯一标识长度不能超过100字符')]) + third_unique_id = StringField('第三方唯一标识', validators=[Length(max=100, message='第三方唯一标识长度不能超过100字符')]) + equal_group_id = IntegerField('等值组ID') + rec_category_id = IntegerField('记录类别ID') + + # 处置与审核 + dispatch_opinion = StringField('派遣意见', validators=[Length(max=500, message='派遣意见长度不能超过500字符')]) + revise_opinion = StringField('修订意见', validators=[Length(max=500, message='修订意见长度不能超过500字符')]) + reply_opinion = StringField('回复意见', validators=[Length(max=500, message='回复意见长度不能超过500字符')]) + new_inst_advise = StringField('立案建议', validators=[Length(max=500, message='立案建议长度不能超过500字符')]) + new_inst_cond_id = IntegerField('立案条件ID') + new_inst_cond_name = StringField('立案条件', validators=[Length(max=200, message='立案条件长度不能超过200字符')]) + case_closure_condition = StringField('结案条件', validators=[Length(max=200, message='结案条件长度不能超过200字符')]) + + # 特殊字段 + event_marks = StringField('事件标记', validators=[Length(max=500, message='事件标记长度不能超过500字符')]) + deduction = StringField('扣减', validators=[Length(max=100, message='扣减长度不能超过100字符')]) + event_property_id = IntegerField('事件属性ID') + event_property_name = StringField('事件属性名称', validators=[Length(max=200, message='事件属性名称长度不能超过200字符')]) + city_village_flag = StringField('城乡标识', validators=[Length(max=50, message='城乡标识长度不能超过50字符')]) + force_handle_flag = StringField('强制处理标识', validators=[Length(max=50, message='强制处理标识长度不能超过50字符')]) + auto_check_count = IntegerField('自动核查次数') + deal_evaluate_ids = StringField('处置评价ID列表', validators=[Length(max=200, message='处置评价ID列表长度不能超过200字符')]) + newinst_no_transit = StringField('立案不转交', validators=[Length(max=50, message='立案不转交长度不能超过50字符')]) + super_rec_id = IntegerField('上级记录ID') + site_num = StringField('站点编号', validators=[Length(max=50, message='站点编号长度不能超过50字符')]) + difficult_type_id = IntegerField('困难类型ID') + event_district_grade_id = IntegerField('事件区域等级ID') + event_district_grade_name = StringField('事件区域等级名称', validators=[Length(max=100, message='事件区域等级名称长度不能超过100字符')]) + cus_grid_code = StringField('自定义网格编码', validators=[Length(max=100, message='自定义网格编码长度不能超过100字符')]) + site_id = IntegerField('站点ID') + shop_id = IntegerField('商铺ID') + shop_name = StringField('商铺名称', validators=[Length(max=200, message='商铺名称长度不能超过200字符')]) + spec_type_id = IntegerField('特殊类型ID') + spec_type_name = StringField('特殊类型名称', validators=[Length(max=100, message='特殊类型名称长度不能超过100字符')]) + proc_account_state_id = IntegerField('处理账户状态ID') + check_type_id = IntegerField('核查类型ID') + rec_analysis_type_id = IntegerField('记录分析类型ID') + proc_time_state_id = IntegerField('处理流程状态ID') + proc_ard_state_id = IntegerField('处理仲裁状态ID') + proc_enq_state_id = IntegerField('处理询问状态ID') + proc_sup_state_id = IntegerField('处理监督状态ID') + func_time_state_id = IntegerField('职能部门时间状态ID') + check_msg_state_id = IntegerField('核查消息状态ID') + verify_msg_state_id = IntegerField('核实消息状态ID') + regather_msg_state_id = IntegerField('重新采集消息状态ID') + supervision_check_state_id = IntegerField('监督核查状态ID') + self_deal_msg_state_id = IntegerField('自行处置消息状态ID') + review_msg_state_id = IntegerField('复核消息状态ID') + proc_press_state_id = IntegerField('处理压力状态ID') + hot_area = StringField('热点区域', validators=[Length(max=100, message='热点区域长度不能超过100字符')]) + cg_area = StringField('城管区域', validators=[Length(max=100, message='城管区域长度不能超过100字符')]) + hw_area = StringField('环卫区域', validators=[Length(max=100, message='环卫区域长度不能超过100字符')]) + sz_area = StringField('市政区域', validators=[Length(max=100, message='市政区域长度不能超过100字符')]) + device_guid = StringField('设备GUID', validators=[Length(max=100, message='设备GUID长度不能超过100字符')]) + jx_id = IntegerField('警讯ID') + jx_jxmc = StringField('警讯名称', validators=[Length(max=200, message='警讯名称长度不能超过200字符')]) + jx_design_type = StringField('警讯设计类型', validators=[Length(max=100, message='警讯设计类型长度不能超过100字符')]) + report_time_segment_id = IntegerField('上报时段ID') + archive_cond_id = IntegerField('归档条件ID') + archive_cond = StringField('归档条件', validators=[Length(max=100, message='归档条件长度不能超过100字符')]) + archive_type_id = IntegerField('归档类型ID') + road_type_id = IntegerField('道路类型ID') + road_name = StringField('道路名称', validators=[Length(max=200, message='道路名称长度不能超过200字符')]) + road_id = IntegerField('道路ID') + road_type_name = StringField('道路类型名称', validators=[Length(max=100, message='道路类型名称长度不能超过100字符')]) + area_type_id = IntegerField('区域类型ID') + duty_grid_type_id = IntegerField('责任网格类型ID') + deal_duty_grid_type_id = IntegerField('处置责任网格类型ID') + time_area_id = IntegerField('时段ID') + time_area_name = StringField('时段名称', validators=[Length(max=100, message='时段名称长度不能超过100字符')]) + card_num = StringField('证件号码', validators=[Length(max=100, message='证件号码长度不能超过100字符')]) + cell_id = IntegerField('单元格ID') + cell_name = StringField('单元格名称', validators=[Length(max=200, message='单元格名称长度不能超过200字符')]) + damage_grade_id = IntegerField('损毁等级ID') + damage_grade_name = StringField('损毁等级名称', validators=[Length(max=100, message='损毁等级名称长度不能超过100字符')]) + event_grade_id = IntegerField('事件等级ID') + event_grade_name = StringField('事件等级名称', validators=[Length(max=100, message='事件等级名称长度不能超过100字符')]) + event_level_id = IntegerField('事件级别ID') + event_level_name = StringField('事件级别名称', validators=[Length(max=100, message='事件级别名称长度不能超过100字符')]) + event_district_id = IntegerField('事件区域ID') + event_district_name = StringField('事件区域名称', validators=[Length(max=100, message='事件区域名称长度不能超过100字符')]) + display_property = StringField('显示属性', validators=[Length(max=200, message='显示属性长度不能超过200字符')]) + display_style_id = IntegerField('显示样式ID') + refresh_flag = IntegerField('刷新标识') + video_device_id = IntegerField('视频设备ID') + video_param = StringField('视频参数', validators=[Length(max=500, message='视频参数长度不能超过500字符')]) + video_device_id = IntegerField('视频设备ID') + video_param = StringField('视频参数', validators=[Length(max=500, message='视频参数长度不能超过500字符')]) + patrol_deal_flag = IntegerField('巡查处置标识') + send_from_type = StringField('发送来源类型', validators=[Length(max=50, message='发送来源类型长度不能超过50字符')]) + reply_intime_deadline = IntegerField('两小时回复截止时间戳') + accept_status = StringField('受理状态', validators=[Length(max=50, message='受理状态长度不能超过50字符')]) + squadron_id = IntegerField('中队ID') + squadron_name = StringField('中队名称', validators=[Length(max=200, message='中队名称长度不能超过200字符')]) + property_company_id = IntegerField('物业公司ID') + act_record_id = IntegerField('操作记录ID') + main_rec_id = IntegerField('主记录ID') + force_handle_flag = StringField('强制处理标识', validators=[Length(max=50, message='强制处理标识长度不能超过50字符')]) + func_custom_limit = StringField('职能部门自定义时限', validators=[Length(max=50, message='职能部门自定义时限长度不能超过50字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskFormDatumBase(TD3iDcmTaskFormDatum, CommonModel): + """ + 企业待办表单数据基础类(完全映射 TD3iDcmTaskFormDatum 字段)。 + + 封装所有与企业待办表单数据相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'rec_id': 'rec_id', + 'rec_disp_num': 'rec_disp_num', + 'rec_type_id': 'rec_type_id', + 'rec_type_name': 'rec_type_name', + 'task_num': 'task_num', + 'other_task_num': 'other_task_num', + 'act_property_id': 'act_property_id', + 'biz_id': 'biz_id', + 'biz_name': 'biz_name', + 'sys_id': 'sys_id', + 'address': 'address', + 'district_name': 'district_name', + 'coordinate_x': 'coordinate_x', + 'coordinate_y': 'coordinate_y', + 'lonlat_x': 'lonlat_x', + 'lonlat_y': 'lonlat_y', + 'event_type_id': 'event_type_id', + 'event_type_name': 'event_type_name', + 'event_src_id': 'event_src_id', + 'event_src_name': 'event_src_name', + 'event_desc': 'event_desc', + 'max_event_type_id': 'max_event_type_id', + 'max_event_type_name': 'max_event_type_name', + 'main_type_id': 'main_type_id', + 'main_type_name': 'main_type_name', + 'sub_type_id': 'sub_type_id', + 'sub_type_name': 'sub_type_name', + 'third_type_id': 'third_type_id', + 'third_type_name': 'third_type_name', + 'forth_type_id': 'forth_type_id', + 'forth_type_name': 'forth_type_name', + 'fifth_type_id': 'fifth_type_id', + 'fifth_type_name': 'fifth_type_name', + 'sixth_type_id': 'sixth_type_id', + 'sixth_type_name': 'sixth_type_name', + 'seventh_type_id': 'seventh_type_id', + 'seventh_type_name': 'seventh_type_name', + 'create_time': 'create_time', + 'update_time': 'update_time', + 'deadline_time': 'deadline_time', + 'warning_time': 'warning_time', + 'occur_time': 'occur_time', + 'dispatch_time': 'dispatch_time', + 'archive_time': 'archive_time', + 'cancel_time': 'cancel_time', + 'refresh_time': 'refresh_time', + 'refresh_start_time': 'refresh_start_time', + 'check_send_time': 'check_send_time', + 'check_reply_time': 'check_reply_time', + 'func_deadline': 'func_deadline', + 'func_deal_time': 'func_deal_time', + 'proc_start_time': 'proc_start_time', + 'custom_deadline': 'custom_deadline', + 'patroltask_deadline_time': 'patroltask_deadline_time', + 'deadline_char': 'deadline_char', + 'func_limit_char': 'func_limit_char', + 'rec_remain_char': 'rec_remain_char', + 'rec_used_char': 'rec_used_char', + 'rec_remain': 'rec_remain', + 'rec_used': 'rec_used', + 'rec_warning': 'rec_warning', + 'rec_deadline': 'rec_deadline', + 'func_part_id': 'func_part_id', + 'func_part_name': 'func_part_name', + 'func_part_list_id': 'func_part_list_id', + 'func_part_list_name': 'func_part_list_name', + 'specify_func_id': 'specify_func_id', + 'specify_func_name': 'specify_func_name', + 'specify_competent_func_id': 'specify_competent_func_id', + 'specify_competent_func_name': 'specify_competent_func_name', + 'first_depart_name': 'first_depart_name', + 'second_depart_name': 'second_depart_name', + 'district_id': 'district_id', + 'street_id': 'street_id', + 'street_name': 'street_name', + 'community_id': 'community_id', + 'community_name': 'community_name', + 'duty_grid_id': 'duty_grid_id', + 'duty_grid_name': 'duty_grid_name', + 'duty_region_id': 'duty_region_id', + 'duty_region_name': 'duty_region_name', + 'duty_district_id': 'duty_district_id', + 'duty_district_name': 'duty_district_name', + 'duty_street_id': 'duty_street_id', + 'duty_street_name': 'duty_street_name', + 'duty_community_id': 'duty_community_id', + 'duty_community_name': 'duty_community_name', + 'law_duty_grid_id': 'law_duty_grid_id', + 'law_duty_grid_name': 'law_duty_grid_name', + 'deal_duty_grid_id': 'deal_duty_grid_id', + 'deal_duty_grid_name': 'deal_duty_grid_name', + 'patrol_id': 'patrol_id', + 'patrol_name': 'patrol_name', + 'accepter_id': 'accepter_id', + 'accepter_name': 'accepter_name', + 'human_id': 'human_id', + 'human_name': 'human_name', + 'reporter_name': 'reporter_name', + 'reporter_contact': 'reporter_contact', + 'tell_num': 'tell_num', + 'read_flag': 'read_flag', + 'reply_intime': 'reply_intime', + 'return_visit_flag': 'return_visit_flag', + 'urgency_level': 'urgency_level', + 'urgent_flag': 'urgent_flag', + 'func_forbid_reporter_info_flag': 'func_forbid_reporter_info_flag', + 'public_flag': 'public_flag', + 'locked_flag': 'locked_flag', + 'transited_flag': 'transited_flag', + 'split_rec_flag': 'split_rec_flag', + 'enable_check_msg': 'enable_check_msg', + 'no_return_visit_flag': 'no_return_visit_flag', + 'common_rec_type_flag': 'common_rec_type_flag', + 'common_rec_attr_flag': 'common_rec_attr_flag', + 'send_pub_check_task_flag': 'send_pub_check_task_flag', + 'reply_flag': 'reply_flag', + 'whistle_flag': 'whistle_flag', + 'repeat_state': 'repeat_state', + 'report_state': 'report_state', + 'dispose_state': 'dispose_state', + 'pre_dispose_state': 'pre_dispose_state', + 'undertake_user_name': 'undertake_user_name', + 'undertake_phone': 'undertake_phone', + 'deal_person_org': 'deal_person_org', + 'media_upload_num': 'media_upload_num', + 'media_upload_total_num': 'media_upload_total_num', + 'media_upload_state': 'media_upload_state', + 'media_check_num': 'media_check_num', + 'media_check_total_num': 'media_check_total_num', + 'media_verify_num': 'media_verify_num', + 'media_verify_total_num': 'media_verify_total_num', + 'media_self_deal_num': 'media_self_deal_num', + 'media_self_deal_total_num': 'media_self_deal_total_num', + 'media_review_num': 'media_review_num', + 'media_review_total_num': 'media_review_total_num', + 'report_pic_num': 'report_pic_num', + 'report_pic_total_num': 'report_pic_total_num', + 'report_video_num': 'report_video_num', + 'report_video_total_num': 'report_video_total_num', + 'report_wav_num': 'report_wav_num', + 'report_wav_total_num': 'report_wav_total_num', + 'check_pic_num': 'check_pic_num', + 'check_pic_total_num': 'check_pic_total_num', + 'check_video_num': 'check_video_num', + 'check_video_total_num': 'check_video_total_num', + 'check_wav_num': 'check_wav_num', + 'check_wav_total_num': 'check_wav_total_num', + 'verify_pic_num': 'verify_pic_num', + 'verify_pic_total_num': 'verify_pic_total_num', + 'verify_video_num': 'verify_video_num', + 'verify_video_total_num': 'verify_video_total_num', + 'verify_wav_num': 'verify_wav_num', + 'verify_wav_total_num': 'verify_wav_total_num', + 'self_deal_pic_num': 'self_deal_pic_num', + 'self_deal_pic_total_num': 'self_deal_pic_total_num', + 'self_deal_video_num': 'self_deal_video_num', + 'self_deal_video_total_num': 'self_deal_video_total_num', + 'self_deal_wav_num': 'self_deal_wav_num', + 'self_deal_wav_total_num': 'self_deal_wav_total_num', + 'review_pic_num': 'review_pic_num', + 'review_pic_total_num': 'review_pic_total_num', + 'review_video_num': 'review_video_num', + 'review_video_total_num': 'review_video_total_num', + 'review_wav_num': 'review_wav_num', + 'review_wav_total_num': 'review_wav_total_num', + 'media_url': 'media_url', + 'mms_pic_path': 'mms_pic_path', + 'media_path': 'media_path', + 'media_type': 'media_type', + 'media_usage': 'media_usage', + 'media_server_name': 'media_server_name', + 'media_property': 'media_property', + 'media_uploaded_name': 'media_uploaded_name', + 'media_shot': 'media_shot', + 'media_label_type_id': 'media_label_type_id', + 'media_default_url': 'media_default_url', + 'display_order': 'display_order', + 'store_type_id': 'store_type_id', + 'special_item_image_type': 'special_item_image_type', + 'height': 'height', + 'width': 'width', + 'send_flag': 'send_flag', + 'public_flag': 'public_flag', + 'gen_thumb': 'gen_thumb', + 'can_delete': 'can_delete', + 'delete_flag': 'delete_flag', + 'delete_reason': 'delete_reason', + 'pos_type': 'pos_type', + 'view_angle': 'view_angle', + 'view_image_name': 'view_image_name', + 'view_image_x': 'view_image_x', + 'view_image_y': 'view_image_y', + 'view_pos_x': 'view_pos_x', + 'view_pos_y': 'view_pos_y', + 'attach_rec_flag': 'attach_rec_flag', + 'gather_flag': 'gather_flag', + 'link_field_value': 'link_field_value', + 'link_field_display_value': 'link_field_display_value', + 'unique_id': 'unique_id', + 'third_unique_id': 'third_unique_id', + 'equal_group_id': 'equal_group_id', + 'rec_category_id': 'rec_category_id', + 'dispatch_opinion': 'dispatch_opinion', + 'revise_opinion': 'revise_opinion', + 'reply_opinion': 'reply_opinion', + 'new_inst_advise': 'new_inst_advise', + 'new_inst_cond_id': 'new_inst_cond_id', + 'new_inst_cond_name': 'new_inst_cond_name', + 'case_closure_condition': 'case_closure_condition', + 'event_marks': 'event_marks', + 'deduction': 'deduction', + 'event_property_id': 'event_property_id', + 'event_property_name': 'event_property_name', + 'city_village_flag': 'city_village_flag', + 'force_handle_flag': 'force_handle_flag', + 'auto_check_count': 'auto_check_count', + 'deal_evaluate_ids': 'deal_evaluate_ids', + 'newinst_no_transit': 'newinst_no_transit', + 'super_rec_id': 'super_rec_id', + 'site_num': 'site_num', + 'difficult_type_id': 'difficult_type_id', + 'event_district_grade_id': 'event_district_grade_id', + 'event_district_grade_name': 'event_district_grade_name', + 'cus_grid_code': 'cus_grid_code', + 'site_id': 'site_id', + 'shop_id': 'shop_id', + 'shop_name': 'shop_name', + 'spec_type_id': 'spec_type_id', + 'spec_type_name': 'spec_type_name', + 'proc_account_state_id': 'proc_account_state_id', + 'check_type_id': 'check_type_id', + 'rec_analysis_type_id': 'rec_analysis_type_id', + 'proc_time_state_id': 'proc_time_state_id', + 'proc_ard_state_id': 'proc_ard_state_id', + 'proc_enq_state_id': 'proc_enq_state_id', + 'proc_sup_state_id': 'proc_sup_state_id', + 'func_time_state_id': 'func_time_state_id', + 'check_msg_state_id': 'check_msg_state_id', + 'verify_msg_state_id': 'verify_msg_state_id', + 'regather_msg_state_id': 'regather_msg_state_id', + 'supervision_check_state_id': 'supervision_check_state_id', + 'self_deal_msg_state_id': 'self_deal_msg_state_id', + 'review_msg_state_id': 'review_msg_state_id', + 'proc_press_state_id': 'proc_press_state_id', + 'hot_area': 'hot_area', + 'cg_area': 'cg_area', + 'hw_area': 'hw_area', + 'sz_area': 'sz_area', + 'device_guid': 'device_guid', + 'jx_id': 'jx_id', + 'jx_jxmc': 'jx_jxmc', + 'jx_design_type': 'jx_design_type', + 'report_time_segment_id': 'report_time_segment_id', + 'archive_cond_id': 'archive_cond_id', + 'archive_cond': 'archive_cond', + 'archive_type_id': 'archive_type_id', + 'road_type_id': 'road_type_id', + 'road_name': 'road_name', + 'road_id': 'road_id', + 'road_type_name': 'road_type_name', + 'area_type_id': 'area_type_id', + 'duty_grid_type_id': 'duty_grid_type_id', + 'deal_duty_grid_type_id': 'deal_duty_grid_type_id', + 'time_area_id': 'time_area_id', + 'time_area_name': 'time_area_name', + 'card_num': 'card_num', + 'cell_id': 'cell_id', + 'cell_name': 'cell_name', + 'damage_grade_id': 'damage_grade_id', + 'damage_grade_name': 'damage_grade_name', + 'event_grade_id': 'event_grade_id', + 'event_grade_name': 'event_grade_name', + 'event_level_id': 'event_level_id', + 'event_level_name': 'event_level_name', + 'event_district_id': 'event_district_id', + 'event_district_name': 'event_district_name', + 'display_property': 'display_property', + 'display_style_id': 'display_style_id', + 'refresh_flag': 'refresh_flag', + 'video_device_id': 'video_device_id', + 'video_param': 'video_param', + 'patrol_deal_flag': 'patrol_deal_flag', + 'send_from_type': 'send_from_type', + 'reply_intime_deadline': 'reply_intime_deadline', + 'accept_status': 'accept_status', + 'squadron_id': 'squadron_id', + 'squadron_name': 'squadron_name', + 'property_company_id': 'property_company_id', + 'act_record_id': 'act_record_id', + 'main_rec_id': 'main_rec_id', + 'func_custom_limit': 'func_custom_limit', + } + + @classmethod + async def exist_other(cls, id: Union[str, int], rec_id: Union[str, int]): + """ + 检查是否存在除当前任务外的其他同记录ID的任务。 + + :param id: 当前任务ID + :param rec_id: 记录ID + :return: 存在返回任务对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.rec_id == rec_id) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找任务数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _task_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _task_list + + @classmethod + async def is_exist(cls, rec_id: Union[str, int]): + """ + 检查任务是否已经存在(根据记录ID)。 + """ + _query = select(cls).where(cls.rec_id == rec_id) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索任务数据的基础方法。 + + 支持字段: + - task_num, rec_disp_num, event_type_name, district_name, urgency_level, read_flag 等 + - 支持模糊匹配:event_type_name, rec_type_name, event_src_name, first_depart_name, second_depart_name + - 支持精确匹配:biz_id, sys_id, urgency_level, read_flag, rec_type_id, deadline_time 等 + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_num': 'asc'} + :key str task_num: 精确匹配任务号 + :key str rec_disp_num: 精确匹配显示编号 + :key str event_type_name: 模糊匹配问题类型 + :key str district_name: 精确匹配区域 + :key int urgency_level: 精确匹配紧急程度 + :key int read_flag: 精确匹配是否已读 + :key int biz_id: 精确匹配业务ID + :key int sys_id: 精确匹配系统ID + :key int rec_type_id: 精确匹配类型ID + :key int deadline_time: 精确匹配处理截止时间戳 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.event_type_name.key: '%{}%', + cls.rec_type_name.key: '%{}%', + cls.event_src_name.key: '%{}%', + cls.first_depart_name.key: '%{}%', + cls.second_depart_name.key: '%{}%', + cls.district_name.key: '%{}%', + cls.street_name.key: '%{}%', + cls.community_name.key: '%{}%', + cls.func_part_name.key: '%{}%', + cls.specify_func_name.key: '%{}%', + cls.specify_competent_func_name.key: '%{}%', + cls.main_type_name.key: '%{}%', + cls.sub_type_name.key: '%{}%', + cls.third_type_name.key: '%{}%', + cls.forth_type_name.key: '%{}%', + cls.fifth_type_name.key: '%{}%', + cls.sixth_type_name.key: '%{}%', + cls.seventh_type_name.key: '%{}%', + cls.duty_grid_name.key: '%{}%', + cls.duty_region_name.key: '%{}%', + cls.duty_district_name.key: '%{}%', + cls.duty_street_name.key: '%{}%', + cls.duty_community_name.key: '%{}%', + cls.law_duty_grid_name.key: '%{}%', + cls.deal_duty_grid_name.key: '%{}%', + cls.patrol_name.key: '%{}%', + cls.accepter_name.key: '%{}%', + cls.human_name.key: '%{}%', + cls.reporter_name.key: '%{}%', + cls.shop_name.key: '%{}%', + cls.spec_type_name.key: '%{}%', + cls.squadron_name.key: '%{}%', + cls.road_name.key: '%{}%', + cls.time_area_name.key: '%{}%', + cls.hot_area.key: '%{}%', + cls.cg_area.key: '%{}%', + cls.hw_area.key: '%{}%', + cls.sz_area.key: '%{}%', + cls.event_district_grade_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_num, cls.rec_disp_num) + + _task_df = await cls.query_as_df(_data_query) + if not _task_df.empty: + _task_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _task_df[cls.id.key] = _task_df[cls.id.key].astype(str) + + return _task_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索任务数据,返回分页格式数据。 + """ + _task_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _task_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 rec_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 rec_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 rec_id 列表(去重) + rec_ids = data_df[cls.rec_id.key].unique().tolist() + if not rec_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 rec_id + _query = select(cls.id, cls.rec_id).where(cls.rec_id.in_(rec_ids)) + rec_ids_df = await cls.query_as_df(_query) + + if rec_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 rec_id -> id 的映射字典 + rec_id_to_id_map = dict(zip(rec_ids_df[cls.rec_id.key], rec_ids_df[cls.id.key])) + + # 根据 rec_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.rec_id.key].isin(rec_ids_df[cls.rec_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.rec_id.key].map(rec_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def fill_form_datum(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'datums', + preprocessing: Optional[Callable] = None): + """ + 填充详细数据到数据框架。 + + 用于在查询结果中添加关联的详细信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`datums` + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 详细数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_id.in_(_task_ids)) + _datum_df: pd.DataFrame = await cls.query_as_df(_query) + if not _datum_df.empty: + _datum_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _datum_df[cls.id.key] = _datum_df[cls.id.key].astype(str) + _datum_df[cls.dcm_task_id.key] = _datum_df[cls.dcm_task_id.key].astype(str) + + # 设置索引 + _datum_df['index_id'] = _datum_df[cls.id.key] + _datum_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _datum_df = preprocessing(_datum_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _datum_df.query(f"{cls.dcm_task_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + + return _datum_df + + +@register_swagger_model +class DcmTaskFormDatum(DcmTaskFormDatumBase): + """ + 企业待办表单数据主业务类(完全继承 TD3iDcmTaskFormDatum 字段)。 + + --- + description: 数字化城市管理信息系统企业待办表单数据 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + rec_id: + description: 记录ID + type: integer + example: 2001 + rec_disp_num: + description: 显示编号 + type: string + example: "D20240501001" + maxLength: 50 + rec_type_id: + description: 类型ID + type: integer + example: 101 + rec_type_name: + description: 案件类型 + type: string + example: "市容环境" + maxLength: 100 + task_num: + description: 任务号 + type: string + example: "TASK20240501001" + maxLength: 50 + other_task_num: + description: 第三方任务号 + type: string + example: "THIRD-2024-001" + maxLength: 100 + act_property_id: + description: 任务属性ID + type: integer + example: 5 + biz_id: + description: 业务ID + type: integer + example: 10 + biz_name: + description: 业务名称 + type: string + example: "市容巡查" + maxLength: 200 + sys_id: + description: 系统ID + type: integer + example: 1 + address: + description: 地址描述 + type: string + example: "中山路与解放路交叉口" + maxLength: 65535 + district_name: + description: 所属区域 + type: string + example: "鼓楼区" + maxLength: 50 + coordinate_x: + description: 经度 + type: number + format: decimal + example: 118.789012 + coordinate_y: + description: 纬度 + type: number + format: decimal + example: 32.045678 + lonlat_x: + description: 经纬度X + type: number + format: decimal + example: 118.789012 + lonlat_y: + description: 经纬度Y + type: number + format: decimal + example: 32.045678 + event_type_id: + description: 问题类型ID + type: integer + example: 1001 + event_type_name: + description: 问题类型 + type: string + example: "道路破损" + maxLength: 100 + event_src_id: + description: 问题来源ID + type: integer + example: 101 + event_src_name: + description: 问题来源 + type: string + example: "市民举报" + maxLength: 100 + event_desc: + description: 问题描述 + type: string + example: "中山路与解放路交叉口路面大面积破损" + maxLength: 65535 + max_event_type_id: + description: 最大事件类型ID + type: integer + example: 1002 + max_event_type_name: + description: 最大事件类型名称 + type: string + example: "市政设施" + maxLength: 200 + main_type_id: + description: 大类ID + type: integer + example: 101 + main_type_name: + description: 大类名称 + type: string + example: "市容环境" + maxLength: 100 + sub_type_id: + description: 小类ID + type: integer + example: 10101 + sub_type_name: + description: 小类名称 + type: string + example: "道路破损" + maxLength: 100 + third_type_id: + description: 第三级类型ID + type: integer + example: 1010101 + third_type_name: + description: 第三级类型名称 + type: string + example: "人行道破损" + maxLength: 100 + forth_type_id: + description: 第四级类型ID + type: integer + example: 101010101 + forth_type_name: + description: 第四级类型名称 + type: string + example: "沥青路面破损" + maxLength: 100 + fifth_type_id: + description: 第五级类型ID + type: integer + example: 10101010101 + fifth_type_name: + description: 第五级类型名称 + type: string + example: "裂缝" + maxLength: 100 + sixth_type_id: + description: 第六级类型ID + type: integer + example: 1010101010101 + sixth_type_name: + description: 第六级类型名称 + type: string + example: "横向裂缝" + maxLength: 100 + seventh_type_id: + description: 第七级类型ID + type: integer + example: 101010101010101 + seventh_type_name: + description: 第七级类型名称 + type: string + example: "细小横向裂缝" + maxLength: 100 + create_time: + description: 创建时间戳 + type: integer + example: 1714567890000 + update_time: + description: 更新时间戳 + type: integer + example: 1714578000000 + deadline_time: + description: 处理截止时间戳 + type: integer + example: 1714578000000 + warning_time: + description: 处理预警时间戳 + type: integer + example: 1714570000000 + occur_time: + description: 发生时间戳 + type: integer + example: 1714567800000 + dispatch_time: + description: 派遣时间戳 + type: integer + example: 1714568000000 + archive_time: + description: 归档时间戳 + type: integer + example: 1714580000000 + cancel_time: + description: 取消时间戳 + type: integer + example: 1714579000000 + refresh_time: + description: 刷新时间戳 + type: integer + example: 1714572000000 + refresh_start_time: + description: 刷新开始时间戳 + type: integer + example: 1714571000000 + check_send_time: + description: 核查发送时间戳 + type: integer + example: 1714570000000 + check_reply_time: + description: 核查回复时间戳 + type: integer + example: 1714571000000 + func_deadline: + description: 职能部门截止时间戳 + type: integer + example: 1714578000000 + func_deal_time: + description: 职能部门处理时间戳 + type: integer + example: 1714576000000 + proc_start_time: + description: 处理开始时间戳 + type: integer + example: 1714572000000 + custom_deadline: + description: 自定义截止时间戳 + type: integer + example: 1714579000000 + patroltask_deadline_time: + description: 巡查任务截止时间戳 + type: integer + example: 1714575000000 + deadline_char: + description: 时限描述 + type: string + example: "24小时" + maxLength: 50 + func_limit_char: + description: 职能部门时限描述 + type: string + example: "48小时" + maxLength: 50 + rec_remain_char: + description: 记录剩余时间描述 + type: string + example: "3天" + maxLength: 50 + rec_used_char: + description: 记录已用时间描述 + type: string + example: "1天" + maxLength: 50 + rec_remain: + description: 记录剩余时间 + type: number + format: decimal + example: 3.5 + rec_used: + description: 记录已用时间 + type: number + format: decimal + example: 1.2 + rec_warning: + description: 记录预警时间 + type: number + format: decimal + example: 0.5 + rec_deadline: + description: 记录时限 + type: number + format: decimal + example: 5.0 + func_part_id: + description: 职能部门ID + type: integer + example: 101 + func_part_name: + description: 职能部门名称 + type: string + example: "市政工程处" + maxLength: 200 + func_part_list_id: + description: 职能部门列表ID + type: string + example: "LIST-001" + maxLength: 100 + func_part_list_name: + description: 职能部门列表名称 + type: string + example: "市政处置组" + maxLength: 200 + specify_func_id: + description: 指定职能部门ID + type: integer + example: 102 + specify_func_name: + description: 指定职能部门名称 + type: string + example: "城市管理局" + maxLength: 200 + specify_competent_func_id: + description: 指定主管职能部门ID + type: integer + example: 103 + specify_competent_func_name: + description: 指定主管职能部门名称 + type: string + example: "市城管委" + maxLength: 200 + first_depart_name: + description: 一级专业部门 + type: string + example: "市政工程处" + maxLength: 100 + second_depart_name: + description: 二级专业部门 + type: string + example: "道路养护科" + maxLength: 100 + district_id: + description: 区域ID + type: integer + example: 1001 + street_id: + description: 街道ID + type: integer + example: 1002 + street_name: + description: 街道名称 + type: string + example: "中山路" + maxLength: 200 + community_id: + description: 社区ID + type: integer + example: 1003 + community_name: + description: 社区名称 + type: string + example: "鼓楼社区" + maxLength: 200 + duty_grid_id: + description: 责任网格ID + type: integer + example: 1004 + duty_grid_name: + description: 责任网格名称 + type: string + example: "鼓楼网格01" + maxLength: 200 + duty_region_id: + description: 责任区域ID + type: integer + example: 1005 + duty_region_name: + description: 责任区域名称 + type: string + example: "鼓楼区" + maxLength: 200 + duty_district_id: + description: 责任区域ID + type: integer + example: 1005 + duty_district_name: + description: 责任区域名称 + type: string + example: "鼓楼区" + maxLength: 200 + duty_street_id: + description: 责任街道ID + type: integer + example: 1006 + duty_street_name: + description: 责任街道名称 + type: string + example: "中山路" + maxLength: 200 + duty_community_id: + description: 责任社区ID + type: integer + example: 1007 + duty_community_name: + description: 责任社区名称 + type: string + example: "鼓楼社区" + maxLength: 200 + law_duty_grid_id: + description: 法律责任网格ID + type: integer + example: 1008 + law_duty_grid_name: + description: 法律责任网格名称 + type: string + example: "执法网格01" + maxLength: 200 + deal_duty_grid_id: + description: 处置责任网格ID + type: integer + example: 1009 + deal_duty_grid_name: + description: 处置责任网格名称 + type: string + example: "处置网格01" + maxLength: 200 + patrol_id: + description: 巡查员ID + type: integer + example: 2001 + patrol_name: + description: 巡查员名称 + type: string + example: "张三" + maxLength: 200 + accepter_id: + description: 受理人ID + type: integer + example: 2002 + accepter_name: + description: 受理人姓名 + type: string + example: "李四" + maxLength: 100 + human_id: + description: 操作人ID + type: integer + example: 2003 + human_name: + description: 操作人名称 + type: string + example: "王五" + maxLength: 255 + reporter_name: + description: 举报人姓名 + type: string + example: "张三" + maxLength: 100 + reporter_contact: + description: 举报电话 + type: string + example: "13800138000" + maxLength: 50 + tell_num: + description: 联系电话 + type: string + example: "13800138000" + maxLength: 50 + read_flag: + description: 是否已读(0未读,1已读) + type: integer + example: 1 + reply_intime: + description: 是否两小时回复(0无需回复,1待回复,2已回复,3超时,4无需回复已恢复) + type: integer + example: 2 + return_visit_flag: + description: 回访标识(0无需,1待回访,2已回访) + type: integer + example: 1 + urgency_level: + description: 紧急程度(0正常,1紧急) + type: integer + example: 1 + urgent_flag: + description: 紧急标识 + type: integer + example: 1 + func_forbid_reporter_info_flag: + description: 是否禁止举报人信息 + type: integer + example: 0 + public_flag: + description: 公开标志 + type: integer + example: 1 + locked_flag: + description: 锁定标识 + type: integer + example: 0 + transited_flag: + description: 转交标识 + type: integer + example: 1 + split_rec_flag: + description: 拆分记录标识 + type: integer + example: 0 + enable_check_msg: + description: 启用核查消息 + type: integer + example: 1 + no_return_visit_flag: + description: 无需回访标识 + type: integer + example: 0 + common_rec_type_flag: + description: 通用记录类型标识 + type: string + example: "COMMON" + maxLength: 50 + common_rec_attr_flag: + description: 通用记录属性标识 + type: string + example: "AUTO" + maxLength: 50 + send_pub_check_task_flag: + description: 发送公共核查任务标识 + type: integer + example: 1 + reply_flag: + description: 回复标识 + type: string + example: "REPLIED" + maxLength: 50 + whistle_flag: + description: 吹哨标识 + type: string + example: "WHISTLE" + maxLength: 50 + repeat_state: + description: 重复状态 + type: string + example: "NOT_REPEAT" + maxLength: 50 + report_state: + description: 上报状态 + type: string + example: "SUBMITTED" + maxLength: 50 + dispose_state: + description: 处置状态 + type: integer + example: 1 + pre_dispose_state: + description: 预处置状态 + type: string + example: "PENDING" + maxLength: 50 + undertake_user_name: + description: 承办人员 + type: string + example: "张三" + maxLength: 50 + undertake_phone: + description: 联系电话 + type: string + example: "13800138000" + maxLength: 50 + deal_person_org: + description: 承办部门 + type: string + example: "部门名称" + maxLength: 50 + media_upload_num: + description: 媒体上传数量 + type: integer + example: 3 + media_upload_total_num: + description: 媒体上传总数 + type: integer + example: 5 + media_upload_state: + description: 媒体上传状态 + type: string + example: "SUCCESS" + maxLength: 50 + media_check_num: + description: 媒体核查数量 + type: integer + example: 2 + media_check_total_num: + description: 媒体核查总数 + type: integer + example: 5 + media_verify_num: + description: 媒体核实数量 + type: integer + example: 1 + media_verify_total_num: + description: 媒体核实总数 + type: integer + example: 5 + media_self_deal_num: + description: 自行处置媒体数量 + type: integer + example: 1 + media_self_deal_total_num: + description: 自行处置媒体总数 + type: integer + example: 3 + media_review_num: + description: 复核媒体数量 + type: integer + example: 1 + media_review_total_num: + description: 复核媒体总数 + type: integer + example: 3 + report_pic_num: + description: 上报图片数量 + type: integer + example: 2 + report_pic_total_num: + description: 上报图片总数 + type: integer + example: 3 + report_video_num: + description: 上报视频数量 + type: integer + example: 1 + report_video_total_num: + description: 上报视频总数 + type: integer + example: 1 + report_wav_num: + description: 上报音频数量 + type: integer + example: 0 + report_wav_total_num: + description: 上报音频总数 + type: integer + example: 0 + check_pic_num: + description: 核查图片数量 + type: integer + example: 2 + check_pic_total_num: + description: 核查图片总数 + type: integer + example: 3 + check_video_num: + description: 核查视频数量 + type: integer + example: 1 + check_video_total_num: + description: 核查视频总数 + type: integer + example: 1 + check_wav_num: + description: 核查音频数量 + type: integer + example: 0 + check_wav_total_num: + description: 核查音频总数 + type: integer + example: 0 + verify_pic_num: + description: 核实图片数量 + type: integer + example: 1 + verify_pic_total_num: + description: 核实图片总数 + type: integer + example: 1 + verify_video_num: + description: 核实视频数量 + type: integer + example: 0 + verify_video_total_num: + description: 核实视频总数 + type: integer + example: 0 + verify_wav_num: + description: 核实音频数量 + type: integer + example: 0 + verify_wav_total_num: + description: 核实音频总数 + type: integer + example: 0 + self_deal_pic_num: + description: 自行处置图片数量 + type: integer + example: 1 + self_deal_pic_total_num: + description: 自行处置图片总数 + type: integer + example: 2 + self_deal_video_num: + description: 自行处置视频数量 + type: integer + example: 0 + self_deal_video_total_num: + description: 自行处置视频总数 + type: integer + example: 1 + self_deal_wav_num: + description: 自行处置音频数量 + type: integer + example: 0 + self_deal_wav_total_num: + description: 自行处置音频总数 + type: integer + example: 0 + review_pic_num: + description: 复核图片数量 + type: integer + example: 1 + review_pic_total_num: + description: 复核图片总数 + type: integer + example: 1 + review_video_num: + description: 复核视频数量 + type: integer + example: 0 + review_video_total_num: + description: 复核视频总数 + type: integer + example: 0 + review_wav_num: + description: 复核音频数量 + type: integer + example: 0 + review_wav_total_num: + description: 复核音频总数 + type: integer + example: 0 + media_url: + description: 内部访问URL + type: string + example: "http://internal/media/123.jpg" + maxLength: 512 + mms_pic_path: + description: 彩信图片路径 + type: string + example: "/mms/123.jpg" + maxLength: 500 + media_path: + description: 服务器存储路径 + type: string + example: "/storage/media/123.jpg" + maxLength: 512 + media_type: + description: 媒体类型 + type: string + example: "IMAGE" + maxLength: 50 + media_usage: + description: 使用场景 + type: string + example: "上报" + maxLength: 100 + media_server_name: + description: 媒体服务器名称 + type: string + example: "media-server-01" + maxLength: 100 + media_property: + description: 媒体属性 + type: integer + example: 1 + media_uploaded_name: + description: 上传时的原始文件名 + type: string + example: "IMG_20240501.jpg" + maxLength: 255 + media_shot: + description: 截图标识或路径 + type: string + example: "/shots/123.jpg" + maxLength: 255 + media_label_type_id: + description: 标签类型ID + type: integer + example: 101 + media_default_url: + description: 外部可访问URL + type: string + example: "https://external/media/123.jpg" + maxLength: 512 + display_order: + description: 显示顺序 + type: integer + example: 1 + store_type_id: + description: 存储类型ID + type: integer + example: 1 + special_item_image_type: + description: 特殊图片类型 + type: string + example: "SIGNATURE" + maxLength: 100 + height: + description: 图片高度 + type: integer + example: 1080 + width: + description: 图片宽度 + type: integer + example: 1920 + send_flag: + description: 发送标志 + type: integer + example: 1 + gen_thumb: + description: 是否生成缩略图 + type: integer + example: 1 + can_delete: + description: 是否可删除 + type: integer + example: 1 + delete_flag: + description: 删除标记 + type: integer + example: 0 + delete_reason: + description: 删除原因 + type: string + example: "数据重复" + maxLength: 65535 + pos_type: + description: 位置类型 + type: string + example: "GPS" + maxLength: 50 + view_angle: + description: 视角 + type: string + example: "FRONT" + maxLength: 100 + view_image_name: + description: 视图图片名称 + type: string + example: "view_123.jpg" + maxLength: 200 + view_image_x: + description: 视图图片X坐标 + type: number + format: decimal + example: 0.5 + view_image_y: + description: 视图图片Y坐标 + type: number + format: decimal + example: 0.5 + view_pos_x: + description: 视图位置X坐标 + type: number + format: decimal + example: 0.5 + view_pos_y: + description: 视图位置Y坐标 + type: number + format: decimal + example: 0.5 + attach_rec_flag: + description: 附件记录标识 + type: string + example: "ATTACH" + maxLength: 50 + gather_flag: + description: 汇总标识 + type: string + example: "GATHERED" + maxLength: 50 + link_field_value: + description: 关联字段值 + type: string + example: "LINK-123" + maxLength: 500 + link_field_display_value: + description: 关联字段显示值 + type: string + example: "关联值显示" + maxLength: 500 + unique_id: + description: 唯一标识 + type: string + example: "UNIQ-20240501-001" + maxLength: 100 + third_unique_id: + description: 第三方唯一标识 + type: string + example: "THIRD-2024-001" + maxLength: 100 + equal_group_id: + description: 等值组ID + type: integer + example: 1001 + rec_category_id: + description: 记录类别ID + type: integer + example: 101 + dispatch_opinion: + description: 派遣意见 + type: string + example: "请尽快处理" + maxLength: 500 + revise_opinion: + description: 修订意见 + type: string + example: "建议补充图片" + maxLength: 500 + reply_opinion: + description: 回复意见 + type: string + example: "已处理完毕" + maxLength: 500 + new_inst_advise: + description: 立案建议 + type: string + example: "建议立案处理" + maxLength: 500 + new_inst_cond_id: + description: 立案条件ID + type: integer + example: 101 + new_inst_cond_name: + description: 立案条件 + type: string + example: "破损面积大于0.5㎡" + maxLength: 200 + case_closure_condition: + description: 结案条件 + type: string + example: "修复完成并验收" + maxLength: 200 + event_marks: + description: 事件标记 + type: string + example: "HIGH_PRIORITY" + maxLength: 500 + deduction: + description: 扣减 + type: string + example: "扣2分" + maxLength: 100 + event_property_id: + description: 事件属性ID + type: integer + example: 101 + event_property_name: + description: 事件属性名称 + type: string + example: "公共设施" + maxLength: 200 + city_village_flag: + description: 城乡标识 + type: string + example: "CITY" + maxLength: 50 + force_handle_flag: + description: 强制处理标识 + type: string + example: "FORCE" + maxLength: 50 + auto_check_count: + description: 自动核查次数 + type: integer + example: 2 + deal_evaluate_ids: + description: 处置评价ID列表 + type: string + example: "101,102,103" + maxLength: 200 + newinst_no_transit: + description: 立案不转交 + type: string + example: "NO_TRANSIT" + maxLength: 50 + super_rec_id: + description: 上级记录ID + type: integer + example: 2001 + site_num: + description: 站点编号 + type: string + example: "SITE-001" + maxLength: 50 + difficult_type_id: + description: 困难类型ID + type: integer + example: 101 + event_district_grade_id: + description: 事件区域等级ID + type: integer + example: 101 + event_district_grade_name: + description: 事件区域等级名称 + type: string + example: "重点区域" + maxLength: 100 + cus_grid_code: + description: 自定义网格编码 + type: string + example: "CUST-GRID-001" + maxLength: 100 + site_id: + description: 站点ID + type: integer + example: 101 + shop_id: + description: 商铺ID + type: integer + example: 101 + shop_name: + description: 商铺名称 + type: string + example: "幸福便利店" + maxLength: 200 + spec_type_id: + description: 特殊类型ID + type: integer + example: 101 + spec_type_name: + description: 特殊类型名称 + type: string + example: "紧急事件" + maxLength: 100 + proc_account_state_id: + description: 处理账户状态ID + type: integer + example: 1 + check_type_id: + description: 核查类型ID + type: integer + example: 1 + rec_analysis_type_id: + description: 记录分析类型ID + type: integer + example: 1 + proc_time_state_id: + description: 处理流程状态ID + type: integer + example: 1 + proc_ard_state_id: + description: 处理仲裁状态ID + type: integer + example: 1 + proc_enq_state_id: + description: 处理询问状态ID + type: integer + example: 1 + proc_sup_state_id: + description: 处理监督状态ID + type: integer + example: 1 + func_time_state_id: + description: 职能部门时间状态ID + type: integer + example: 1 + check_msg_state_id: + description: 核查消息状态ID + type: integer + example: 1 + verify_msg_state_id: + description: 核实消息状态ID + type: integer + example: 1 + regather_msg_state_id: + description: 重新采集消息状态ID + type: integer + example: 1 + supervision_check_state_id: + description: 监督核查状态ID + type: integer + example: 1 + self_deal_msg_state_id: + description: 自行处置消息状态ID + type: integer + example: 1 + review_msg_state_id: + description: 复核消息状态ID + type: integer + example: 1 + proc_press_state_id: + description: 处理压力状态ID + type: integer + example: 1 + hot_area: + description: 热点区域 + type: string + example: "市中心" + maxLength: 100 + cg_area: + description: 城管区域 + type: string + example: "鼓楼区" + maxLength: 100 + hw_area: + description: 环卫区域 + type: string + example: "鼓楼区" + maxLength: 100 + sz_area: + description: 市政区域 + type: string + example: "鼓楼区" + maxLength: 100 + device_guid: + description: 设备GUID + type: string + example: "A1B2-C3D4-E5F6" + maxLength: 100 + jx_id: + description: 警讯ID + type: integer + example: 1001 + jx_jxmc: + description: 警讯名称 + type: string + example: "道路塌陷警讯" + maxLength: 200 + jx_design_type: + description: 警讯设计类型 + type: string + example: "自动触发" + maxLength: 100 + report_time_segment_id: + description: 上报时段ID + type: integer + example: 101 + archive_cond_id: + description: 归档条件ID + type: integer + example: 101 + archive_cond: + description: 归档条件 + type: string + example: "处理完成" + maxLength: 100 + archive_type_id: + description: 归档类型ID + type: integer + example: 101 + road_type_id: + description: 道路类型ID + type: integer + example: 101 + road_name: + description: 道路名称 + type: string + example: "中山路" + maxLength: 200 + road_id: + description: 道路ID + type: integer + example: 101 + road_type_name: + description: 道路类型名称 + type: string + example: "主干道" + maxLength: 100 + area_type_id: + description: 区域类型ID + type: integer + example: 101 + duty_grid_type_id: + description: 责任网格类型ID + type: integer + example: 101 + deal_duty_grid_type_id: + description: 处置责任网格类型ID + type: integer + example: 101 + time_area_id: + description: 时段ID + type: integer + example: 101 + time_area_name: + description: 时段名称 + type: string + example: "早高峰" + maxLength: 100 + card_num: + description: 证件号码 + type: string + example: "110101199001012345" + maxLength: 100 + cell_id: + description: 单元格ID + type: integer + example: 101 + cell_name: + description: 单元格名称 + type: string + example: "A01单元" + maxLength: 200 + damage_grade_id: + description: 损毁等级ID + type: integer + example: 101 + damage_grade_name: + description: 损毁等级名称 + type: string + example: "严重" + maxLength: 100 + event_grade_id: + description: 事件等级ID + type: integer + example: 101 + event_grade_name: + description: 事件等级名称 + type: string + example: "重大" + maxLength: 100 + event_level_id: + description: 事件级别ID + type: integer + example: 101 + event_level_name: + description: 事件级别名称 + type: string + example: "一级" + maxLength: 100 + event_district_id: + description: 事件区域ID + type: integer + example: 101 + event_district_name: + description: 事件区域名称 + type: string + example: "鼓楼区" + maxLength: 100 + display_property: + description: 显示属性 + type: string + example: "高亮显示" + maxLength: 200 + display_style_id: + description: 显示样式ID + type: integer + example: 1 + refresh_flag: + description: 刷新标识 + type: integer + example: 1 + video_device_id: + description: 视频设备ID + type: integer + example: 101 + video_param: + description: 视频参数 + type: string + example: "1080p,30fps" + maxLength: 500 + patrol_deal_flag: + description: 巡查处置标识 + type: integer + example: 1 + send_from_type: + description: 发送来源类型 + type: string + example: "APP" + maxLength: 50 + reply_intime_deadline: + description: 两小时回复截止时间戳 + type: integer + example: 1714568000000 + accept_status: + description: 受理状态 + type: string + example: "ACCEPTED" + maxLength: 50 + squadron_id: + description: 中队ID + type: integer + example: 101 + squadron_name: + description: 中队名称 + type: string + example: "第一中队" + maxLength: 200 + property_company_id: + description: 物业公司ID + type: integer + example: 101 + act_record_id: + description: 操作记录ID + type: integer + example: 1001 + main_rec_id: + description: 主记录ID + type: integer + example: 2001 + func_custom_limit: + description: 职能部门自定义时限 + type: string + example: "72小时" + maxLength: 50 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, **kwargs): + """ + 创建新的任务表单数据。 + + 业务流程: + 1. 使用 HumanTaskFormDatumForm 验证表单数据完整性 + 2. 检查任务是否已存在(根据 rec_id) + 3. 创建新任务对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的任务对象 + + :param kwargs: 任务参数字典 + :return: 新建任务对象 + :rtype: DcmTaskFormDatum + :raises AssertionError: 当任务已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _task_form = DcmTaskFormDatumForm(formdata=kwargs) + _task_form.validate_form() + + # 检查是否存在同记录ID的任务 + _task: cls = await cls.is_exist(_task_form.rec_id.data) + assert _task is None, "记录ID已存在,不能重复创建。" + + # 创建任务对象 + _task = cls().copy_from_dict(_task_form.data, skip_none=True).before_save() + await _task.async_save() + return _task + + @classmethod + async def delete(cls, task_id: Union[str, int]): + """ + 删除任务表单数据。 + + 业务流程: + 1. 根据ID查找任务 + 2. 验证任务存在性 + 3. 执行删除操作 + + :param task_id: 要删除的任务ID + :return: 删除的任务对象 + :rtype: DcmTaskFormDatum + :raises AssertionError: 当任务不存在时抛出 + """ + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f"根据 ID {task_id} 未找到任务。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _task.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除任务表单数据(记录ID:{_task.rec_id},ID:{_task.id}).') + return _task + + @classmethod + async def modify(cls, task_id: Union[str, int], **kwargs): + """ + 修改已有任务表单数据。 + + 业务流程: + 1. 将 task_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 HumanTaskFormDatumForm 验证表单数据 + 4. 检查是否有其他任务使用了相同的 rec_id + 5. 查询原任务对象 + 6. 验证任务存在性 + 7. 更新字段并设置更新者 + 8. 保存到数据库 + 9. 返回更新后的任务对象 + + :param task_id: 要修改的任务ID + :param kwargs: 需要更新的字段 + :return: 修改后的任务对象 + :rtype: DcmTaskFormDatum + :raises AssertionError: 当任务不存在或信息重复时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _task_form = DcmTaskFormDatumForm(formdata=kwargs) + _task_form.validate_form() + + # 检查是否与其他任务重复(排除自身) + _other = await cls.exist_other(task_id, _task_form.rec_id.data) + assert _other is None, "记录ID已存在,不能重复修改。" + + # 查询原任务 + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f'查无此任务信息。' + + # 更新字段 + _task.copy_from_dict(_task_form.data, skip_none=True).before_save() + await _task.async_save() + return _task + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新任务表单数据(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含任务数据的 DataFrame,字段需与模型属性匹配(如 rec_id, task_num 等) + :return: 成功创建的任务数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + tasks = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(tasks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(tasks)} 条新任务表单数据。") + return len(tasks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有任务表单数据。 + + :param data_df: 包含任务数据的 DataFrame + :return: 成功更新的任务数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条任务表单数据。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmTaskFormDatum.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmTaskFormDatum.create_batch(_latest_df) + _updated_count = await DcmTaskFormDatum.modify_batch(_exists_df) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task_more_info.py b/models/dcm_task_more_info.py new file mode 100644 index 0000000..070a638 --- /dev/null +++ b/models/dcm_task_more_info.py @@ -0,0 +1,236 @@ +from typing import Optional, Callable +from paste.web.form import ModelForm +from paste.core.logging import echo_log +from wtforms import StringField, IntegerField +from wtforms.validators import Length +from tornado_swagger.model import register_swagger_model +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTaskMoreInfo +import pandas as pd +from sqlalchemy import select + + +class DcmTaskMoreInfoForm(ModelForm): + """ + 更多信息表单验证类(完全映射 TD3iDcmTaskMoreInfo 字段)。 + + 用于验证和处理数字城管-部门待办任务更多信息数据。 + 字段完全映射数据库表 t_d3i_dcm_task_more_info 的字段结构。 + """ + rec_id=IntegerField('记录ID') + create_time=StringField('创建时间',validators=[Length(max=64,message='创建时间长度不能超过64个字符')]) + ex_info_id=IntegerField('更多信息ID') + ex_info_msg=StringField('更多信息内容',validators=[Length(max=255,message='更多信息内容长度不能超过255个字符')]) + human_id=IntegerField('人员ID') + human_name=StringField('人员姓名',validators=[Length(max=50,message='人员姓名长度不能超过50个字符')]) + msg_id=IntegerField('消息ID') + msg_info=StringField('消息详情',validators=[Length(max=255,message='消息详情长度不能超过255个字符')]) + msg_type=StringField('消息类型名称',validators=[Length(max=50,message='消息类型名称长度不能超过50个字符')]) + msg_type_id=IntegerField('消息类型ID') + role_name=StringField('角色名称',validators=[Length(max=50,message='角色名称长度不能超过50个字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskMoreInfoBase(TD3iDcmTaskMoreInfo, CommonModel): + """ + 更多信息基础类(完全映射 TD3iDcmTaskMoreInfo 字段)。 + + 封装所有与任更多信息相关的通用操作方法。 + """ + FieldMapping = { + 'rec_id': 'recID', + 'create_time': 'createTime', + 'ex_info_id': 'exInfoID', + 'ex_info_msg': 'exInfoMsg', + 'human_id': 'humanID', + 'human_name': 'humanName', + 'msg_id': 'msgID', + 'msg_info': 'msgInfo', + 'msg_type': 'msgType', + 'msg_type_id': 'msgTypeID', + 'role_name': 'roleName' + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。仅根据 rec_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 raw_id(rec_id)列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录(已匹配数据库id) + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 rec_id(去重) + rec_ids = data_df[cls.rec_id.key].drop_duplicates().tolist() + if not rec_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库仅根据 rec_id 匹配 + _query = select(cls.id, cls.rec_id).where( + cls.rec_id.in_(rec_ids) + ) + exists_df = await cls.query_as_df(_query) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 rec_id -> 数据库id 的映射(单字段) + key_to_id_map = dict(zip(exists_df[cls.rec_id.key], exists_df[cls.id.key])) + + # 根据 rec_id 判断是否存在 + mask_exists = data_df.apply(lambda row: row[cls.rec_id.key] in key_to_id_map, axis=1) + + # 拆分存在/不存在的数据 + exists_df = data_df[mask_exists].copy() + # 通过 rec_id 匹配数据库主键 + exists_df[cls.id.key] = exists_df.apply(lambda row: key_to_id_map[row[cls.rec_id.key]], axis=1) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def fill_more_info(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'more_infos', + preprocessing: Optional[Callable] = None): + """ + 填充更多信息数据到数据框架。 + + 用于在查询结果中添加关联的更多信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`more_info` + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 更多信息数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_id.in_(_task_ids)) + _more_info_df: pd.DataFrame = await cls.query_as_df(_query) + if not _more_info_df.empty: + _more_info_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _more_info_df[cls.id.key] = _more_info_df[cls.id.key].astype(str) + _more_info_df[cls.dcm_task_id.key] = _more_info_df[cls.dcm_task_id.key].astype(str) + # 设置索引 + _more_info_df['index_id'] = _more_info_df[cls.dcm_task_id.key] + _more_info_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _more_info_df = preprocessing(_more_info_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _more_info_df.query(f"{cls.dcm_task_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + return _more_info_df + + +@register_swagger_model +class DcmTaskMoreInfo(DcmTaskMoreInfoBase): + """ + 更多信息业务模型类(主业务类,完全继承 TD3iDcmTaskMoreInfo 字段)。 + """ + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新更多信息(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含更多信息数据的 DataFrame,字段需与模型属性匹配 + :return: 成功创建的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + records = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条任务更多信息。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有更多信息。 + + :param data_df: 包含更多信息数据的 DataFrame + :return: 成功更新的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条任务的更多信息。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmTaskMoreInfo.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmTaskMoreInfo.create_batch(_latest_df) + _updated_count = await DcmTaskMoreInfo.modify_batch(_exists_df) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/dcm_task_process_info.py b/models/dcm_task_process_info.py new file mode 100644 index 0000000..30e7b4c --- /dev/null +++ b/models/dcm_task_process_info.py @@ -0,0 +1,757 @@ +import random +from typing import Union, Optional, Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iDcmTaskProcessInfo +from paste.core.logging import echo_log +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class DcmTaskProcessInfoForm(ModelForm): + """ + 办理经过表单验证类(完全映射 TD3iDcmTaskProcessInfo 字段)。 + + 用于验证和处理数字城管-部门待办任务办理经过的记录/更新表单数据。 + 字段完全映射数据库表 t_d3i_dcm_task_process_info 的字段结构。 + """ + + # 基础信息 + raw_id = IntegerField('原始主键ID') + rec_id = IntegerField('记录ID') + act_id = IntegerField('任务ID') + act_def_id = IntegerField('流程节点定义ID') + act_def_name = StringField('流程节点名称', validators=[Length(max=100, message='流程节点名称长度不能超过100字符')]) + act_time_state_id = IntegerField('操作时间状态ID') + act_limit_info = StringField('操作时限信息', validators=[Length(max=255, message='时限信息长度不能超过255字符')]) + act_used_time_char = StringField('已用时间(字符串)', validators=[Length(max=50, message='已用时间描述长度不能超过50字符')]) + act_remain_time_char = StringField('剩余时间(字符串)', validators=[Length(max=50, message='剩余时间描述长度不能超过50字符')]) + act_deadline_time = IntegerField('操作截止时间戳(毫秒)') + act_property_id = IntegerField('操作属性ID') + + # 操作信息 + action_name = StringField('操作动作名称', validators=[Length(max=100, message='操作动作名称长度不能超过100字符')]) + action_time = IntegerField('操作时间戳(毫秒)') + title = StringField('操作标题', validators=[Length(max=100, message='标题长度不能超过100字符')]) + detail = TextAreaField('操作详细意见', validators=[Length(max=65535, message='意见长度不能超过65535字符')]) + backup_detail = TextAreaField('备用意见', validators=[Length(max=65535, message='备用意见长度不能超过65535字符')]) + medias = TextAreaField('附件信息(JSON格式)', validators=[Length(max=65535, message='附件信息长度不能超过65535字符')]) + + # 单位与人员 + unit_name = StringField('当前操作单位', validators=[Length(max=100, message='单位名称长度不能超过100字符')]) + unit_contact = StringField('单位联系方式', validators=[Length(max=255, message='联系方式长度不能超过255字符')]) + human_id = IntegerField('操作人ID') + human_name = StringField('操作人名称(含单位)', validators=[Length(max=255, message='操作人名称长度不能超过255字符')]) + role_name = StringField('当前角色名称', validators=[Length(max=100, message='角色名称长度不能超过100字符')]) + + # 项目信息 + item_id = IntegerField('项目ID') + item_type_id = IntegerField('任务类型ID') + item_content = TextAreaField('任务内容摘要', validators=[Length(max=65535, message='内容摘要长度不能超过65535字符')]) + item_process_info_list = TextAreaField('子流程列表(JSON)', validators=[Length(max=65535, message='子流程列表长度不能超过65535字符')]) + sub_process_info = TextAreaField('子流程信息', validators=[Length(max=65535, message='子流程信息长度不能超过65535字符')]) + + # 捆绑信息 + bundle_time_state_id = IntegerField('组合时间状态ID') + bundle_limit_info = StringField('组合时限信息', validators=[Length(max=255, message='组合时限信息长度不能超过255字符')]) + bundle_used_char = StringField('组合已用时间', validators=[Length(max=50, message='组合已用时间长度不能超过50字符')]) + bundle_remain_char = StringField('组合剩余时间', validators=[Length(max=50, message='组合剩余时间长度不能超过50字符')]) + bundle_deadline_time = IntegerField('组合截止时间戳(毫秒)') + + # 下一节点信息 + show_unit_contact = IntegerField('是否显示单位联系方式') + pre_unit_name = StringField('上一单位', validators=[Length(max=100, message='上一单位名称长度不能超过100字符')]) + pre_action_name = StringField('上一操作名称', validators=[Length(max=100, message='上一操作名称长度不能超过100字符')]) + pre_human_name = StringField('上一操作人', validators=[Length(max=255, message='上一操作人名称长度不能超过255字符')]) + pre_act_opinion = TextAreaField('上一操作意见', validators=[Length(max=65535, message='上一操作意见长度不能超过65535字符')]) + next_act_def_name = StringField('下一节点名称', validators=[Length(max=100, message='下一节点名称长度不能超过100字符')]) + next_role_part_name = StringField('下一角色/单位', validators=[Length(max=255, message='下一角色/单位长度不能超过255字符')]) + next_role_name = StringField('下一角色名称', validators=[Length(max=100, message='下一角色名称长度不能超过100字符')]) + next_act_property_id = IntegerField('下一操作属性ID') + + # 最后节点标识 + last_act_flag = IntegerField('是否为最后一节点(0否,1是)') + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class DcmTaskProcessInfoBase(TD3iDcmTaskProcessInfo, CommonModel): + """ + 办理经过基础类(完全映射 TD3iDcmTaskProcessInfo 字段)。 + + 封装所有与任务办理经过相关的通用操作方法。 + """ + + FieldMapping = { + 'raw_id': 'id', + 'act_id': 'actID', + 'act_def_id': 'actDefID', + 'act_def_name': 'actDefName', + 'act_time_state_id': 'actTimeStateID', + 'act_limit_info': 'actLimitInfo', + 'act_used_time_char': 'actUsedTimeChar', + 'act_remain_time_char': 'actRemainTimeChar', + 'act_deadline_time': 'actDeadlineTime', + 'act_property_id': 'actPropertyID', + 'action_name': 'actionName', + 'action_time': 'actionTime', + 'title': 'title', + 'detail': 'detail', + 'backup_detail': 'backupDetail', + 'medias': 'medias', + 'unit_name': 'unitName', + 'unit_contact': 'unitContact', + 'human_id': 'humanID', + 'human_name': 'humanName', + 'role_name': 'roleName', + 'item_id': 'itemID', + 'item_type_id': 'itemTypeID', + 'item_content': 'itemContent', + 'item_process_info_list': 'itemProcessInfoList', + 'sub_process_info': 'subProcessInfo', + 'bundle_time_state_id': 'bundleTimeStateID', + 'bundle_limit_info': 'bundleLimitInfo', + 'bundle_used_char': 'bundleUsedChar', + 'bundle_remain_char': 'bundleRemainChar', + 'bundle_deadline_time': 'bundleDeadlineTime', + 'show_unit_contact': 'showUnitContact', + 'pre_unit_name': 'preUnitName', + 'pre_action_name': 'preActionName', + 'pre_human_name': 'preHumanName', + 'pre_act_opinion': 'preActOpinion', + 'next_act_def_name': 'nextActDefName', + 'next_role_part_name': 'nextRolePartName', + 'next_role_name': 'nextRoleName', + 'next_act_property_id': 'nextActPropertyID', + 'last_act_flag': 'lastActFlag', + } + """ + 处理流程映射 + """ + + @classmethod + async def exist_other(cls, id: Union[str, int], rec_id: Union[str, int], act_id: Union[str, int]): + """ + 检查是否存在除当前记录外的其他同任务ID或同原始主键ID的记录。 + + :param act_id: 当前任务ID + :param rec_id: 原始主键ID + :return: 存在返回记录对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.rec_id == rec_id, cls.act_id == act_id) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找办理经过。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _record_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _record_list + + @classmethod + async def is_exist(cls, rec_id: Union[str, int], act_id: Union[str, int]): + """ + 检查办理经过是否已经存在(根据原始主键ID)。 + """ + _query = select(cls).where(cls.rec_id == rec_id, cls.act_id == act_id) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索办理流程记录的基础方法。 + + 支持字段: + - act_id, unit_name, human_name, role_name, last_act_flag + - 支持模糊匹配:act_def_name, action_name, title, item_content + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'action_time': 'desc'} + :key int act_id: 精确匹配任务ID + :key str unit_name: 精确匹配单位 + :key str human_name: 精确匹配操作人 + :key str role_name: 精确匹配角色 + :key int last_act_flag: 精确匹配是否为最后一节点 + :key str act_def_name: 模糊匹配流程节点名称 + :key str action_name: 模糊匹配操作动作 + :key str title: 模糊匹配标题 + :key str item_content: 模糊匹配内容摘要 + :key int action_time_start: 时间范围起始(毫秒) + :key int action_time_end: 时间范围结束(毫秒) + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + _name_likes = { + cls.act_def_name.key: '%{}%', + cls.action_name.key: '%{}%', + cls.title.key: '%{}%', + cls.item_content.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ) + + if 'action_time_start' in kwargs and 'action_time_end' in kwargs: + _query = _query.where( + cls.action_time >= kwargs['action_time_start'], + cls.action_time <= kwargs['action_time_end'] + ) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.action_time.desc()) + + _process_df = await cls.query_as_df(_data_query) + if not _process_df.empty: + _process_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + return _process_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索办理流程记录,返回分页格式数据。 + """ + _process_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _process_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_rec_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 raw_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 raw_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 (rec_id, act_id) 组合 + pairs = data_df[[cls.rec_id.key, cls.act_id.key]].drop_duplicates().values.tolist() + if not pairs: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.rec_id, cls.act_id).where( + (cls.rec_id.in_([p[0] for p in pairs])) & + (cls.act_id.in_([p[1] for p in pairs])) + ) + exists_df = await cls.query_as_df(_query) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 (rec_id, act_id) -> id 的映射 + key_to_id_map = dict(zip(zip(exists_df[cls.rec_id.key], exists_df[cls.act_id.key]), exists_df[cls.id.key])) + + # 根据组合是否在数据库中划分数据 + mask_exists = data_df.apply(lambda row: (row[cls.rec_id.key], row[cls.act_id.key]) in key_to_id_map, axis=1) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df.apply(lambda row: key_to_id_map[(row[cls.rec_id.key], row[cls.act_id.key])], axis=1) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def fill_process_info(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'process_infos', + preprocessing: Optional[Callable] = None): + """ + 填充办理过程数据到数据框架。 + + 用于在查询结果中添加关联的办理过程信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`process_infos` + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 办理过程数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.dcm_task_id.in_(_task_ids)) + _info_df: pd.DataFrame = await cls.query_as_df(_query) + if not _info_df.empty: + _info_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _info_df[cls.id.key] = _info_df[cls.id.key].astype(str) + _info_df[cls.dcm_task_id.key] = _info_df[cls.dcm_task_id.key].astype(str) + + # 设置索引 + _info_df['index_id'] = _info_df[cls.dcm_task_id.key] + _info_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _info_df = preprocessing(_info_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _info_df.query(f"{cls.dcm_task_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + + return _info_df + + +@register_swagger_model +class DcmTaskProcessInfo(DcmTaskProcessInfoBase): + """ + 办理经过业务模型类(主业务类,完全继承 TD3iDcmTaskProcessInfo 字段)。 + + --- + description: 数字城管-部门待办任务办理经过 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + raw_id: + description: 原始主键ID + type: integer + example: 2001 + act_id: + description: 任务ID + type: integer + example: 3001 + act_def_id: + description: 流程节点定义ID + type: integer + example: 101 + act_def_name: + description: 流程节点名称 + type: string + example: "受理" + maxLength: 100 + act_time_state_id: + description: 操作时间状态ID + type: integer + example: 1 + act_limit_info: + description: 操作时限信息 + type: string + example: "24小时内" + maxLength: 255 + act_used_time_char: + description: 已用时间(字符串) + type: string + example: "12小时" + maxLength: 50 + act_remain_time_char: + description: 剩余时间(字符串) + type: string + example: "12小时" + maxLength: 50 + act_deadline_time: + description: 操作截止时间戳(毫秒) + type: integer + example: 1714578000000 + act_property_id: + description: 操作属性ID + type: integer + example: 5 + action_name: + description: 操作动作名称(如批转、回退) + type: string + example: "批转" + maxLength: 100 + action_time: + description: 操作时间戳(毫秒) + type: integer + example: 1714567890000 + title: + description: 操作标题 + type: string + example: "案件受理完成" + maxLength: 100 + detail: + description: 操作详细意见 + type: string + example: "经核查,该案件属实,已转交市政工程处处理。" + maxLength: 65535 + backup_detail: + description: 备用意见 + type: string + example: "系统自动记录" + maxLength: 65535 + medias: + description: 附件信息(JSON格式) + type: string + example: "[{\"media_id\":3001,\"name\":\"photo.jpg\"}]" + maxLength: 65535 + unit_name: + description: 当前操作单位 + type: string + example: "市政工程处" + maxLength: 100 + unit_contact: + description: 单位联系方式 + type: string + example: "025-88888888" + maxLength: 255 + human_id: + description: 操作人ID,-1为系统 + type: integer + example: 101 + human_name: + description: 操作人名称(含单位) + type: string + example: "张三(市政工程处)" + maxLength: 255 + role_name: + description: 当前角色名称 + type: string + example: "审批员" + maxLength: 100 + item_id: + description: 项目ID + type: integer + example: 4001 + item_type_id: + description: 任务类型ID + type: integer + example: 101 + item_content: + description: 任务内容摘要 + type: string + example: "中山路破损路面修复" + maxLength: 65535 + item_process_info_list: + description: 子流程列表(JSON) + type: string + example: "[{\"node\":\"受理\",\"time\":1714567890000}]" + maxLength: 65535 + sub_process_info: + description: 子流程信息 + type: string + example: "{\"sub1\":\"已处理\",\"sub2\":\"待验收\"}" + maxLength: 65535 + bundle_time_state_id: + description: 组合时间状态ID + type: integer + example: 2 + bundle_limit_info: + description: 组合时限信息 + type: string + example: "48小时内" + maxLength: 255 + bundle_used_char: + description: 组合已用时间 + type: string + example: "36小时" + maxLength: 50 + bundle_remain_char: + description: 组合剩余时间 + type: string + example: "12小时" + maxLength: 50 + bundle_deadline_time: + description: 组合截止时间戳(毫秒) + type: integer + example: 1714580000000 + show_unit_contact: + description: 是否显示单位联系方式 + type: integer + example: 1 + pre_unit_name: + description: 上一单位 + type: string + example: "街道办" + maxLength: 100 + pre_action_name: + description: 上一操作名称 + type: string + example: "初审" + maxLength: 100 + pre_human_name: + description: 上一操作人 + type: string + example: "李四(街道办)" + maxLength: 255 + pre_act_opinion: + description: 上一操作意见 + type: string + example: "建议转交专业部门处理" + maxLength: 65535 + next_act_def_name: + description: 下一节点名称 + type: string + example: "审批" + maxLength: 100 + next_role_part_name: + description: 下一角色/单位 + type: string + example: "市政工程处-审批组" + maxLength: 255 + next_role_name: + description: 下一角色名称 + type: string + example: "审批员" + maxLength: 100 + next_act_property_id: + description: 下一操作属性ID + type: integer + example: 6 + last_act_flag: + description: 是否为最后一节点(0否,1是) + type: integer + example: 0 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, **kwargs): + """ + 创建办理经过。 + + 业务流程: + 1. 使用 D3iDcmTaskProcessInfoForm 验证表单数据 + 2. 设置创建者、更新者 + 3. 保存到数据库 + 4. 返回创建的流程记录对象 + + :param kwargs: 办理经过参数字典 + :return: 新建流程记录对象 + :rtype: DcmTaskProcessInfo + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmTaskProcessInfoForm(formdata=kwargs) + _form.validate_form() + + # 检查是否存在同记录ID的任务 + _task: cls = await cls.is_exist(_form.rec_id.data, _form.act_id.data) + assert _task is None, "记录ID已存在,不能重复创建。" + + _record = cls().copy_from_dict(_form.data, skip_none=True).before_save() + await _record.async_save() + return _record + + @classmethod + async def delete(cls, process_id: Union[str, int]): + """ + 软删除办理经过(设置 delete_flag=1)。 + + :param process_id: 要删除的办理经过ID + :return: 删除的流程记录对象 + :rtype: DcmTaskProcessInfo + :raises AssertionError: 当记录不存在时抛出 + """ + _record: cls = await cls.async_find_by_id(process_id) + assert _record, f"根据 ID {process_id} 未找到办理经过。" + + # 执行删除 + _del_query = delete(cls).where(cls.id == _record.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除任务办理经过(记录ID:{_record.rec_id},ID:{_record.id}).') + return _record + + @classmethod + async def modify(cls, process_id: Union[str, int], **kwargs): + """ + 修改办理经过(仅允许修改非核心流程字段,如意见、标题、联系方式等)。 + + 注意:不允许修改 act_id、action_time、act_def_name 等关键流程节点信息。 + + 业务流程: + 1. 将 process_id 加入参数 + 2. 处理字符串字段去除空格 + 3. 使用表单验证 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新允许字段 + 7. 设置更新者 + 8. 保存 + + :param process_id: 要修改的办理经过ID + :param kwargs: 需要更新的字段 + :return: 修改后的流程记录对象 + :rtype: DcmTaskProcessInfo + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + kwargs[cls.id.key] = process_id + + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = DcmTaskProcessInfoForm(formdata=kwargs) + _form.validate_form() + + _record: cls = await cls.async_find_by_id(process_id) + assert _record, f'查无此办理经过。' + + # 仅允许更新非核心流程字段 + allowed_fields = { + 'title', 'detail', 'backup_detail', 'medias', + 'unit_contact', 'human_name', 'role_name', + 'item_content', 'item_process_info_list', 'sub_process_info', + 'show_unit_contact', 'pre_act_opinion', 'pre_human_name' + } + + update_data = {k: v for k, v in _form.data.items() if k in allowed_fields and v is not None} + _record.copy_from_dict(update_data, skip_none=True).before_save() + await _record.async_save() + + return _record + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新办理经过(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含办理经过数据的 DataFrame,字段需与模型属性匹配(如 raw_id, act_id 等) + :return: 成功创建的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + records = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条任务办理经过。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有办理经过。 + + :param data_df: 包含办理经过数据的 DataFrame + :return: 成功更新的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条任务办理经过。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await DcmTaskProcessInfo.exists_rec_id(data_df) + # 保存到数据库 + _created_count = await DcmTaskProcessInfo.create_batch(_latest_df) + _updated_count = await DcmTaskProcessInfo.modify_batch(_exists_df) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task.py b/models/govc_task.py new file mode 100644 index 0000000..85fac98 --- /dev/null +++ b/models/govc_task.py @@ -0,0 +1,636 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTask +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskForm(ModelForm): + """ + 市12345工单表单验证类(完全映射 TD3iGovcTask 字段)。 + + 用于验证和处理市12345工单主表的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + evl_result = StringField('结果满意度', validators=[Length(max=64, message='结果满意度长度不能超过64字符')]) + finish_result = TextAreaField('办结结果', validators=[Length(max=65535, message='办结结果长度不能超过65535字符')]) + serial_num = StringField('工单编号', validators=[Length(max=64, message='工单编号长度不能超过64字符')]) + t_status = StringField('任务单状态', validators=[Length(max=64, message='任务单状态长度不能超过64字符')]) + accord_type = StringField('归口类型', validators=[Length(max=255, message='归口类型长度不能超过255字符')]) + create_date = DateTimeField('交办时间') + back_time_bf = DateTimeField('拒绝时限') + sub_handle_ou_name = StringField('子处办单位', + validators=[Length(max=255, message='子处办单位长度不能超过255字符')]) + sign_time_bf = IntegerField('签收时限时间戳') + is_leaf = StringField('是否叶子节点', validators=[Length(max=32, message='是否叶子节点长度不能超过32字符')]) + row_guid = StringField('rowguid', validators=[Length(max=64, message='rowguid长度不能超过64字符')]) + c_guid = StringField('查询详情使用guid', validators=[Length(max=64, message='查询详情使用guid长度不能超过64字符')]) + finish_time = IntegerField('办结时间戳') + sign_time = IntegerField('签收时间戳') + is_secret = StringField('是否保密', validators=[Length(max=32, message='是否保密长度不能超过32字符')]) + finish_time_bf = DateTimeField('办结时限') + link_number = StringField('联系号码', validators=[Length(max=64, message='联系号码长度不能超过64字符')]) + pvi_guid = StringField('查询详情使用pviguid', + validators=[Length(max=64, message='查询详情使用pviguid长度不能超过64字符')]) + rqst_type = StringField('诉求类型', validators=[Length(max=64, message='诉求类型长度不能超过64字符')]) + rqst_content = TextAreaField('诉求内容', validators=[Length(max=65535, message='诉求内容长度不能超过65535字符')]) + handle_ou_name = StringField('处办单位', validators=[Length(max=255, message='处办单位长度不能超过255字符')]) + rqst_title = StringField('标题', validators=[Length(max=500, message='标题长度不能超过500字符')]) + sign_person = StringField('签收人', validators=[Length(max=128, message='签收人长度不能超过128字符')]) + rqst_person = StringField('诉求人', validators=[Length(max=128, message='诉求人长度不能超过128字符')]) + rqs_channel = StringField('渠道来源', validators=[Length(max=64, message='渠道来源长度不能超过64字符')]) + t_type = StringField('工单类型', validators=[Length(max=64, message='工单类型长度不能超过64字符')]) + solve_situation = StringField('解决情况', validators=[Length(max=64, message='解决情况长度不能超过64字符')]) + evl_style = StringField('态度满意度', validators=[Length(max=64, message='态度满意度长度不能超过64字符')]) + send_opinion = TextAreaField('派送意见', validators=[Length(max=65535, message='派送意见长度不能超过65535字符')]) + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64, message='创建者长度不能超过64字符')]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64, message='更新者长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskBase(TD3iGovcTask, CommonModel): + """ + 市12345工单基础类(完全映射 TD3iGovcTask 字段)。 + + 继承自数据库模型 TD3iGovcTask 和通用模型 CommonModel。 + 封装所有与市12345工单相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'evl_result': 'evlresult', + 'finish_result': 'finishresult', + 'serial_num': 'serialnum', + 't_status': 'tstatus', + 'accord_type': 'accordtype', + 'create_date': 'createdate', + 'back_time_bf': 'backtime_bf', + 'sub_handle_ou_name': 'subhandleouname', + 'sign_time_bf': 'signtime_bf', + 'is_leaf': 'isLeaf', + 'row_guid': 'rowguid', + 'c_guid': 'cguid', + 'finish_time': 'finishtime', + 'sign_time': 'signtime', + 'is_secret': 'issecret', + 'finish_time_bf': 'finishtime_bf', + 'link_number': 'linknumber', + 'pvi_guid': 'pviguid', + 'rqst_type': 'rqsttype', + 'rqst_content': 'rqstcontent', + 'handle_ou_name': 'handleouname', + 'rqst_title': 'rqsttitle', + 'sign_person': 'signperson', + 'rqst_person': 'rqstperson', + 'rqs_channel': 'rqschannel', + 't_type': 'ttype', + 'solve_situation': 'solvesituation', + 'evl_style': 'evlstyle', + 'send_opinion': 'sendopinion' + } + """ + 工单数据映射 + """ + + @classmethod + async def is_exist(cls, serial_num: str): + """ + 检查工单记录是否已存在(根据工单编号)。 + + :param serial_num: 工单编号 + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.serial_num == serial_num) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单数据的基础方法。 + + 支持字段: + - serial_num, row_guid, c_guid, pvi_guid, t_status, t_type, is_leaf, is_secret + - 支持模糊匹配:rqst_content, finish_result, send_opinion, rqst_title + - 支持精确匹配:t_status, t_type, is_leaf, is_secret + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'serial_num': 'asc'} + :key str serial_num: 精确匹配工单编号 + :key str row_guid: 精确匹配rowguid + :key str c_guid: 精确匹配查询详情使用guid + :key str pvi_guid: 精确匹配查询详情使用pviguid + :key str t_status: 精确匹配任务单状态 + :key str t_type: 精确匹配工单类型 + :key str is_leaf: 精确匹配是否叶子节点 + :key str is_secret: 精确匹配是否保密 + :key str rqst_content: 模糊匹配诉求内容 + :key str finish_result: 模糊匹配办结结果 + :key str send_opinion: 模糊匹配派送意见 + :key str rqst_title: 模糊匹配标题 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.rqst_content.key: '%{}%', + cls.finish_result.key: '%{}%', + cls.send_opinion.key: '%{}%', + cls.rqst_title.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.serial_num, cls.id) + + _task_df = await cls.query_as_df(_data_query) + if not _task_df.empty: + _task_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _task_df[cls.id.key] = _task_df[cls.id.key].astype(str) + + return _task_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单数据,返回分页格式数据。 + """ + _task_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _task_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_serial_num(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 serial_num 字段判断。 + + :param data_df: 输入的数据框架,必须包含 serial_num 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 serial_num 列表(去重) + serial_nums = data_df[cls.serial_num.key].unique().tolist() + if not serial_nums: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 serial_num + _query = select(cls.id, cls.serial_num).where(cls.serial_num.in_(serial_nums)) + serial_nums_df = await cls.query_as_df(_query) + + if serial_nums_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 serial_num -> id 的映射字典 + serial_num_to_id_map = dict(zip(serial_nums_df[cls.serial_num.key], serial_nums_df[cls.id.key])) + + # 根据 serial_num 是否在数据库中,划分数据 + mask_exists = data_df[cls.serial_num.key].isin(serial_nums_df[cls.serial_num.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.serial_num.key].map(serial_num_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTask(GovcTaskBase): + """ + 市12345工单模型类(主业务类,完全继承 TD3iGovcTask 字段)。 + + --- + description: 市12345工单主表接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + evl_result: + description: 结果满意度 + type: string + example: "满意" + maxLength: 64 + finish_result: + description: 办结结果 + type: string + example: "已完成诉求处理,用户反馈满意" + maxLength: 65535 + serial_num: + description: 工单编号 + type: string + example: "GOV20240501001" + maxLength: 64 + t_status: + description: 任务单状态 + type: string + example: "已办结" + maxLength: 64 + accord_type: + description: 归口类型 + type: string + example: "城市管理" + maxLength: 255 + create_date: + description: 交办时间 + type: string + format: date-time + example: "2024-01-15 10:30:00" + back_time_bf: + description: 拒绝时限 + type: string + format: date-time + example: "2024-01-20 18:00:00" + sub_handle_ou_name: + description: 子处办单位 + type: string + example: "XX街道办事处" + maxLength: 255 + sign_time_bf: + description: 签收时限时间戳 + type: integer + example: 1705324800000 + is_leaf: + description: 是否叶子节点 + type: string + example: "1" + maxLength: 32 + row_guid: + description: rowguid + type: string + example: "8f9e7d6c-5b4a-3210-9876-abcdef123456" + maxLength: 64 + c_guid: + description: 查询详情使用guid + type: string + example: "7e8d9c0b-1a2b-3c4d-5e6f-7890abcdef12" + maxLength: 64 + finish_time: + description: 办结时间戳 + type: integer + example: 1705843200000 + sign_time: + description: 签收时间戳 + type: integer + example: 1705411200000 + is_secret: + description: 是否保密 + type: string + example: "0" + maxLength: 32 + finish_time_bf: + description: 办结时限 + type: string + format: date-time + example: "2024-01-25 18:00:00" + link_number: + description: 联系号码 + type: string + example: "13800138000" + maxLength: 64 + pvi_guid: + description: 查询详情使用pviguid + type: string + example: "9d8c7b6a-5f4e-3d2c-1b0a-9876543210fe" + maxLength: 64 + rqst_type: + description: 诉求类型 + type: string + example: "投诉" + maxLength: 64 + rqst_content: + description: 诉求内容 + type: string + example: "XX小区垃圾堆积未及时清理,影响居民生活" + maxLength: 65535 + handle_ou_name: + description: 处办单位 + type: string + example: "XX区城市管理局" + maxLength: 255 + rqst_title: + description: 标题 + type: string + example: "XX小区垃圾清理问题" + maxLength: 500 + sign_person: + description: 签收人 + type: string + example: "张三" + maxLength: 128 + rqst_person: + description: 诉求人 + type: string + example: "李四" + maxLength: 128 + rqs_channel: + description: 渠道来源 + type: string + example: "12345热线" + maxLength: 64 + t_type: + description: 工单类型 + type: string + example: "民生类" + maxLength: 64 + solve_situation: + description: 解决情况 + type: string + example: "已解决" + maxLength: 64 + evl_style: + description: 态度满意度 + type: string + example: "满意" + maxLength: 64 + send_opinion: + description: 派送意见 + type: string + example: "请XX街道办事处尽快处理" + maxLength: 65535 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单记录。 + + 业务流程: + 1. 使用 GovcTaskForm 验证表单数据完整性 + 2. 检查是否已存在相同 serial_num 的记录(避免重复提交) + 3. 创建新工单对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 工单参数字典 + :return: 新建工单对象 + :rtype: GovcTask + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 serial_num 的记录 + _existing = await cls.is_exist(_form.serial_num.data) + assert _existing is None, "该工单编号已存在记录,不能重复提交。" + + # 创建对象 + _task = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _task.created_by = user.username + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def delete(cls, task_id: Union[str, int]): + """ + 删除工单记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param task_id: 要删除的工单记录ID + :return: 删除的记录对象 + :rtype: GovcTask + :raises AssertionError: 当记录不存在时抛出 + """ + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f"根据 ID {task_id} 未找到工单记录。" + + _del_query = delete(cls).where(cls.id == _task.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单记录(工单编号:{_task.serial_num},ID:{_task.id}).') + return _task + + @classmethod + async def modify(cls, task_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单记录。 + + 业务流程: + 1. 将 task_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param task_id: 要修改的工单记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的工单对象 + :rtype: GovcTask + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f'查无此工单信息。' + + # 更新字段 + _task.copy_from_dict(_form.data, skip_none=True).before_save() + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单记录(传入数据应为全新记录)。 + + :param data_df: 包含工单数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + tasks = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(tasks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(tasks)} 条工单记录。") + return len(tasks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单记录。 + + :param data_df: 包含工单数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTask.exists_serial_num(data_df) + # 保存到数据库 + _created_count = await GovcTask.create_batch(_latest_df, user) + _updated_count = await GovcTask.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govc_task_attachment.py b/models/govc_task_attachment.py new file mode 100644 index 0000000..bac7cf7 --- /dev/null +++ b/models/govc_task_attachment.py @@ -0,0 +1,527 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField, TextAreaField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskAttachment +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskAttachmentForm(ModelForm): + """ + 工单附件表单验证类(完全映射 TD3iGovcTaskAttachment 字段)。 + + 用于验证和处理市12345工单附件的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_attachment 的字段结构。 + """ + + # 基础信息 + id = IntegerField('主键ID') + task_id = IntegerField('关联工单主表ID', validators=[], # 外键非空由数据库约束,此处可补充自定义验证 + description='关联工单主表ID(t_d3i_govc_task.id)') + detail_id = IntegerField('关联工单详情ID', validators=[], + description='关联工单详情ID(t_d3i_govc_task_detail.id)') + name = StringField('附件名称', validators=[Length(max=500, message='附件名称长度不能超过500字符')]) + attach_url = TextAreaField('附件地址', validators=[Length(max=65535, message='附件地址长度不能超过65535字符')]) + type = StringField('附件类型', validators=[Length(max=64, message='附件类型长度不能超过64字符')]) + created_at = StringField('创建时间', render_kw={'readonly': True}) + created_by = StringField('创建者', validators=[Length(max=64, message='创建者长度不能超过64字符')]) + updated_at = StringField('更新时间', render_kw={'readonly': True}) + updated_by = StringField('更新者', validators=[Length(max=64, message='更新者长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskAttachmentBase(TD3iGovcTaskAttachment, CommonModel): + """ + 工单附件基础类(完全映射 TD3iGovcTaskAttachment 字段)。 + + 继承自数据库模型 TD3iGovcTaskAttachment 和通用模型 CommonModel。 + 封装所有与工单附件相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'detail_id': 'detail_id', + 'name': 'name', + 'attach_url': 'attach_url', + 'type': 'type', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单附件数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int, detail_id: int, name: str): + """ + 检查工单附件记录是否已存在(根据工单ID+详情ID+附件名称)。 + + :param task_id: 关联工单主表ID + :param detail_id: 关联工单详情ID + :param name: 附件名称 + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where( + cls.task_id == task_id, + cls.detail_id == detail_id, + cls.name == name + ) + _attachment: cls = await cls.query_first(_query) + return _attachment + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单附件数据的基础方法。 + + 支持字段: + - 精确匹配:task_id, detail_id, type, created_by, updated_by + - 模糊匹配:name, attach_url + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'name': 'asc'} + :key int task_id: 精确匹配关联工单主表ID + :key int detail_id: 精确匹配关联工单详情ID + :key str name: 模糊匹配附件名称 + :key str attach_url: 模糊匹配附件地址 + :key str type: 精确匹配附件类型 + :key str created_by: 精确匹配创建者 + :key str updated_by: 精确匹配更新者 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.name.key: '%{}%', + cls.attach_url.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.detail_id, cls.name) + + _attachment_df = await cls.query_as_df(_data_query) + if not _attachment_df.empty: + _attachment_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _attachment_df[cls.id.key] = _attachment_df[cls.id.key].astype(str) + _attachment_df[cls.task_id.key] = _attachment_df[cls.task_id.key].astype(str) + _attachment_df[cls.detail_id.key] = _attachment_df[cls.detail_id.key].astype(str) + + return _attachment_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单附件数据,返回分页格式数据。 + """ + _attachment_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _attachment_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_by_unique_key(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。 + 根据 task_id + detail_id + name 组合判断唯一性。 + + :param data_df: 输入的数据框架,必须包含 task_id、detail_id、name 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录(补充id字段) + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 校验必要列 + required_cols = ['task_id', 'detail_id', 'name'] + missing_cols = [col for col in required_cols if col not in data_df.columns] + if missing_cols: + echo_log(f"错误:exists_by_unique_key 要求输入数据必须包含 {missing_cols} 列") + return pd.DataFrame(), data_df.copy() + + # 去重并构建查询条件 + unique_keys = data_df[required_cols].drop_duplicates() + if unique_keys.empty: + return pd.DataFrame(), data_df.copy() + + # 构建批量查询条件 + _query_conditions = [] + for _, row in unique_keys.iterrows(): + _query_conditions.append( + (cls.task_id == row['task_id']) & + (cls.detail_id == row['detail_id']) & + (cls.name == row['name']) + ) + + if not _query_conditions: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.task_id, cls.detail_id, cls.name).where( + * _query_conditions + ) + exist_keys_df = await cls.query_as_df(_query) + + if exist_keys_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建唯一键映射(task_id|detail_id|name -> id) + exist_keys_df['unique_key'] = exist_keys_df.apply( + lambda x: f"{x['task_id']}|{x['detail_id']}|{x['name']}", axis=1 + ) + data_df['unique_key'] = data_df.apply( + lambda x: f"{x['task_id']}|{x['detail_id']}|{x['name']}", axis=1 + ) + key_to_id_map = dict(zip(exist_keys_df['unique_key'], exist_keys_df['id'])) + + # 划分存在/不存在的记录 + mask_exists = data_df['unique_key'].isin(exist_keys_df['unique_key']) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df['unique_key'].map(key_to_id_map) + latest_df = data_df[~mask_exists].copy() + + # 清理临时列 + for df in [exists_df, latest_df]: + if 'unique_key' in df.columns: + df.drop('unique_key', axis=1, inplace=True) + + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskAttachment(GovcTaskAttachmentBase): + """ + 工单附件模型类(主业务类,完全继承 TD3iGovcTaskAttachment 字段)。 + + --- + description: 市12345工单附件接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 5001 + required: true + detail_id: + description: 关联工单详情ID + type: integer + example: 6001 + required: true + name: + description: 附件名称 + type: string + example: "现场照片.jpg" + maxLength: 500 + required: true + attach_url: + description: 附件地址 + type: string + example: "/uploads/2024/05/现场照片.jpg" + maxLength: 65535 + required: true + type: + description: 附件类型 + type: string + example: "image/jpeg" + maxLength: 64 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + maxLength: 64 + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + maxLength: 64 + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单附件记录。 + + 业务流程: + 1. 使用 GovcTaskAttachmentForm 验证表单数据完整性 + 2. 检查是否已存在相同 (task_id+detail_id+name) 的记录(避免重复提交) + 3. 创建新附件对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 附件参数字典 + :return: 新建附件对象 + :rtype: GovcTaskAttachment + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskAttachmentForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同唯一键的记录 + _existing = await cls.is_exist( + task_id=_form.task_id.data, + detail_id=_form.detail_id.data, + name=_form.name.data + ) + assert _existing is None, "该工单下已存在同名附件,不能重复提交。" + + # 创建对象 + _attachment = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _attachment.created_by = user.username + _attachment.updated_by = user.username + await _attachment.async_save() + return _attachment + + @classmethod + async def delete(cls, attachment_id: Union[str, int]): + """ + 删除工单附件记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param attachment_id: 要删除的附件记录ID + :return: 删除的记录对象 + :rtype: GovcTaskAttachment + :raises AssertionError: 当记录不存在时抛出 + """ + _attachment: cls = await cls.async_find_by_id(attachment_id) + assert _attachment, f"根据 ID {attachment_id} 未找到工单附件记录。" + + _del_query = delete(cls).where(cls.id == _attachment.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log( + f'已删除工单附件记录(工单ID:{_attachment.task_id},附件名称:{_attachment.name},ID:{_attachment.id}).') + return _attachment + + @classmethod + async def modify(cls, attachment_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单附件记录。 + + 业务流程: + 1. 处理字符串字段去除首尾空格 + 2. 使用 GovcTaskAttachmentForm 验证表单数据 + 3. 查询原记录 + 4. 验证存在性 + 5. 更新字段并设置更新者 + 6. 保存到数据库 + 7. 返回更新后的对象 + + :param attachment_id: 要修改的附件记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的附件对象 + :rtype: GovcTaskAttachment + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskAttachmentForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _attachment: cls = await cls.async_find_by_id(attachment_id) + assert _attachment, f'查无此工单附件信息。' + + # 更新字段 + _attachment.copy_from_dict(_form.data, skip_none=True).before_save() + _attachment.updated_by = user.username if user else _attachment.updated_by + await _attachment.async_save() + return _attachment + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单附件记录(传入数据应为全新记录)。 + + :param data_df: 包含附件数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 补充创建者/更新者信息 + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + # 数据预处理(去空格) + str_cols = ['name', 'attach_url', 'type', 'created_by', 'updated_by'] + for col in str_cols: + if col in data_df.columns: + data_df[col] = data_df[col].apply(lambda x: x.strip() if isinstance(x, str) else x) + + records = data_df.to_dict('records') + attachments = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(attachments) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(attachments)} 条工单附件记录。") + return len(attachments) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单附件记录。 + + :param data_df: 包含附件数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 数据预处理(去空格) + str_cols = ['name', 'attach_url', 'type', 'updated_by'] + for col in str_cols: + if col in data_df.columns: + data_df[col] = data_df[col].apply(lambda x: x.strip() if isinstance(x, str) else x) + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings 批量更新 + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单附件记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单附件数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架(需包含 task_id、detail_id、name 列) + :param user: 操作用户对象 + :return: (created_count, updated_count) 新建和更新的数量 + """ + # 筛选数据状态(按唯一键判断存在性) + _exists_df, _latest_df = await cls.exists_by_unique_key(data_df) + # 批量创建/更新 + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_contact.py b/models/govc_task_contact.py new file mode 100644 index 0000000..c41b028 --- /dev/null +++ b/models/govc_task_contact.py @@ -0,0 +1,498 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete, BIGINT, String, DateTime, Text, ForeignKey, text +from sqlalchemy.orm import relationship, Mapped, mapped_column +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.db_models import TD3iGovcTaskContact +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm +from models.common_model import CommonModel + + +# 表单验证类 +class GovcTaskContactForm(ModelForm): + """ + 工单联系信息表单验证类(完全映射 TD3iGovcTaskContact 字段)。 + + 用于验证和处理市12345工单联系信息的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_contact 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 非空在数据库层约束 + link_person = StringField('联系人', validators=[Length(max=128, message='联系人长度不能超过128字符')]) + link_status = StringField('联系类型', validators=[Length(max=64, message='联系类型长度不能超过64字符')]) + link_date = DateTimeField('联系时间', validators=[Optional()], format='%Y-%m-%d %H:%M:%S') + link_content = TextAreaField('联系内容') # Text类型无长度限制 + created_at = DateTimeField('创建时间', validators=[Optional()], format='%Y-%m-%d %H:%M:%S') + created_by = StringField('创建者', validators=[Length(max=64, message='创建者长度不能超过64字符')]) + updated_at = DateTimeField('更新时间', validators=[Optional()], format='%Y-%m-%d %H:%M:%S') + updated_by = StringField('更新者', validators=[Length(max=64, message='更新者长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +# 基础业务类 +class GovcTaskContactBase(TD3iGovcTaskContact, CommonModel): + """ + 工单联系信息基础类(完全映射 TD3iGovcTaskContact 字段)。 + + 继承自数据库模型 TD3iGovcTaskContact 和通用模型 CommonModel。 + 封装所有与工单联系信息相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'link_person': 'link_person', + 'link_status': 'link_status', + 'link_date': 'link_date', + 'link_content': 'link_content', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单联系信息字段映射 + """ + + @classmethod + async def is_exist(cls, task_id: int, link_person: str = None, link_date: datetime.datetime = None): + """ + 检查工单联系记录是否已存在(根据工单ID+联系人+联系时间组合判断)。 + + :param task_id: 关联工单主表ID + :param link_person: 联系人(可选) + :param link_date: 联系时间(可选) + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + if link_person: + _query = _query.where(cls.link_person == link_person) + if link_date: + _query = _query.where(cls.link_date == link_date) + _contact: cls = await cls.query_first(_query) + return _contact + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单联系信息的基础方法。 + + 支持字段: + - 精确匹配:task_id, link_person, link_status + - 模糊匹配:link_content + - 时间范围:link_date (支持 link_date_start/link_date_end) + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配工单ID + :key str link_person: 精确匹配联系人 + :key str link_status: 精确匹配联系类型 + :key str link_content: 模糊匹配联系内容 + :key str link_date_start: 联系时间起始(格式:%Y-%m-%d %H:%M:%S) + :key str link_date_end: 联系时间结束(格式:%Y-%m-%d %H:%M:%S) + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.link_content.key: '%{}%', + } + + # 基础查询条件 + _wheres = cls.search_wheres(likes=_name_likes, **kwargs) + + # 时间范围条件 + if kwargs.get('link_date_start'): + _link_date_start = datetime.datetime.strptime(kwargs['link_date_start'], '%Y-%m-%d %H:%M:%S') + _wheres.append(cls.link_date >= _link_date_start) + if kwargs.get('link_date_end'): + _link_date_end = datetime.datetime.strptime(kwargs['link_date_end'], '%Y-%m-%d %H:%M:%S') + _wheres.append(cls.link_date <= _link_date_end) + + _query = select(cls).where(*_wheres).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + # 排序处理 + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id.desc(), cls.link_date.desc()) + + _contact_df = await cls.query_as_df(_data_query) + if not _contact_df.empty: + _contact_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _contact_df[cls.id.key] = _contact_df[cls.id.key].astype(str) + # 时间字段格式化 + for dt_field in ['link_date', 'created_at', 'updated_at']: + if dt_field in _contact_df.columns: + _contact_df[dt_field] = _contact_df[dt_field].dt.strftime('%Y-%m-%d %H:%M:%S') + + return _contact_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单联系信息,返回分页格式数据。 + """ + _contact_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count if _paging else len(_contact_df), + 'rows': _contact_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number if _paging else 1, + 'page_count': _paging.page_count if _paging else 1, + 'page_size': _paging.page_size if _paging else len(_contact_df), + }, + } + + @classmethod + async def exists_by_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id+link_person+link_date 组合判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 检查必要列 + required_cols = ['task_id', 'link_person', 'link_date'] + missing_cols = [col for col in required_cols if col not in data_df.columns] + if missing_cols: + echo_log(f"警告:exists_by_task_id 缺少必要列 {missing_cols},返回空数据") + return pd.DataFrame(), data_df.copy() + + # 格式化时间字段 + data_df['link_date'] = pd.to_datetime(data_df['link_date'], format='%Y-%m-%d %H:%M:%S', errors='coerce') + + # 构建查询条件 + _exists_records = [] + for _, row in data_df.iterrows(): + _contact = await cls.is_exist( + task_id=row['task_id'], + link_person=row['link_person'], + link_date=row['link_date'] + ) + if _contact: + row['id'] = _contact.id + _exists_records.append(row) + + # 划分数据 + exists_df = pd.DataFrame(_exists_records) if _exists_records else pd.DataFrame() + latest_df = data_df[~data_df.index.isin(exists_df.index)].copy() + + return exists_df, latest_df + + +# 主业务模型类(带Swagger文档) +@register_swagger_model +class GovcTaskContact(GovcTaskContactBase): + """ + 工单联系信息模型类(主业务类,完全继承 TD3iGovcTaskContact 字段)。 + + --- + description: 市12345工单联系信息接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 5001 + required: true + link_person: + description: 联系人 + type: string + example: "张三" + maxLength: 128 + link_status: + description: 联系类型 + type: string + example: "电话联系" + maxLength: 64 + link_date: + description: 联系时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-05-20 14:30:00" + link_content: + description: 联系内容 + type: string + example: "用户反馈问题已解决,确认无异议" + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-05-20 14:35:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + maxLength: 64 + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-05-21 09:15:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + maxLength: 64 + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单联系记录。 + + 业务流程: + 1. 使用 GovcTaskContactForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id+link_person+link_date 的记录(避免重复提交) + 3. 创建新联系对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 联系信息参数字典 + :return: 新建联系对象 + :rtype: GovcTaskContact + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskContactForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在重复记录 + _existing = await cls.is_exist( + task_id=_form.task_id.data, + link_person=_form.link_person.data, + link_date=_form.link_date.data + ) + assert _existing is None, "该工单的该联系人在该时间的联系记录已存在,不能重复提交。" + + # 创建对象 + _contact = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _contact.created_by = user.username + _contact.updated_by = user.username + await _contact.async_save() + return _contact + + @classmethod + async def delete(cls, contact_id: Union[str, int]): + """ + 删除工单联系记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param contact_id: 要删除的联系记录ID + :return: 删除的记录对象 + :rtype: GovcTaskContact + :raises AssertionError: 当记录不存在时抛出 + """ + _contact: cls = await cls.async_find_by_id(contact_id) + assert _contact, f"根据 ID {contact_id} 未找到工单联系记录。" + + _del_query = delete(cls).where(cls.id == _contact.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单联系记录(工单ID:{_contact.task_id},联系人:{_contact.link_person},ID:{_contact.id}).') + return _contact + + @classmethod + async def modify(cls, contact_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单联系记录。 + + 业务流程: + 1. 处理字符串字段去除首尾空格 + 2. 使用 GovcTaskContactForm 验证表单数据 + 3. 查询原记录 + 4. 验证存在性 + 5. 更新字段并设置更新者 + 6. 保存到数据库 + 7. 返回更新后的对象 + + :param contact_id: 要修改的联系记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的联系对象 + :rtype: GovcTaskContact + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskContactForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _contact: cls = await cls.async_find_by_id(contact_id) + assert _contact, f'查无此工单联系信息。' + + # 更新字段 + _contact.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _contact.updated_by = user.username + await _contact.async_save() + return _contact + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单联系记录(传入数据应为全新记录)。 + + :param data_df: 包含联系信息的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 设置创建者/更新者 + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + # 转换为模型对象 + records = data_df.to_dict('records') + contacts = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量保存 + session = cls.get_aio_session() + try: + session.add_all(contacts) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量创建成功:创建 {len(contacts)} 条工单联系记录。") + return len(contacts) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单联系记录。 + + :param data_df: 包含联系信息的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 设置更新时间和更新者 + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + # 批量更新 + update_data = data_df.to_dict('records') + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单联系记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单联系数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态(已存在/新增) + _exists_df, _latest_df = await cls.exists_by_task_id(data_df) + # 批量创建和更新 + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_delay.py b/models/govc_task_delay.py new file mode 100644 index 0000000..86ec633 --- /dev/null +++ b/models/govc_task_delay.py @@ -0,0 +1,477 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskDelay +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +# 表单验证类 +class GovcTaskDelayForm(ModelForm): + """ + 工单延迟信息表单验证类(完全映射 TD3iGovcTaskDelay 字段)。 + + 用于验证和处理市12345工单延迟信息的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_delay 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 外键非空,实际业务中需确保传入 + delay_status = StringField('审核状态', validators=[Length(max=64, message='审核状态长度不能超过64字符')]) + delay_num_unit = StringField('通过时长', validators=[Length(max=64, message='通过时长长度不能超过64字符')]) + delay_type = StringField('申请类型', validators=[Length(max=64, message='申请类型长度不能超过64字符')]) + delay_num = IntegerField('延迟时长') + apply_ou = StringField('申请部门', validators=[Length(max=255, message='申请部门长度不能超过255字符')]) + apply_time = DateTimeField('申请时间', validators=[Optional()], format='%Y-%m-%d %H:%M:%S') + status = IntegerField('提交状态') # 若业务需要可保留,无则删除 + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +# 基础业务类 +class GovcTaskDelayBase(TD3iGovcTaskDelay, CommonModel): + """ + 工单延迟信息基础类(完全映射 TD3iGovcTaskDelay 字段)。 + + 继承自数据库模型 TD3iGovcTaskDelay 和通用模型 CommonModel。 + 封装所有与工单延迟信息相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'delay_status': 'delay_status', + 'delay_num_unit': 'delay_num_unit', + 'delay_type': 'delay_type', + 'delay_num': 'delay_num', + 'apply_ou': 'apply_ou', + 'apply_time': 'apply_time', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单延迟数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查延迟记录是否已存在(根据工单ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _delay: cls = await cls.query_first(_query) + return _delay + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单延迟数据的基础方法。 + + 支持字段: + - task_id, delay_status, delay_type, delay_num_unit + - 支持模糊匹配:apply_ou + - 支持精确匹配:delay_num, apply_time + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配工单ID + :key str delay_status: 精确匹配审核状态 + :key str delay_type: 精确匹配申请类型 + :key str delay_num_unit: 精确匹配通过时长 + :key int delay_num: 精确匹配延迟时长 + :key str apply_ou: 模糊匹配申请部门 + :key datetime apply_time: 精确匹配申请时间 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.apply_ou.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + _delay_df = await cls.query_as_df(_data_query) + if not _delay_df.empty: + _delay_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _delay_df[cls.id.key] = _delay_df[cls.id.key].astype(str) + + return _delay_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单延迟数据,返回分页格式数据。 + """ + _delay_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _delay_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskDelay(GovcTaskDelayBase): + """ + 工单延迟信息模型类(主业务类,完全继承 TD3iGovcTaskDelay 字段)。 + + --- + description: 市12345工单延迟信息接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 2001 + required: true + delay_status: + description: 审核状态 + type: string + example: "审核通过" + maxLength: 64 + delay_num_unit: + description: 通过时长 + type: string + example: "小时" + maxLength: 64 + delay_type: + description: 申请类型 + type: string + example: "紧急延迟" + maxLength: 64 + delay_num: + description: 延迟时长 + type: integer + example: 24 + apply_ou: + description: 申请部门 + type: string + example: "城市管理局" + maxLength: 255 + apply_time: + description: 申请时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单延迟记录。 + + 业务流程: + 1. 使用 GovcTaskDelayForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新延迟对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 延迟参数字典 + :return: 新建延迟对象 + :rtype: GovcTaskDelay + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskDelayForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, f"工单ID {_form.task_id.data} 已存在延迟记录,不能重复提交。" + + # 创建对象 + _delay = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _delay.created_by = user.username + _delay.updated_by = user.username + await _delay.async_save() + return _delay + + @classmethod + async def delete(cls, delay_id: Union[str, int]): + """ + 删除工单延迟记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param delay_id: 要删除的延迟记录ID + :return: 删除的记录对象 + :rtype: GovcTaskDelay + :raises AssertionError: 当记录不存在时抛出 + """ + _delay: cls = await cls.async_find_by_id(delay_id) + assert _delay, f"根据 ID {delay_id} 未找到工单延迟记录。" + + _del_query = delete(cls).where(cls.id == _delay.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单延迟记录(工单ID:{_delay.task_id},ID:{_delay.id}).') + return _delay + + @classmethod + async def modify(cls, delay_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单延迟记录。 + + 业务流程: + 1. 将 delay_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskDelayForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param delay_id: 要修改的延迟记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的延迟对象 + :rtype: GovcTaskDelay + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskDelayForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _delay: cls = await cls.async_find_by_id(delay_id) + assert _delay, f'查无此工单延迟信息。' + + # 更新字段 + _delay.copy_from_dict(_form.data, skip_none=True).before_save() + _delay.updated_by = user.username + await _delay.async_save() + return _delay + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单延迟记录(传入数据应为全新记录)。 + + :param data_df: 包含延迟数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + delays = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(delays) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(delays)} 条工单延迟记录。") + return len(delays) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单延迟记录。 + + :param data_df: 包含延迟数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单延迟记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单延迟数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTaskDelay.exists_task_id(data_df) + # 保存到数据库 + _created_count = await GovcTaskDelay.create_batch(_latest_df, user) + _updated_count = await GovcTaskDelay.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_department_feedback.py b/models/govc_task_department_feedback.py new file mode 100644 index 0000000..f2751db --- /dev/null +++ b/models/govc_task_department_feedback.py @@ -0,0 +1,545 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskDepartmentFeedback +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskDeptFeedbackForm(ModelForm): + """ + 部门处置反馈表单验证类(完全映射 TD3iGovcTaskDepartmentFeedback 字段)。 + + 用于验证和处理市12345部门处置信息的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_department_feedback 的字段结构。 + """ + # 基础信息 + id = IntegerField('主键ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 实际应根据业务调整必填性 + zxhf_info = TextAreaField('专项回复信息') + back_info = TextAreaField('退回信息') + sign_time_bf = DateTimeField('签收时限', validators=[Optional()]) + operation_text = StringField('操作描述', validators=[Length(max=255, message='操作描述长度不能超过255字符')]) + opinion = TextAreaField('反馈意见') + unit = StringField('承办单位', validators=[Length(max=255, message='承办单位长度不能超过255字符')]) + finish_time_bf = DateTimeField('反馈时限', validators=[Optional()]) + person = StringField('承办人', validators=[Length(max=128, message='承办人长度不能超过128字符')]) + sign_time = DateTimeField('签收时间', validators=[Optional()]) + name = StringField('负责人', validators=[Length(max=128, message='负责人长度不能超过128字符')]) + tel = StringField('联系电话', validators=[Length(max=64, message='联系电话长度不能超过64字符')]) + time = DateTimeField('反馈时间', validators=[Optional()]) + department = StringField('部门', validators=[Length(max=255, message='部门长度不能超过255字符')]) + status = IntegerField('状态') + back_time_bf = DateTimeField('拒绝时限', validators=[Optional()]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskDeptFeedbackBase(TD3iGovcTaskDepartmentFeedback, CommonModel): + """ + 部门处置反馈基础类(完全映射 TD3iGovcTaskDepartmentFeedback 字段)。 + + 继承自数据库模型 TD3iGovcTaskDepartmentFeedback 和通用模型 CommonModel。 + 封装所有与部门处置反馈相关的通用操作方法。 + """ + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'zxhf_info': 'zxhfinfo', + 'back_info':'backinfo', + 'sign_time_bf': 'signtimebf', + 'operation_text': 'operationText', + 'opinion': 'opinion', + 'unit': 'unit', + 'finish_time_bf': 'finishtimebf', + 'person': 'person', + 'sign_time': 'signtime', + 'name': 'name', + 'tel': 'tel', + 'time': 'time', + 'department': 'department', + 'status': 'status', + 'back_time_bf': 'backtimebf', + } + """部门处置反馈数据映射""" + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查部门处置反馈记录是否已存在(根据工单ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _feedback: cls = await cls.query_first(_query) + return _feedback + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索部门处置反馈数据的基础方法。 + + 支持字段: + - task_id, status, unit, department(精确匹配) + - 支持模糊匹配:operation_text, opinion, person, name + - 支持时间范围:sign_time_bf, finish_time_bf, sign_time, time, back_time_bf + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配工单ID + :key int status: 精确匹配状态 + :key str unit: 精确匹配承办单位 + :key str department: 精确匹配部门 + :key str operation_text: 模糊匹配操作描述 + :key str opinion: 模糊匹配反馈意见 + :key str person: 模糊匹配承办人 + :key str name: 模糊匹配负责人 + :key str sign_time_bf_start: 签收时限开始时间 + :key str sign_time_bf_end: 签收时限结束时间 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.operation_text.key: '%{}%', + cls.opinion.key: '%{}%', + cls.person.key: '%{}%', + cls.name.key: '%{}%', + } + + # 构建基础查询 + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + # 处理时间范围查询(示例:签收时限) + if kwargs.get('sign_time_bf_start'): + _query = _query.where(cls.sign_time_bf >= kwargs['sign_time_bf_start']) + if kwargs.get('sign_time_bf_end'): + _query = _query.where(cls.sign_time_bf <= kwargs['sign_time_bf_end']) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + # 处理排序 + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + # 执行查询并处理结果 + _feedback_df = await cls.query_as_df(_data_query) + if not _feedback_df.empty: + _feedback_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _feedback_df[cls.id.key] = _feedback_df[cls.id.key].astype(str) + # 处理时间字段格式化 + datetime_fields = ['sign_time_bf', 'finish_time_bf', 'sign_time', 'time', 'back_time_bf', 'created_at', + 'updated_at'] + for field in datetime_fields: + if field in _feedback_df.columns: + _feedback_df[field] = _feedback_df[field].dt.strftime('%Y-%m-%d %H:%M:%S').fillna('') + + return _feedback_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索部门处置反馈数据,返回分页格式数据。 + """ + _feedback_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _feedback_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskDeptFeedback(GovcTaskDeptFeedbackBase): + """ + 部门处置反馈模型类(主业务类,完全继承 TD3iGovcTaskDepartmentFeedback 字段)。 + + --- + description: 市12345部门处置信息接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 20240501001 + zxhf_info: + description: 专项回复信息 + type: string + example: "该工单已完成处置,符合要求" + sign_time_bf: + description: 签收时限 + type: string + format: date-time + example: "2024-05-01 10:00:00" + operation_text: + description: 操作描述 + type: string + example: "接收工单并开始处置" + maxLength: 255 + opinion: + description: 反馈意见 + type: string + example: "经核查,该问题已妥善解决" + unit: + description: 承办单位 + type: string + example: "XX市城市管理局" + maxLength: 255 + finish_time_bf: + description: 反馈时限 + type: string + format: date-time + example: "2024-05-05 18:00:00" + person: + description: 承办人 + type: string + example: "张三" + maxLength: 128 + sign_time: + description: 签收时间 + type: string + format: date-time + example: "2024-05-01 10:10:00" + name: + description: 负责人 + type: string + example: "李四" + maxLength: 128 + tel: + description: 联系电话 + type: string + example: "13800138000" + maxLength: 64 + time: + description: 反馈时间 + type: string + format: date-time + example: "2024-05-05 17:30:00" + department: + description: 部门 + type: string + example: "市容管理科" + maxLength: 255 + status: + description: 状态(1:已签收,2:处置中,3:已反馈,4:已拒绝) + type: integer + example: 3 + back_time_bf: + description: 拒绝时限 + type: string + format: date-time + example: "2024-05-03 18:00:00" + created_at: + description: 创建时间 + type: string + format: date-time + example: "2024-05-01 10:00:00" + readOnly: true + created_by: + description: 创建者 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间 + type: string + format: date-time + example: "2024-05-05 17:30:00" + readOnly: true + updated_by: + description: 修改者 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的部门处置反馈记录。 + + 业务流程: + 1. 使用 GovcTaskDeptFeedbackForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新反馈对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 反馈参数字典 + :return: 新建反馈对象 + :rtype: GovcTaskDeptFeedback + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskDeptFeedbackForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + if _form.task_id.data: + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, f"工单ID {_form.task_id.data} 已存在处置反馈记录,不能重复提交。" + + # 创建对象 + _feedback = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _feedback.created_by = user.username + _feedback.updated_by = user.username + await _feedback.async_save() + return _feedback + + @classmethod + async def delete(cls, feedback_id: Union[str, int]): + """ + 删除部门处置反馈记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param feedback_id: 要删除的反馈记录ID + :return: 删除的记录对象 + :rtype: GovcTaskDeptFeedback + :raises AssertionError: 当记录不存在时抛出 + """ + _feedback: cls = await cls.async_find_by_id(feedback_id) + assert _feedback, f"根据 ID {feedback_id} 未找到部门处置反馈记录。" + + _del_query = delete(cls).where(cls.id == _feedback.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除部门处置反馈记录(工单ID:{_feedback.task_id},ID:{_feedback.id}).') + return _feedback + + @classmethod + async def modify(cls, feedback_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有部门处置反馈记录。 + + 业务流程: + 1. 处理字符串字段去除首尾空格 + 2. 使用 GovcTaskDeptFeedbackForm 验证表单数据 + 3. 查询原记录 + 4. 验证存在性 + 5. 更新字段并设置更新者 + 6. 保存到数据库 + 7. 返回更新后的对象 + + :param feedback_id: 要修改的反馈记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的反馈对象 + :rtype: GovcTaskDeptFeedback + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskDeptFeedbackForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _feedback: cls = await cls.async_find_by_id(feedback_id) + assert _feedback, f'查无此部门处置反馈信息。' + + # 更新字段 + _feedback.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _feedback.updated_by = user.username + await _feedback.async_save() + return _feedback + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建部门处置反馈记录(传入数据应为全新记录)。 + + :param data_df: 包含反馈数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 设置创建者/更新者信息 + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + # 转换为对象列表 + records = data_df.to_dict('records') + feedbacks = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + # 批量保存 + session = cls.get_aio_session() + try: + session.add_all(feedbacks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量创建成功:创建 {len(feedbacks)} 条部门处置反馈记录。") + return len(feedbacks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有部门处置反馈记录。 + + :param data_df: 包含反馈数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 设置更新信息 + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 批量更新 + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条部门处置反馈记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存部门处置反馈数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态(按task_id判断存在性) + _exists_df, _latest_df = await cls.exists_task_id(data_df) + # 批量创建和更新 + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govc_task_detail.py b/models/govc_task_detail.py new file mode 100644 index 0000000..dde538b --- /dev/null +++ b/models/govc_task_detail.py @@ -0,0 +1,626 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete, text +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskDetail +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskDetailForm(ModelForm): + """ + 市12345工单详情表单验证类(完全映射 TD3iGovcTaskDetail 字段)。 + + 用于验证和处理市12345工单详情的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_detail 的字段结构。 + """ + + # 基础信息 + id = IntegerField('主键ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) + note = TextAreaField('备注') + purpose = StringField('诉求目的', validators=[Length(max=255, message='诉求目的长度不能超过255字符')]) + type_level = StringField('诉求类型等级', validators=[Length(max=64, message='诉求类型等级长度不能超过64字符')]) + type = StringField('诉求类型', validators=[Length(max=64, message='诉求类型长度不能超过64字符')]) + sign_time_bf = DateTimeField('签收时限', validators=[Optional()]) + matter = StringField('窗口进驻事项', validators=[Length(max=32, message='窗口进驻事项长度不能超过32字符')]) + case_form_type = StringField('个性化表单类型', validators=[Length(max=64, message='个性化表单类型长度不能超过64字符')]) + content = TextAreaField('诉求内容') + handle_ou = StringField('处办单位', validators=[Length(max=255, message='处办单位长度不能超过255字符')]) + urgency = IntegerField('是否紧急', validators=[Optional()]) + sj_handle_ou = StringField('涉及单位', validators=[Length(max=255, message='涉及单位长度不能超过255字符')]) + ccb_content = TextAreaField('催补撤内容') + is_secret = StringField('是否保密', validators=[Length(max=32, message='是否保密长度不能超过32字符')]) + theme = StringField('主题工单', validators=[Length(max=32, message='主题工单长度不能超过32字符')]) + attribute = StringField('归口类型', validators=[Length(max=255, message='归口类型长度不能超过255字符')]) + zqt = StringField('企业名称', validators=[Length(max=255, message='企业名称长度不能超过255字符')]) + address = StringField('详细地址', validators=[Length(max=500, message='详细地址长度不能超过500字符')]) + seng_again_num = IntegerField('再交办次数', validators=[Optional()]) + epidemic = StringField('是否疫情工单', validators=[Length(max=32, message='是否疫情工单长度不能超过32字符')]) + has_ccb = IntegerField('是否有催补撤信息', validators=[Optional()]) + way = StringField('受理方式', validators=[Length(max=64, message='受理方式长度不能超过64字符')]) + return_visit = StringField('回访类型', validators=[Length(max=64, message='回访类型长度不能超过64字符')]) + finish_time_bf = DateTimeField('反馈时限', validators=[Optional()]) + is_email = IntegerField('是否邮箱提交', validators=[Optional()]) + time = DateTimeField('事发时间', validators=[Optional()]) + called_tx = StringField('被叫号码', validators=[Length(max=64, message='被叫号码长度不能超过64字符')]) + back_time_bf = DateTimeField('拒绝时限', validators=[Optional()]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskDetailBase(TD3iGovcTaskDetail, CommonModel): + """ + 市12345工单详情基础类(完全映射 TD3iGovcTaskDetail 字段)。 + + 继承自数据库模型 TD3iGovcTaskDetail 和通用模型 CommonModel。 + 封装所有与工单详情相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'note': 'note', + 'purpose': 'purpose', + 'type_level': 'type_level', + 'type': 'type', + 'sign_time_bf': 'sign_time_bf', + 'matter': 'matter', + 'case_form_type': 'case_form_type', + 'content': 'content', + 'handle_ou': 'handle_ou', + 'urgency': 'urgency', + 'sj_handle_ou': 'sj_handle_ou', + 'ccb_content': 'ccb_content', + 'is_secret': 'is_secret', + 'theme': 'theme', + 'attribute': 'attribute', + 'zqt': 'zqt', + 'address': 'address', + 'seng_again_num': 'seng_again_num', + 'epidemic': 'epidemic', + 'has_ccb': 'has_ccb', + 'way': 'way', + 'return_visit': 'return_visit', + 'finish_time_bf': 'finish_time_bf', + 'is_email': 'is_email', + 'time': 'time', + 'called_tx': 'called_tx', + 'back_time_bf': 'back_time_bf', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单详情数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查工单详情记录是否已存在(根据关联工单主表ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _detail: cls = await cls.query_first(_query) + return _detail + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单详情数据的基础方法。 + + 支持字段: + - task_id, type_level, type, urgency, epidemic, has_ccb, way, return_visit, is_email + - 支持模糊匹配:note, purpose, content, handle_ou, sj_handle_ou, ccb_content, address + - 支持精确匹配:matter, case_form_type, is_secret, theme, attribute, zqt, called_tx + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配关联工单主表ID + :key str type_level: 精确匹配诉求类型等级 + :key str type: 精确匹配诉求类型 + :key int urgency: 精确匹配是否紧急 + :key str epidemic: 精确匹配是否疫情工单 + :key int has_ccb: 精确匹配是否有催补撤信息 + :key str way: 精确匹配受理方式 + :key str return_visit: 精确匹配回访类型 + :key int is_email: 精确匹配是否邮箱提交 + :key str note: 模糊匹配备注 + :key str purpose: 模糊匹配诉求目的 + :key str content: 模糊匹配诉求内容 + :key str handle_ou: 模糊匹配处办单位 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.note.key: '%{}%', + cls.purpose.key: '%{}%', + cls.content.key: '%{}%', + cls.handle_ou.key: '%{}%', + cls.sj_handle_ou.key: '%{}%', + cls.ccb_content.key: '%{}%', + cls.address.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + _detail_df = await cls.query_as_df(_data_query) + if not _detail_df.empty: + _detail_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _detail_df[cls.id.key] = _detail_df[cls.id.key].astype(str) + + return _detail_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单详情数据,返回分页格式数据。 + """ + _detail_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _detail_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskDetail(GovcTaskDetailBase): + """ + 市12345工单详情模型类(主业务类,完全继承 TD3iGovcTaskDetail 字段)。 + + --- + description: 市12345工单详情接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 2001 + note: + description: 备注 + type: string + example: "该工单需加急处理" + purpose: + description: 诉求目的 + type: string + example: "投诉小区物业不作为" + maxLength: 255 + type_level: + description: 诉求类型等级 + type: string + example: "一级诉求" + maxLength: 64 + type: + description: 诉求类型 + type: string + example: "投诉" + maxLength: 64 + sign_time_bf: + description: 签收时限 + type: string + format: date-time + example: "2024-01-15 10:30:00" + matter: + description: 窗口进驻事项 + type: string + example: "民生服务" + maxLength: 32 + case_form_type: + description: 个性化表单类型 + type: string + example: "通用投诉表单" + maxLength: 64 + content: + description: 诉求内容 + type: string + example: "小区垃圾堆积无人清理,物业未及时处理" + handle_ou: + description: 处办单位 + type: string + example: "XX街道办事处" + maxLength: 255 + urgency: + description: 是否紧急(1:紧急,0:不紧急) + type: integer + example: 1 + sj_handle_ou: + description: 涉及单位 + type: string + example: "XX物业公司,XX社区" + maxLength: 255 + ccb_content: + description: 催补撤内容 + type: string + example: "请尽快补充工单相关证明材料" + is_secret: + description: 是否保密 + type: string + example: "0" + maxLength: 32 + theme: + description: 主题工单 + type: string + example: "民生保障" + maxLength: 32 + attribute: + description: 归口类型 + type: string + example: "城乡建设" + maxLength: 255 + zqt: + description: 企业名称 + type: string + example: "XX物业服务有限公司" + maxLength: 255 + address: + description: 详细地址 + type: string + example: "XX市XX区XX街道XX小区1号楼" + maxLength: 500 + seng_again_num: + description: 再交办次数 + type: integer + example: 2 + epidemic: + description: 是否疫情工单 + type: string + example: "0" + maxLength: 32 + has_ccb: + description: 是否有催补撤信息(1:有,0:无) + type: integer + example: 1 + way: + description: 受理方式 + type: string + example: "电话受理" + maxLength: 64 + return_visit: + description: 回访类型 + type: string + example: "电话回访" + maxLength: 64 + finish_time_bf: + description: 反馈时限 + type: string + format: date-time + example: "2024-01-20 17:00:00" + is_email: + description: 是否邮箱提交(1:是,0:否) + type: integer + example: 0 + time: + description: 事发时间 + type: string + format: date-time + example: "2024-01-14 09:15:00" + called_tx: + description: 被叫号码 + type: string + example: "021-12345678" + maxLength: 64 + back_time_bf: + description: 拒绝时限 + type: string + format: date-time + example: "2024-01-16 12:00:00" + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单详情记录。 + + 业务流程: + 1. 使用 GovcTaskDetailForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新工单详情对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 工单详情参数字典 + :return: 新建工单详情对象 + :rtype: GovcTaskDetail + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskDetailForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, "该工单已存在详情记录,不能重复提交。" + + # 创建对象 + _detail = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _detail.created_by = user.username + _detail.updated_by = user.username + await _detail.async_save() + return _detail + + @classmethod + async def delete(cls, detail_id: Union[str, int]): + """ + 删除工单详情记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param detail_id: 要删除的工单详情记录ID + :return: 删除的记录对象 + :rtype: GovcTaskDetail + :raises AssertionError: 当记录不存在时抛出 + """ + _detail: cls = await cls.async_find_by_id(detail_id) + assert _detail, f"根据 ID {detail_id} 未找到工单详情记录。" + + _del_query = delete(cls).where(cls.id == _detail.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单详情记录(工单ID:{_detail.task_id},ID:{_detail.id}).') + return _detail + + @classmethod + async def modify(cls, detail_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单详情记录。 + + 业务流程: + 1. 将 detail_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskDetailForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param detail_id: 要修改的工单详情记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的工单详情对象 + :rtype: GovcTaskDetail + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskDetailForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _detail: cls = await cls.async_find_by_id(detail_id) + assert _detail, f'查无此工单详情信息。' + + # 更新字段 + _detail.copy_from_dict(_form.data, skip_none=True).before_save() + _detail.updated_by = user.username + await _detail.async_save() + return _detail + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单详情记录(传入数据应为全新记录)。 + + :param data_df: 包含工单详情数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + details = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(details) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(details)} 条工单详情记录。") + return len(details) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单详情记录。 + + :param data_df: 包含工单详情数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单详情记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单详情数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTaskDetail.exists_task_id(data_df) + # 保存到数据库 + _created_count = await GovcTaskDetail.create_batch(_latest_df, user) + _updated_count = await GovcTaskDetail.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_finish.py b/models/govc_task_finish.py new file mode 100644 index 0000000..d78f593 --- /dev/null +++ b/models/govc_task_finish.py @@ -0,0 +1,493 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskFinish +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskFinishForm(ModelForm): + """ + 工单办结信息表单验证类(完全映射 TD3iGovcTaskFinish 字段)。 + + 用于验证和处理市12345工单办结信息的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_finish 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 外键,非空由数据库约束 + bj_result = TextAreaField('办结意见', validators=[Length(max=65535, message='办结意见长度不能超过65535字符')]) + evl_result = StringField('结果满意度', validators=[Length(max=64, message='结果满意度长度不能超过64字符')]) + replay_person = StringField('回访人', validators=[Length(max=128, message='回访人长度不能超过128字符')]) + processing_results = StringField('处理结果', validators=[Length(max=255, message='处理结果长度不能超过255字符')]) + solve_situation = StringField('解决情况', validators=[Length(max=64, message='解决情况长度不能超过64字符')]) + replay_time = DateTimeField('回访时间', validators=[Optional()]) + evl_style = StringField('态度满意度', validators=[Length(max=64, message='态度满意度长度不能超过64字符')]) + is_citizen = IntegerField('是否市民', validators=[Optional()]) + status = IntegerField('提交状态') # 兼容通用状态字段,若有需要可调整 + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskFinishBase(TD3iGovcTaskFinish, CommonModel): + """ + 工单办结信息基础类(完全映射 TD3iGovcTaskFinish 字段)。 + + 继承自数据库模型 TD3iGovcTaskFinish 和通用模型 CommonModel。 + 封装所有与工单办结操作相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'bj_result': 'bj_result', + 'evl_result': 'evl_result', + 'replay_person': 'replay_person', + 'processing_results': 'processing_results', + 'solve_situation': 'solve_situation', + 'replay_time': 'replay_time', + 'evl_style': 'evl_style', + 'is_citizen': 'is_citizen', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单办结数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查工单办结记录是否已存在(根据关联工单主表ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _finish: cls = await cls.query_first(_query) + return _finish + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单办结数据的基础方法。 + + 支持字段: + - task_id, evl_result, replay_person, solve_situation, evl_style, is_citizen + - 支持模糊匹配:bj_result, processing_results + - 支持精确匹配:is_citizen, evl_result + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配关联工单主表ID + :key str evl_result: 精确匹配结果满意度 + :key str replay_person: 精确匹配回访人 + :key str solve_situation: 精确匹配解决情况 + :key str evl_style: 精确匹配态度满意度 + :key int is_citizen: 精确匹配是否市民 + :key str bj_result: 模糊匹配办结意见 + :key str processing_results: 模糊匹配处理结果 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.bj_result.key: '%{}%', + cls.processing_results.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + _finish_df = await cls.query_as_df(_data_query) + if not _finish_df.empty: + _finish_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _finish_df[cls.id.key] = _finish_df[cls.id.key].astype(str) + # 处理时间字段格式化 + if cls.replay_time.key in _finish_df.columns: + _finish_df[cls.replay_time.key] = _finish_df[cls.replay_time.key].dt.strftime('%Y-%m-%d %H:%M:%S') + + return _finish_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单办结数据,返回分页格式数据。 + """ + _finish_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _finish_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskFinish(GovcTaskFinishBase): + """ + 工单办结信息模型类(主业务类,完全继承 TD3iGovcTaskFinish 字段)。 + + --- + description: 市12345工单办结接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 2001 + bj_result: + description: 办结意见 + type: string + example: "经核实,该问题已妥善解决,市民表示满意。" + maxLength: 65535 + evl_result: + description: 结果满意度 + type: string + example: "满意" + maxLength: 64 + replay_person: + description: 回访人 + type: string + example: "张三" + maxLength: 128 + processing_results: + description: 处理结果 + type: string + example: "已协调相关部门完成整改,问题闭环。" + maxLength: 255 + solve_situation: + description: 解决情况 + type: string + example: "完全解决" + maxLength: 64 + replay_time: + description: 回访时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + evl_style: + description: 态度满意度 + type: string + example: "满意" + maxLength: 64 + is_citizen: + description: 是否市民(1:是,0:否) + type: integer + example: 1 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单办结记录。 + + 业务流程: + 1. 使用 GovcTaskFinishForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新办结对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 办结参数字典 + :return: 新建办结对象 + :rtype: GovcTaskFinish + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskFinishForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, f"工单ID {_form.task_id.data} 已存在办结记录,不能重复提交。" + + # 创建对象 + _finish = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _finish.created_by = user.username + _finish.updated_by = user.username + await _finish.async_save() + return _finish + + @classmethod + async def delete(cls, finish_id: Union[str, int]): + """ + 删除工单办结记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param finish_id: 要删除的办结记录ID + :return: 删除的记录对象 + :rtype: GovcTaskFinish + :raises AssertionError: 当记录不存在时抛出 + """ + _finish: cls = await cls.async_find_by_id(finish_id) + assert _finish, f"根据 ID {finish_id} 未找到工单办结记录。" + + _del_query = delete(cls).where(cls.id == _finish.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单办结记录(工单ID:{_finish.task_id},ID:{_finish.id}).') + return _finish + + @classmethod + async def modify(cls, finish_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单办结记录。 + + 业务流程: + 1. 将 finish_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskFinishForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param finish_id: 要修改的办结记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的办结对象 + :rtype: GovcTaskFinish + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskFinishForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _finish: cls = await cls.async_find_by_id(finish_id) + assert _finish, f'查无此工单办结信息。' + + # 更新字段 + _finish.copy_from_dict(_form.data, skip_none=True).before_save() + _finish.updated_by = user.username + await _finish.async_save() + return _finish + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单办结记录(传入数据应为全新记录)。 + + :param data_df: 包含办结数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + finishes = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(finishes) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(finishes)} 条工单办结记录。") + return len(finishes) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单办结记录。 + + :param data_df: 包含办结数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单办结记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单办结数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTaskFinish.exists_task_id(data_df) + # 保存到数据库 + _created_count = await GovcTaskFinish.create_batch(_latest_df, user) + _updated_count = await GovcTaskFinish.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_history.py b/models/govc_task_history.py new file mode 100644 index 0000000..fb34103 --- /dev/null +++ b/models/govc_task_history.py @@ -0,0 +1,469 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskHistory +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskHistoryForm(ModelForm): + """ + 历史工单表单验证类(完全映射 TD3iGovcTaskHistory 字段)。 + + 用于验证和处理市12345历史工单的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_history 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[], message='关联工单主表ID必须为整数') + history_date = StringField('日期', validators=[Length(max=32, message='日期长度不能超过32字符')]) + serial_num = StringField('历史工单号', validators=[Length(max=64, message='历史工单号长度不能超过64字符')]) + detail_url = StringField('详情页URL', validators=[Length(max=65535, message='详情页URL长度不能超过65535字符')]) + rqst_title = StringField('工单标题', validators=[Length(max=500, message='工单标题长度不能超过500字符')]) + state = StringField('状态', validators=[Length(max=64, message='状态长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskHistoryBase(TD3iGovcTaskHistory, CommonModel): + """ + 历史工单基础类(完全映射 TD3iGovcTaskHistory 字段)。 + + 继承自数据库模型 TD3iGovcTaskHistory 和通用模型 CommonModel。 + 封装所有与历史工单相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'history_date': 'history_date', + 'serial_num': 'serial_num', + 'detail_url': 'detail_url', + 'rqst_title': 'rqst_title', + 'state': 'state', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 历史工单数据映射 + """ + + @classmethod + async def is_exist(cls, serial_num: str): + """ + 检查历史工单记录是否已存在(根据历史工单号)。 + + :param serial_num: 历史工单号 + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.serial_num == serial_num) + _history: cls = await cls.query_first(_query) + return _history + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索历史工单数据的基础方法。 + + 支持字段: + - task_id, serial_num, state, history_date + - 支持模糊匹配:rqst_title, detail_url + - 支持精确匹配:task_id, state + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'serial_num': 'asc'} + :key int task_id: 精确匹配关联工单主表ID + :key str serial_num: 精确匹配历史工单号 + :key str history_date: 精确匹配日期 + :key str rqst_title: 模糊匹配工单标题 + :key str detail_url: 模糊匹配详情页URL + :key str state: 精确匹配状态 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.rqst_title.key: '%{}%', + cls.detail_url.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.serial_num, cls.task_id) + + _history_df = await cls.query_as_df(_data_query) + if not _history_df.empty: + _history_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _history_df[cls.id.key] = _history_df[cls.id.key].astype(str) + _history_df[cls.task_id.key] = _history_df[cls.task_id.key].astype(str) + + return _history_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索历史工单数据,返回分页格式数据。 + """ + _history_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _history_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_serial_num(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 serial_num 字段判断。 + + :param data_df: 输入的数据框架,必须包含 serial_num 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 serial_num 列表(去重) + serial_nums = data_df[cls.serial_num.key].unique().tolist() + if not serial_nums: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 serial_num + _query = select(cls.id, cls.serial_num).where(cls.serial_num.in_(serial_nums)) + serial_nums_df = await cls.query_as_df(_query) + + if serial_nums_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 serial_num -> id 的映射字典 + serial_num_to_id_map = dict(zip(serial_nums_df[cls.serial_num.key], serial_nums_df[cls.id.key])) + + # 根据 serial_num 是否在数据库中,划分数据 + mask_exists = data_df[cls.serial_num.key].isin(serial_nums_df[cls.serial_num.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.serial_num.key].map(serial_num_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskHistory(GovcTaskHistoryBase): + """ + 历史工单模型类(主业务类,完全继承 TD3iGovcTaskHistory 字段)。 + + --- + description: 市12345历史工单接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 5001 + maxLength: 20 + history_date: + description: 日期 + type: string + example: "2024-05-01" + maxLength: 32 + serial_num: + description: 历史工单号 + type: string + example: "HIST20240501001" + maxLength: 64 + detail_url: + description: 详情页URL + type: string + example: "http://12345.gov.cn/detail/1001" + maxLength: 65535 + rqst_title: + description: 工单标题 + type: string + example: "市民反映小区垃圾分类问题" + maxLength: 500 + state: + description: 状态 + type: string + example: "已办结" + maxLength: 64 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的历史工单记录。 + + 业务流程: + 1. 使用 GovcTaskHistoryForm 验证表单数据完整性 + 2. 检查是否已存在相同 serial_num 的记录(避免重复提交) + 3. 创建新历史工单对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 历史工单参数字典 + :return: 新建历史工单对象 + :rtype: GovcTaskHistory + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskHistoryForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 serial_num 的记录 + _existing = await cls.is_exist(_form.serial_num.data) + assert _existing is None, "该历史工单已存在,不能重复提交。" + + # 创建对象 + _history = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _history.created_by = user.username + _history.updated_by = user.username + await _history.async_save() + return _history + + @classmethod + async def delete(cls, history_id: Union[str, int]): + """ + 删除历史工单记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param history_id: 要删除的历史工单记录ID + :return: 删除的记录对象 + :rtype: GovcTaskHistory + :raises AssertionError: 当记录不存在时抛出 + """ + _history: cls = await cls.async_find_by_id(history_id) + assert _history, f"根据 ID {history_id} 未找到历史工单记录。" + + _del_query = delete(cls).where(cls.id == _history.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除历史工单记录(历史工单号:{_history.serial_num},ID:{_history.id}).') + return _history + + @classmethod + async def modify(cls, history_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有历史工单记录。 + + 业务流程: + 1. 将 history_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskHistoryForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param history_id: 要修改的历史工单记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的历史工单对象 + :rtype: GovcTaskHistory + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskHistoryForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _history: cls = await cls.async_find_by_id(history_id) + assert _history, f'查无此历史工单信息。' + + # 更新字段 + _history.copy_from_dict(_form.data, skip_none=True).before_save() + _history.updated_by = user.username + await _history.async_save() + return _history + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建历史工单记录(传入数据应为全新记录)。 + + :param data_df: 包含历史工单数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + histories = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(histories) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(histories)} 条历史工单记录。") + return len(histories) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有历史工单记录。 + + :param data_df: 包含历史工单数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条历史工单记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存历史工单数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTaskHistory.exists_serial_num(data_df) + # 保存到数据库 + _created_count = await GovcTaskHistory.create_batch(_latest_df, user) + _updated_count = await GovcTaskHistory.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_process.py b/models/govc_task_process.py new file mode 100644 index 0000000..eb64bfb --- /dev/null +++ b/models/govc_task_process.py @@ -0,0 +1,519 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskProces +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskProcessForm(ModelForm): + """ + 工单流程追踪表单验证类(完全映射 TD3iGovcTaskProcess 字段)。 + + 用于验证和处理市12345工单流程追踪的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_process 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) + handle_time = DateTimeField('办理时间', validators=[Optional()]) + operate_status = StringField('办理状态', validators=[Length(max=128, message='办理状态长度不能超过128字符')]) + activity_guid = StringField('办理环节名称', validators=[Length(max=255, message='办理环节名称长度不能超过255字符')]) + handle_opinion = TextAreaField('办理意见') + is_finish = IntegerField('是否结束') + operator_ou_name = StringField('部门', validators=[Length(max=255, message='部门长度不能超过255字符')]) + is_back = IntegerField('是否回退') + operator_name = StringField('办理人', validators=[Length(max=128, message='办理人长度不能超过128字符')]) + created_at = DateTimeField('创建时间', validators=[Optional()]) + created_by = StringField('创建者', validators=[Length(max=64, message='创建者长度不能超过64字符')]) + updated_at = DateTimeField('更新时间', validators=[Optional()]) + updated_by = StringField('更新者', validators=[Length(max=64, message='更新者长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskProcessBase(TD3iGovcTaskProces, CommonModel): + """ + 工单流程追踪基础类(完全映射 TD3iGovcTaskProcess 字段)。 + + 继承自数据库模型 TD3iGovcTaskProcess 和通用模型 CommonModel。 + 封装所有与工单流程追踪相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'handle_time': 'handle_time', + 'operate_status': 'operate_status', + 'activity_guid': 'activity_guid', + 'handle_opinion': 'handle_opinion', + 'is_finish': 'is_finish', + 'operator_ou_name': 'operator_ou_name', + 'is_back': 'is_back', + 'operator_name': 'operator_name', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单流程追踪数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int, activity_guid: str): + """ + 检查工单流程记录是否已存在(根据工单ID+办理环节名称)。 + + :param task_id: 关联工单主表ID + :param activity_guid: 办理环节名称 + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where( + cls.task_id == task_id, + cls.activity_guid == activity_guid + ) + _process: cls = await cls.query_first(_query) + return _process + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单流程追踪数据的基础方法。 + + 支持字段: + - task_id, operate_status, is_finish, is_back, operator_name + - 支持模糊匹配:activity_guid, handle_opinion, operator_ou_name + - 支持精确匹配:task_id, is_finish, is_back + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'handle_time': 'desc'} + :key int task_id: 精确匹配关联工单主表ID + :key str operate_status: 精确匹配办理状态 + :key str activity_guid: 模糊匹配办理环节名称 + :key str handle_opinion: 模糊匹配办理意见 + :key int is_finish: 精确匹配是否结束 + :key str operator_ou_name: 模糊匹配部门 + :key int is_back: 精确匹配是否回退 + :key str operator_name: 精确匹配办理人 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.activity_guid.key: '%{}%', + cls.handle_opinion.key: '%{}%', + cls.operator_ou_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.handle_time.desc()) + + _process_df = await cls.query_as_df(_data_query) + if not _process_df.empty: + _process_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _process_df[cls.id.key] = _process_df[cls.id.key].astype(str) + _process_df[cls.task_id.key] = _process_df[cls.task_id.key].astype(str) + + return _process_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单流程追踪数据,返回分页格式数据。 + """ + _process_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _process_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_activity(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id + activity_guid 判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 和 activity_guid 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 校验必要列 + required_cols = [cls.task_id.key, cls.activity_guid.key] + if not all(col in data_df.columns for col in required_cols): + echo_log(f"错误:exists_task_activity 要求输入数据必须包含 {required_cols} 列") + return pd.DataFrame(), data_df.copy() + + # 构建 task_id+activity_guid 组合键 + data_df['_combine_key'] = data_df[cls.task_id.key].astype(str) + '|' + data_df[ + cls.activity_guid.key].str.strip() + + # 获取待查询的组合键列表(去重) + combine_keys = data_df['_combine_key'].unique().tolist() + if not combine_keys: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.task_id, cls.activity_guid) + _process_df = await cls.query_as_df(_query) + if _process_df.empty: + data_df.drop(columns=['_combine_key'], inplace=True) + return pd.DataFrame(), data_df.copy() + + # 构建数据库组合键 + _process_df['_combine_key'] = _process_df[cls.task_id.key].astype(str) + '|' + _process_df[ + cls.activity_guid.key].str.strip() + combine_key_to_id_map = dict(zip(_process_df['_combine_key'], _process_df[cls.id.key])) + + # 根据组合键划分数据 + mask_exists = data_df['_combine_key'].isin(_process_df['_combine_key']) + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df['_combine_key'].map(combine_key_to_id_map) + + latest_df = data_df[~mask_exists].copy() + + # 清理临时列 + for df in [exists_df, latest_df, data_df]: + if '_combine_key' in df.columns: + df.drop(columns=['_combine_key'], inplace=True) + + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskProcess(GovcTaskProcessBase): + """ + 工单流程追踪模型类(主业务类,完全继承 TD3iGovcTaskProcess 字段)。 + + --- + description: 市12345工单流程追踪接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 5001 + handle_time: + description: 办理时间 + type: string + format: date-time + example: "2024-01-15 10:30:00" + operate_status: + description: 办理状态 + type: string + example: "处理中" + maxLength: 128 + activity_guid: + description: 办理环节名称 + type: string + example: "市级受理" + maxLength: 255 + handle_opinion: + description: 办理意见 + type: string + example: "已接收工单,正在分派处理" + is_finish: + description: 是否结束(0:未结束,1:已结束) + type: integer + example: 0 + operator_ou_name: + description: 部门 + type: string + example: "市12345政务服务中心" + maxLength: 255 + is_back: + description: 是否回退(0:否,1:是) + type: integer + example: 0 + operator_name: + description: 办理人 + type: string + example: "张三" + maxLength: 128 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "system" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单流程追踪记录。 + + 业务流程: + 1. 使用 GovcTaskProcessForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id+activity_guid 的记录(避免重复提交) + 3. 创建新流程追踪对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 工单流程追踪参数字典 + :return: 新建流程追踪对象 + :rtype: GovcTaskProcess + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskProcessForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id+activity_guid 的记录 + _existing = await cls.is_exist(_form.task_id.data, _form.activity_guid.data) + assert _existing is None, f"工单ID {_form.task_id.data} 的 {_form.activity_guid.data} 环节记录已存在,不能重复提交。" + + # 创建对象 + _process = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _process.created_by = user.username + _process.updated_by = user.username + await _process.async_save() + return _process + + @classmethod + async def delete(cls, process_id: Union[str, int]): + """ + 删除工单流程追踪记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param process_id: 要删除的流程追踪记录ID + :return: 删除的记录对象 + :rtype: GovcTaskProcess + :raises AssertionError: 当记录不存在时抛出 + """ + _process: cls = await cls.async_find_by_id(process_id) + assert _process, f"根据 ID {process_id} 未找到工单流程追踪记录。" + + _del_query = delete(cls).where(cls.id == _process.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单流程追踪记录(工单ID:{_process.task_id},环节:{_process.activity_guid},ID:{_process.id}).') + return _process + + @classmethod + async def modify(cls, process_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单流程追踪记录。 + + 业务流程: + 1. 将 process_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskProcessForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param process_id: 要修改的流程追踪记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的流程追踪对象 + :rtype: GovcTaskProcess + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskProcessForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _process: cls = await cls.async_find_by_id(process_id) + assert _process, f'查无此工单流程追踪信息(ID:{process_id})。' + + # 更新字段 + _process.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _process.updated_by = user.username + await _process.async_save() + return _process + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单流程追踪记录(传入数据应为全新记录)。 + + :param data_df: 包含流程追踪数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 设置创建者/更新者信息 + current_time = datetime.datetime.now() + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + # 补充默认时间字段 + data_df['created_at'] = data_df.get('created_at', current_time) + data_df['updated_at'] = data_df.get('updated_at', current_time) + + records = data_df.to_dict('records') + processes = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(processes) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(processes)} 条工单流程追踪记录。") + return len(processes) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单流程追踪记录。 + + :param data_df: 包含流程追踪数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if cls.id.key not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳和更新者 + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings 批量更新 + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单流程追踪记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单流程追踪数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态(根据 task_id + activity_guid 判断是否已存在) + _exists_df, _latest_df = await cls.exists_task_activity(data_df) + # 保存到数据库 + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_requester.py b/models/govc_task_requester.py new file mode 100644 index 0000000..2e3e728 --- /dev/null +++ b/models/govc_task_requester.py @@ -0,0 +1,525 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskRequester # 确保该模型已在db_models中定义 +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskRequesterForm(ModelForm): + """ + 诉求人信息表单验证类(完全映射 TD3iGovcTaskRequester 字段)。 + + 用于验证和处理市12345诉求人信息的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_requester 的字段结构。 + """ + + # 基础信息 + id = IntegerField('主键ID') + task_id = StringField('关联工单主表ID', validators=[Length(max=64, message='关联工单主表ID长度不能超过64字符')]) + card_num = StringField('身份证号', validators=[Length(max=128, message='身份证号长度不能超过128字符')]) + emotion = StringField('诉求情绪', validators=[Length(max=64, message='诉求情绪长度不能超过64字符')]) + name_scope = StringField('年龄范围', validators=[Length(max=64, message='年龄范围长度不能超过64字符')]) + sex = StringField('性别', validators=[Length(max=32, message='性别长度不能超过32字符')]) + name = StringField('诉求人', validators=[Length(max=128, message='诉求人长度不能超过128字符')]) + secret_flag = StringField('保密标识', validators=[Length(max=32, message='保密标识长度不能超过32字符')]) + is_secret = StringField('是否保密', validators=[Length(max=32, message='是否保密长度不能超过32字符')]) + is_not_show_record = IntegerField('是否不展示记录') + phone_num = StringField('来电号码', validators=[Length(max=64, message='来电号码长度不能超过64字符')]) + limk_num = StringField('联系号码1', validators=[Length(max=64, message='联系号码1长度不能超过64字符')]) + c_guid = StringField('cguid', validators=[Length(max=64, message='cguid长度不能超过64字符')]) + phone_num1 = StringField('联系号码2', validators=[Length(max=64, message='联系号码2长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskRequesterBase(TD3iGovcTaskRequester, CommonModel): + """ + 诉求人信息基础类(完全映射 TD3iGovcTaskRequester 字段)。 + + 继承自数据库模型 TD3iGovcTaskRequester 和通用模型 CommonModel。 + 封装所有与诉求人信息相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'card_num': 'card_num', + 'emotion': 'emotion', + 'name_scope': 'name_scope', + 'sex': 'sex', + 'name': 'name', + 'secret_flag': 'secret_flag', + 'is_secret': 'is_secret', + 'is_not_show_record': 'is_not_show_record', + 'phone_num': 'phone_num', + 'limk_num': 'limk_num', + 'c_guid': 'c_guid', + 'phone_num1': 'phone_num1', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 诉求人信息数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: str): + """ + 检查诉求人信息是否已存在(根据工单主表ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _requester: cls = await cls.query_first(_query) + return _requester + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索诉求人信息的基础方法。 + + 支持字段: + - task_id, card_num, emotion, name_scope, sex, name, secret_flag, is_secret + - 支持模糊匹配:phone_num, limk_num, c_guid, phone_num1 + - 支持精确匹配:is_not_show_record + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key str task_id: 精确匹配关联工单主表ID + :key str card_num: 精确匹配身份证号 + :key str emotion: 精确匹配诉求情绪 + :key str name_scope: 精确匹配年龄范围 + :key str sex: 精确匹配性别 + :key str name: 精确匹配诉求人 + :key str secret_flag: 精确匹配保密标识 + :key str is_secret: 精确匹配是否保密 + :key int is_not_show_record: 精确匹配是否不展示记录 + :key str phone_num: 模糊匹配来电号码 + :key str limk_num: 模糊匹配联系号码1 + :key str c_guid: 模糊匹配cguid + :key str phone_num1: 模糊匹配联系号码2 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.phone_num.key: '%{}%', + cls.limk_num.key: '%{}%', + cls.c_guid.key: '%{}%', + cls.phone_num1.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + _requester_df = await cls.query_as_df(_data_query) + if not _requester_df.empty: + _requester_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _requester_df[cls.id.key] = _requester_df[cls.id.key].astype(str) + + return _requester_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索诉求人信息,返回分页格式数据。 + """ + _requester_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _requester_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskRequester(GovcTaskRequesterBase): + """ + 诉求人信息模型类(主业务类,完全继承 TD3iGovcTaskRequester 字段)。 + + --- + description: 市12345诉求人信息接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: string + example: "TASK20240501001" + maxLength: 64 + card_num: + description: 身份证号 + type: string + example: "110101199001011234" + maxLength: 128 + emotion: + description: 诉求情绪 + type: string + example: "愤怒" + maxLength: 64 + name_scope: + description: 年龄范围 + type: string + example: "20-30岁" + maxLength: 64 + sex: + description: 性别 + type: string + example: "男" + maxLength: 32 + name: + description: 诉求人 + type: string + example: "张三" + maxLength: 128 + secret_flag: + description: 保密标识 + type: string + example: "0" + maxLength: 32 + is_secret: + description: 是否保密 + type: string + example: "否" + maxLength: 32 + is_not_show_record: + description: 是否不展示记录 + type: integer + example: 0 + phone_num: + description: 来电号码 + type: string + example: "13800138000" + maxLength: 64 + limk_num: + description: 联系号码1 + type: string + example: "13900139000" + maxLength: 64 + c_guid: + description: cguid + type: string + example: "GUID20240501001" + maxLength: 64 + phone_num1: + description: 联系号码2 + type: string + example: "13700137000" + maxLength: 64 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的诉求人信息记录。 + + 业务流程: + 1. 使用 GovcTaskRequesterForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新诉求人信息对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 诉求人信息参数字典 + :return: 新建诉求人信息对象 + :rtype: GovcTaskRequester + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskRequesterForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, "该工单已存在诉求人信息记录,不能重复提交。" + + # 创建对象 + _requester = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _requester.created_by = user.username + _requester.updated_by = user.username + await _requester.async_save() + return _requester + + @classmethod + async def delete(cls, requester_id: Union[str, int]): + """ + 删除诉求人信息记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param requester_id: 要删除的诉求人信息记录ID + :return: 删除的记录对象 + :rtype: GovcTaskRequester + :raises AssertionError: 当记录不存在时抛出 + """ + _requester: cls = await cls.async_find_by_id(requester_id) + assert _requester, f"根据 ID {requester_id} 未找到诉求人信息记录。" + + _del_query = delete(cls).where(cls.id == _requester.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除诉求人信息记录(工单ID:{_requester.task_id},ID:{_requester.id}).') + return _requester + + @classmethod + async def modify(cls, requester_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有诉求人信息记录。 + + 业务流程: + 1. 将 requester_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskRequesterForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param requester_id: 要修改的诉求人信息记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的诉求人信息对象 + :rtype: GovcTaskRequester + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskRequesterForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _requester: cls = await cls.async_find_by_id(requester_id) + assert _requester, f'查无此诉求人信息。' + + # 更新字段 + _requester.copy_from_dict(_form.data, skip_none=True).before_save() + _requester.updated_by = user.username + await _requester.async_save() + return _requester + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建诉求人信息记录(传入数据应为全新记录)。 + + :param data_df: 包含诉求人信息的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + requesters = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(requesters) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(requesters)} 条诉求人信息记录。") + return len(requesters) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有诉求人信息记录。 + + :param data_df: 包含诉求人信息的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条诉求人信息记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存诉求人信息数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTaskRequester.exists_task_id(data_df) + # 保存到数据库 + _created_count = await GovcTaskRequester.create_batch(_latest_df, user) + _updated_count = await GovcTaskRequester.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_return_visit.py b/models/govc_task_return_visit.py new file mode 100644 index 0000000..0035a53 --- /dev/null +++ b/models/govc_task_return_visit.py @@ -0,0 +1,503 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskReturnVisit +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +# 表单验证类 +class GovcTaskReturnVisitForm(ModelForm): + """ + 工单回访结果表单验证类(完全映射 TD3iGovcTaskReturnVisit 字段)。 + + 用于验证和处理市12345工单回访结果的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_return_visit 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 外键字段,非空验证由数据库层保证 + evl_result = StringField('结果满意度', validators=[Length(max=64, message='结果满意度长度不能超过64字符')]) + replay_person = StringField('回访人', validators=[Length(max=128, message='回访人长度不能超过128字符')]) + is_rg_reply = StringField('是否人工回访', validators=[Length(max=32, message='是否人工回访长度不能超过32字符')]) + processing_results = StringField('处理结果', validators=[Length(max=255, message='处理结果长度不能超过255字符')]) + solve_situation = StringField('解决情况', validators=[Length(max=64, message='解决情况长度不能超过64字符')]) + replay_time = DateTimeField('回访时间', validators=[Optional()]) + evl_style = StringField('态度满意度', validators=[Length(max=64, message='态度满意度长度不能超过64字符')]) + is_citizen = IntegerField('是否市民', validators=[Optional()]) + replay_content = TextAreaField('回访内容') + status = IntegerField('提交状态') # 兼容通用状态字段,若表中无则可删除 + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +# 基础业务类 +class GovcTaskReturnVisitBase(TD3iGovcTaskReturnVisit, CommonModel): + """ + 工单回访结果基础类(完全映射 TD3iGovcTaskReturnVisit 字段)。 + + 继承自数据库模型 TD3iGovcTaskReturnVisit 和通用模型 CommonModel。 + 封装所有与工单回访结果相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'evl_result': 'evl_result', + 'replay_person': 'replay_person', + 'is_rg_reply': 'is_rg_reply', + 'processing_results': 'processing_results', + 'solve_situation': 'solve_situation', + 'replay_time': 'replay_time', + 'evl_style': 'evl_style', + 'is_citizen': 'is_citizen', + 'replay_content': 'replay_content', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 回访结果数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查回访记录是否已存在(根据工单ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _visit: cls = await cls.query_first(_query) + return _visit + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索回访数据的基础方法。 + + 支持字段: + - task_id, evl_result, replay_person, is_rg_reply, solve_situation, evl_style, is_citizen + - 支持模糊匹配:processing_results, replay_content + - 支持精确匹配:is_rg_reply, is_citizen + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配工单ID + :key str evl_result: 精确匹配结果满意度 + :key str replay_person: 精确匹配回访人 + :key str is_rg_reply: 精确匹配是否人工回访 + :key str processing_results: 模糊匹配处理结果 + :key str solve_situation: 精确匹配解决情况 + :key str evl_style: 精确匹配态度满意度 + :key int is_citizen: 精确匹配是否市民 + :key str replay_content: 模糊匹配回访内容 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.processing_results.key: '%{}%', + cls.replay_content.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + _visit_df = await cls.query_as_df(_data_query) + if not _visit_df.empty: + _visit_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _visit_df[cls.id.key] = _visit_df[cls.id.key].astype(str) + # 处理时间字段格式 + if cls.replay_time.key in _visit_df.columns: + _visit_df[cls.replay_time.key] = _visit_df[cls.replay_time.key].dt.strftime('%Y-%m-%d %H:%M:%S') + + return _visit_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索回访数据,返回分页格式数据。 + """ + _visit_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _visit_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +# 主业务模型类 +@register_swagger_model +class GovcTaskReturnVisit(GovcTaskReturnVisitBase): + """ + 工单回访结果模型类(主业务类,完全继承 TD3iGovcTaskReturnVisit 字段)。 + + --- + description: 市12345工单回访结果接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 2001 + evl_result: + description: 结果满意度 + type: string + example: "满意" + maxLength: 64 + replay_person: + description: 回访人 + type: string + example: "张三" + maxLength: 128 + is_rg_reply: + description: 是否人工回访 + type: string + example: "是" + maxLength: 32 + processing_results: + description: 处理结果 + type: string + example: "已完成问题整改,用户认可" + maxLength: 255 + solve_situation: + description: 解决情况 + type: string + example: "完全解决" + maxLength: 64 + replay_time: + description: 回访时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + evl_style: + description: 态度满意度 + type: string + example: "满意" + maxLength: 64 + is_citizen: + description: 是否市民(0:否,1:是) + type: integer + example: 1 + replay_content: + description: 回访内容 + type: string + example: "用户反馈问题已解决,对处理态度表示满意" + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单回访记录。 + + 业务流程: + 1. 使用 GovcTaskReturnVisitForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新回访对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 回访参数字典 + :return: 新建回访对象 + :rtype: GovcTaskReturnVisit + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskReturnVisitForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, "该工单已存在回访记录,不能重复提交。" + + # 创建对象 + _visit = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _visit.created_by = user.username + _visit.updated_by = user.username + await _visit.async_save() + return _visit + + @classmethod + async def delete(cls, visit_id: Union[str, int]): + """ + 删除工单回访记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param visit_id: 要删除的回访记录ID + :return: 删除的记录对象 + :rtype: GovcTaskReturnVisit + :raises AssertionError: 当记录不存在时抛出 + """ + _visit: cls = await cls.async_find_by_id(visit_id) + assert _visit, f"根据 ID {visit_id} 未找到工单回访记录。" + + _del_query = delete(cls).where(cls.id == _visit.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单回访记录(工单ID:{_visit.task_id},ID:{_visit.id}).') + return _visit + + @classmethod + async def modify(cls, visit_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单回访记录。 + + 业务流程: + 1. 将 visit_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskReturnVisitForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param visit_id: 要修改的回访记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的回访对象 + :rtype: GovcTaskReturnVisit + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskReturnVisitForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _visit: cls = await cls.async_find_by_id(visit_id) + assert _visit, f'查无此工单回访信息。' + + # 更新字段 + _visit.copy_from_dict(_form.data, skip_none=True).before_save() + _visit.updated_by = user.username + await _visit.async_save() + return _visit + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单回访记录(传入数据应为全新记录)。 + + :param data_df: 包含回访数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + visits = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(visits) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(visits)} 条工单回访记录。") + return len(visits) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单回访记录。 + + :param data_df: 包含回访数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单回访记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单回访数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await cls.exists_task_id(data_df) + # 保存到数据库 + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_status.py b/models/govc_task_status.py new file mode 100644 index 0000000..c8dc588 --- /dev/null +++ b/models/govc_task_status.py @@ -0,0 +1,467 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskStatu # 确保数据库模型已导入 +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskStatusForm(ModelForm): + """ + 12345工单办理状态表单验证类(完全映射 TD3iGovcTaskStatu 字段)。 + + 用于验证和处理市12345工单办理状态的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_status 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Length(max=20, message='关联工单主表ID长度不能超过20字符')]) + shou_li = StringField('受理状态', validators=[Length(max=32, message='受理状态长度不能超过32字符')]) + jie_dan = StringField('接单状态', validators=[Length(max=32, message='接单状态长度不能超过32字符')]) + hui_fang = StringField('回访状态', validators=[Length(max=32, message='回访状态长度不能超过32字符')]) + ban_li = StringField('办理状态', validators=[Length(max=32, message='办理状态长度不能超过32字符')]) + created_at = StringField('创建时间') # 只读,表单仅用于展示 + created_by = StringField('创建者') # 只读,表单仅用于展示 + updated_at = StringField('更新时间') # 只读,表单仅用于展示 + updated_by = StringField('更新者') # 只读,表单仅用于展示 + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskStatusBase(TD3iGovcTaskStatu, CommonModel): + """ + 12345工单办理状态基础类(完全映射 TD3iGovcTaskStatu 字段)。 + + 继承自数据库模型 TD3iGovcTaskStatu 和通用模型 CommonModel。 + 封装所有与工单办理状态相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'shou_li': 'shou_li', + 'jie_dan': 'jie_dan', + 'hui_fang': 'hui_fang', + 'ban_li': 'ban_li', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单办理状态数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查工单办理状态记录是否已存在(根据关联工单主表ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _status: cls = await cls.query_first(_query) + return _status + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单办理状态数据的基础方法。 + + 支持字段: + - task_id, shou_li, jie_dan, hui_fang, ban_li + - 支持精确匹配:所有字段 + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'task_id': 'asc'} + :key int task_id: 精确匹配关联工单主表ID + :key str shou_li: 精确匹配受理状态 + :key str jie_dan: 精确匹配接单状态 + :key str hui_fang: 精确匹配回访状态 + :key str ban_li: 精确匹配办理状态 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 精确查询字段(无模糊查询) + _query = select(cls).where( + *cls.search_wheres(likes={}, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.id) + + _status_df = await cls.query_as_df(_data_query) + if not _status_df.empty: + _status_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _status_df[cls.id.key] = _status_df[cls.id.key].astype(str) + _status_df[cls.task_id.key] = _status_df[cls.task_id.key].astype(str) + + return _status_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单办理状态数据,返回分页格式数据。 + """ + _status_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _status_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskStatus(GovcTaskStatusBase): + """ + 12345工单办理状态模型类(主业务类,完全继承 TD3iGovcTaskStatu 字段)。 + + --- + description: 市12345工单办理状态接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 20240501001 + maxLength: 20 + shou_li: + description: 受理状态 + type: string + example: "已受理" + maxLength: 32 + jie_dan: + description: 接单状态 + type: string + example: "已接单" + maxLength: 32 + hui_fang: + description: 回访状态 + type: string + example: "已回访" + maxLength: 32 + ban_li: + description: 办理状态 + type: string + example: "已办结" + maxLength: 32 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "D3I" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "admin" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单办理状态记录。 + + 业务流程: + 1. 使用 GovcTaskStatusForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新状态对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 工单办理状态参数字典 + :return: 新建状态对象 + :rtype: GovcTaskStatus + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskStatusForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, "该工单已存在办理状态记录,不能重复提交。" + + # 创建对象 + _status = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _status.created_by = user.username + _status.updated_by = user.username + await _status.async_save() + return _status + + @classmethod + async def delete(cls, status_id: Union[str, int]): + """ + 删除工单办理状态记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param status_id: 要删除的状态记录ID + :return: 删除的记录对象 + :rtype: GovcTaskStatus + :raises AssertionError: 当记录不存在时抛出 + """ + _status: cls = await cls.async_find_by_id(status_id) + assert _status, f"根据 ID {status_id} 未找到工单办理状态记录。" + + _del_query = delete(cls).where(cls.id == _status.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单办理状态记录(工单ID:{_status.task_id},ID:{_status.id}).') + return _status + + @classmethod + async def modify(cls, status_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单办理状态记录。 + + 业务流程: + 1. 将 status_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskStatusForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param status_id: 要修改的状态记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的状态对象 + :rtype: GovcTaskStatus + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskStatusForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _status: cls = await cls.async_find_by_id(status_id) + assert _status, f'查无此工单办理状态信息。' + + # 更新字段 + _status.copy_from_dict(_form.data, skip_none=True).before_save() + _status.updated_by = user.username if user else _status.updated_by + await _status.async_save() + return _status + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单办理状态记录(传入数据应为全新记录)。 + + :param data_df: 包含状态数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 设置创建/更新者信息 + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + else: + data_df['created_by'] = data_df.get('created_by', 'D3I') + data_df['updated_by'] = data_df.get('updated_by', 'D3I') + + # 补充创建/更新时间 + data_df['created_at'] = datetime.datetime.now() + data_df['updated_at'] = datetime.datetime.now() + + records = data_df.to_dict('records') + statuses = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(statuses) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(statuses)} 条工单办理状态记录。") + return len(statuses) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单办理状态记录。 + + :param data_df: 包含状态数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings 批量更新 + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单办理状态记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单办理状态数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态(按task_id判断存在性) + _exists_df, _latest_df = await GovcTaskStatus.exists_task_id(data_df) + # 保存到数据库 + _created_count = await GovcTaskStatus.create_batch(_latest_df, user) + _updated_count = await GovcTaskStatus.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_supervision.py b/models/govc_task_supervision.py new file mode 100644 index 0000000..b228de9 --- /dev/null +++ b/models/govc_task_supervision.py @@ -0,0 +1,504 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, DateTimeField, IntegerField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskSupervision +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovcTaskSupervisionForm(ModelForm): + """ + 工单监察信息表单验证类(完全映射 TD3iGovcTaskSupervision 字段)。 + + 用于验证和处理市12345工单监察信息的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_supervision 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 非空在数据库层约束 + supervision_name = StringField('监察点名称', validators=[Length(max=255, message='监察点名称长度不能超过255字符')]) + supervision_type = StringField('监察点类型', validators=[Length(max=255, message='监察点类型长度不能超过255字符')]) + supervision_date = DateTimeField('监察点时间', validators=[Optional()]) + supervision_ou_name = StringField('部门', validators=[Length(max=255, message='部门长度不能超过255字符')]) + hj_date = DateTimeField('核减时间', validators=[Optional()]) + supervise_type = StringField('监察类别', validators=[Length(max=32, message='监察类别长度不能超过32字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovcTaskSupervisionBase(TD3iGovcTaskSupervision, CommonModel): + """ + 工单监察信息基础类(完全映射 TD3iGovcTaskSupervision 字段)。 + + 继承自数据库模型 TD3iGovcTaskSupervision 和通用模型 CommonModel。 + 封装所有与工单监察相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'supervision_name': 'supervision_name', + 'supervision_type': 'supervision_type', + 'supervision_date': 'supervision_date', + 'supervision_ou_name': 'supervision_ou_name', + 'hj_date': 'hj_date', + 'supervise_type': 'supervise_type', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单监察数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int, supervise_type: str = None): + """ + 检查监察记录是否已存在(根据工单ID+监察类别组合,可扩展)。 + + :param task_id: 关联工单主表ID + :param supervise_type: 监察类别(可选) + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + if supervise_type: + _query = _query.where(cls.supervise_type == supervise_type) + _supervision: cls = await cls.query_first(_query) + return _supervision + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单监察数据的基础方法。 + + 支持字段: + - task_id, supervision_name, supervision_type, supervise_type + - 支持模糊匹配:supervision_ou_name + - 支持精确匹配:id, supervise_type + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'supervision_date': 'desc'} + :key int id: 精确匹配记录ID + :key int task_id: 精确匹配工单ID + :key str supervision_name: 精确匹配监察点名称 + :key str supervision_type: 精确匹配监察点类型 + :key str supervision_ou_name: 模糊匹配部门 + :key str supervise_type: 精确匹配监察类别 + :key datetime supervision_date: 精确匹配监察点时间 + :key datetime hj_date: 精确匹配核减时间 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.supervision_ou_name.key: '%{}%', + cls.supervision_name.key: '%{}%', # 补充名称模糊匹配 + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.task_id, cls.supervision_date.desc()) + + _supervision_df = await cls.query_as_df(_data_query) + if not _supervision_df.empty: + _supervision_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _supervision_df[cls.id.key] = _supervision_df[cls.id.key].astype(str) + # 日期字段格式化 + for dt_field in ['supervision_date', 'hj_date', 'created_at', 'updated_at']: + if dt_field in _supervision_df.columns: + _supervision_df[dt_field] = _supervision_df[dt_field].dt.strftime('%Y-%m-%d %H:%M:%S') + + return _supervision_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单监察数据,返回分页格式数据。 + """ + _supervision_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _supervision_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id+supervise_type 组合判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列(可选supervise_type) + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id+supervise_type 组合 + _query = select(cls.id, cls.task_id, cls.supervise_type).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建复合键映射 (task_id, supervise_type) -> id + task_supervise_map = {} + for _, row in task_ids_df.iterrows(): + key = (row[cls.task_id.key], row[cls.supervise_type.key]) + task_supervise_map[key] = row[cls.id.key] + + # 根据复合键划分数据 + def is_exist(row): + key = (row[cls.task_id.key], row.get(cls.supervise_type.key, '')) + return key in task_supervise_map + + mask_exists = data_df.apply(is_exist, axis=1) + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df.apply( + lambda row: task_supervise_map.get((row[cls.task_id.key], row.get(cls.supervise_type.key, '')), ''), + axis=1 + ) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovcTaskSupervision(GovcTaskSupervisionBase): + """ + 工单监察信息模型类(主业务类,完全继承 TD3iGovcTaskSupervision 字段)。 + + --- + description: 市12345工单监察信息接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 5001 + supervision_name: + description: 监察点名称 + type: string + example: "超时未办结监察" + maxLength: 255 + supervision_type: + description: 监察点类型 + type: string + example: "时限监察" + maxLength: 255 + supervision_date: + description: 监察点时间 + type: string + format: date-time + example: "2024-05-01 10:00:00" + supervision_ou_name: + description: 部门 + type: string + example: "市政务服务中心" + maxLength: 255 + hj_date: + description: 核减时间 + type: string + format: date-time + example: "2024-05-02 15:30:00" + supervise_type: + description: 监察类别(zx/bm/bmhj) + type: string + example: "zx" + maxLength: 32 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单监察记录。 + + 业务流程: + 1. 使用 GovcTaskSupervisionForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id+supervise_type 的记录(避免重复提交) + 3. 创建新监察对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 监察参数字典 + :return: 新建监察对象 + :rtype: GovcTaskSupervision + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskSupervisionForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id+supervise_type 的记录 + _existing = await cls.is_exist(_form.task_id.data, _form.supervise_type.data) + assert _existing is None, f"工单ID {_form.task_id.data} 已存在[{_form.supervise_type.data}]类型的监察记录,不能重复提交。" + + # 创建对象 + _supervision = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _supervision.created_by = user.username + _supervision.updated_by = user.username + await _supervision.async_save() + return _supervision + + @classmethod + async def delete(cls, supervision_id: Union[str, int]): + """ + 删除工单监察记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param supervision_id: 要删除的监察记录ID + :return: 删除的记录对象 + :rtype: GovcTaskSupervision + :raises AssertionError: 当记录不存在时抛出 + """ + _supervision: cls = await cls.async_find_by_id(supervision_id) + assert _supervision, f"根据 ID {supervision_id} 未找到工单监察记录。" + + _del_query = delete(cls).where(cls.id == _supervision.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单监察记录(工单ID:{_supervision.task_id},ID:{_supervision.id}).') + return _supervision + + @classmethod + async def modify(cls, supervision_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单监察记录。 + + 业务流程: + 1. 将 supervision_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskSupervisionForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param supervision_id: 要修改的监察记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的监察对象 + :rtype: GovcTaskSupervision + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskSupervisionForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _supervision: cls = await cls.async_find_by_id(supervision_id) + assert _supervision, f'查无此工单监察信息。' + + # 更新字段 + _supervision.copy_from_dict(_form.data, skip_none=True).before_save() + _supervision.updated_by = user.username if user else _supervision.updated_by + await _supervision.async_save() + return _supervision + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单监察记录(传入数据应为全新记录)。 + + :param data_df: 包含监察数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 补充创建者/更新者信息 + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + else: + data_df['created_by'] = 'D3I' + data_df['updated_by'] = 'D3I' + + # 处理日期字段格式 + for dt_field in ['supervision_date', 'hj_date']: + if dt_field in data_df.columns: + data_df[dt_field] = pd.to_datetime(data_df[dt_field], errors='coerce') + + records = data_df.to_dict('records') + supervisions = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(supervisions) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(supervisions)} 条工单监察记录。") + return len(supervisions) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单监察记录。 + + :param data_df: 包含监察数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳和更新者 + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + # 处理日期字段格式 + for dt_field in ['supervision_date', 'hj_date']: + if dt_field in data_df.columns: + data_df[dt_field] = pd.to_datetime(data_df[dt_field], errors='coerce') + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings 批量更新 + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单监察记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单监察数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态(按task_id+supervise_type判断存在性) + _exists_df, _latest_df = await cls.exists_task_id(data_df) + # 保存到数据库 + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govc_task_title.py b/models/govc_task_title.py new file mode 100644 index 0000000..6e9f94f --- /dev/null +++ b/models/govc_task_title.py @@ -0,0 +1,465 @@ +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length, Optional + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovcTaskTitle +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +# 表单验证类 +class GovcTaskTitleForm(ModelForm): + """ + 工单标题表单验证类(完全映射 TD3iGovcTaskTitle 字段)。 + + 用于验证和处理市12345工单标题表的创建/修改表单数据。 + 字段完全映射数据库表 t_d3i_govc_task_title 的字段结构。 + """ + + # 基础信息 + id = IntegerField('记录ID') + task_id = IntegerField('关联工单主表ID', validators=[Optional()]) # 外键字段,非空由数据库约束 + urgency = IntegerField('是否紧急', validators=[Optional()]) + order_num = StringField('工单编号', validators=[Length(max=64, message='工单编号长度不能超过64字符')]) + source = StringField('来源', validators=[Length(max=64, message='来源长度不能超过64字符')]) + title = StringField('标题', validators=[Length(max=500, message='标题长度不能超过500字符')]) + created_at = StringField('创建时间', validators=[Optional()]) # 只读字段,仅用于展示 + created_by = StringField('创建者', validators=[Optional()]) # 只读字段,仅用于展示 + updated_at = StringField('更新时间', validators=[Optional()]) # 只读字段,仅用于展示 + updated_by = StringField('更新者', validators=[Optional()]) # 只读字段,仅用于展示 + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +# 基础业务类 +class GovcTaskTitleBase(TD3iGovcTaskTitle, CommonModel): + """ + 工单标题基础类(完全映射 TD3iGovcTaskTitle 字段)。 + + 继承自数据库模型 TD3iGovcTaskTitle 和通用模型 CommonModel。 + 封装所有与工单标题相关的通用操作方法。 + """ + + FieldMapping = { + 'id': 'id', + 'task_id': 'task_id', + 'urgency': 'urgency', + 'order_num': 'order_num', + 'source': 'source', + 'title': 'title', + 'created_at': 'created_at', + 'created_by': 'created_by', + 'updated_at': 'updated_at', + 'updated_by': 'updated_by', + } + """ + 工单标题数据映射 + """ + + @classmethod + async def is_exist(cls, task_id: int): + """ + 检查工单标题记录是否已存在(根据关联工单主表ID)。 + + :param task_id: 关联工单主表ID + :return: 存在返回对象,不存在返回None + """ + _query = select(cls).where(cls.task_id == task_id) + _title: cls = await cls.query_first(_query) + return _title + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索工单标题数据的基础方法。 + + 支持字段: + - task_id, urgency, order_num, source + - 支持模糊匹配:title + - 支持精确匹配:urgency, order_num, source + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'order_num': 'asc'} + :key int task_id: 精确匹配关联工单主表ID + :key int urgency: 精确匹配是否紧急 + :key str order_num: 精确匹配工单编号 + :key str source: 精确匹配来源 + :key str title: 模糊匹配标题 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段 + _name_likes = { + cls.title.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.order_num, cls.task_id) + + _title_df = await cls.query_as_df(_data_query) + if not _title_df.empty: + _title_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _title_df[cls.id.key] = _title_df[cls.id.key].astype(str) + _title_df[cls.task_id.key] = _title_df[cls.task_id.key].astype(str) + + return _title_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索工单标题数据,返回分页格式数据。 + """ + _title_df, _paging = await cls.search_base(** kwargs) + return { + 'total': _paging.row_count, + 'rows': _title_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_task_id(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 task_id 字段判断。 + + :param data_df: 输入的数据框架,必须包含 task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 task_id 列表(去重) + task_ids = data_df[cls.task_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的 task_id + _query = select(cls.id, cls.task_id).where(cls.task_id.in_(task_ids)) + task_ids_df = await cls.query_as_df(_query) + + if task_ids_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 task_id -> id 的映射字典 + task_id_to_id_map = dict(zip(task_ids_df[cls.task_id.key], task_ids_df[cls.id.key])) + + # 根据 task_id 是否在数据库中,划分数据 + mask_exists = data_df[cls.task_id.key].isin(task_ids_df[cls.task_id.key]) + # 数据库已经有的记录 + exists_df = data_df[mask_exists].copy() + # 自动补充从数据库查到的 id 字段 + exists_df[cls.id.key] = exists_df[cls.task_id.key].map(task_id_to_id_map) + # 新的数据 + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +# 主业务模型类(带Swagger文档) +@register_swagger_model +class GovcTaskTitle(GovcTaskTitleBase): + """ + 工单标题模型类(主业务类,完全继承 TD3iGovcTaskTitle 字段)。 + + --- + description: 市12345工单标题接口 + type: object + properties: + id: + description: 主键ID + type: integer + example: 1001 + readOnly: true + task_id: + description: 关联工单主表ID + type: integer + example: 2001 + urgency: + description: 是否紧急(0:不紧急,1:紧急) + type: integer + example: 1 + order_num: + description: 工单编号 + type: string + example: "GOV20240501001" + maxLength: 64 + source: + description: 来源 + type: string + example: "市民来电" + maxLength: 64 + title: + description: 标题 + type: string + example: "XX小区垃圾分类设施缺失问题" + maxLength: 500 + created_at: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + created_by: + description: 创建者用户名 + type: string + example: "admin" + readOnly: true + updated_at: + description: 修改时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + updated_by: + description: 修改者用户名 + type: string + example: "editor" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """ + 创建新的工单标题记录。 + + 业务流程: + 1. 使用 GovcTaskTitleForm 验证表单数据完整性 + 2. 检查是否已存在相同 task_id 的记录(避免重复提交) + 3. 创建新工单标题对象 + 4. 设置创建者和更新者为当前用户 + 5. 保存到数据库 + 6. 返回创建的对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: 工单标题参数字典 + :return: 新建工单标题对象 + :rtype: GovcTaskTitle + :raises AssertionError: 当记录已存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovcTaskTitleForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在相同 task_id 的记录 + _existing = await cls.is_exist(_form.task_id.data) + assert _existing is None, f"工单ID {_form.task_id.data} 已存在标题记录,不能重复提交。" + + # 创建对象 + _title = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _title.created_by = user.username + _title.updated_by = user.username + await _title.async_save() + return _title + + @classmethod + async def delete(cls, title_id: Union[str, int]): + """ + 删除工单标题记录。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行删除 + + :param title_id: 要删除的工单标题记录ID + :return: 删除的记录对象 + :rtype: GovcTaskTitle + :raises AssertionError: 当记录不存在时抛出 + """ + _title: cls = await cls.async_find_by_id(title_id) + assert _title, f"根据 ID {title_id} 未找到工单标题记录。" + + _del_query = delete(cls).where(cls.id == _title.id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除工单标题记录(工单编号:{_title.order_num},ID:{_title.id}).') + return _title + + @classmethod + async def modify(cls, title_id: Union[str, int], user: RbacUser = None, **kwargs): + """ + 修改已有工单标题记录。 + + 业务流程: + 1. 将 title_id 添加到参数中 + 2. 处理字符串字段去除首尾空格 + 3. 使用 GovcTaskTitleForm 验证表单数据 + 4. 查询原记录 + 5. 验证存在性 + 6. 更新字段并设置更新者 + 7. 保存到数据库 + 8. 返回更新后的对象 + + :param title_id: 要修改的工单标题记录ID + :param RbacUser user: 操作用户对象 + :param kwargs: 需要更新的字段 + :return: 修改后的工单标题对象 + :rtype: GovcTaskTitle + :raises AssertionError: 当记录不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 表单验证 + _form = GovcTaskTitleForm(formdata=kwargs) + _form.validate_form() + + # 查询原记录 + _title: cls = await cls.async_find_by_id(title_id) + assert _title, f'查无此工单标题信息。' + + # 更新字段 + _title.copy_from_dict(_form.data, skip_none=True).before_save() + _title.updated_by = user.username + await _title.async_save() + return _title + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量创建工单标题记录(传入数据应为全新记录)。 + + :param data_df: 包含工单标题数据的 DataFrame + :param user: 操作用户对象,用于设置 created_by / updated_by + :return: 成功创建的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + titles = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(titles) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(titles)} 条工单标题记录。") + return len(titles) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量修改已有工单标题记录。 + + :param data_df: 包含工单标题数据的 DataFrame(必须包含 id 列) + :param user: 操作用户对象,用于设置 updated_by + :return: 成功更新的数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 手动添加更新时间戳 + data_df['updated_at'] = datetime.datetime.now() + # 添加更新者信息 + if user: + data_df['updated_by'] = user.username + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单标题记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """ + 批量保存工单标题数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :param user: 用户 + :return: 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovcTaskTitle.exists_task_id(data_df) + # 保存到数据库 + _created_count = await GovcTaskTitle.create_batch(_latest_df, user) + _updated_count = await GovcTaskTitle.modify_batch(_exists_df, user) + return _created_count, _updated_count \ No newline at end of file diff --git a/models/govs_create_delay.py b/models/govs_create_delay.py new file mode 100644 index 0000000..65adf79 --- /dev/null +++ b/models/govs_create_delay.py @@ -0,0 +1,346 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.db_models import TD3iGovsApplicationForDelay +from models.common_model import CommonModel +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsApplicationForDelayForm(ModelForm): + """延时申请表单验证类""" + + id = IntegerField('主键') + master_id = IntegerField('主表ID') + gd_id = StringField('代签收唯一标志', validators=[Length(max=64)]) + finally_time_after_approve = StringField('延时申请通过后时间', validators=[Length(max=64)]) + finally_time_before_approve = StringField('计划完成时间', validators=[Length(max=64)]) + request_delay = StringField('申请延时时长', validators=[Length(max=64)]) + is_nature_day = StringField('延时时长类型', validators=[Length(max=10)]) + already_notify_order_user = StringField('是否已告知诉求人', validators=[Length(max=10)]) + request_reason = TextAreaField('延时原因') + remarks = StringField('备注', validators=[Length(max=500)]) + contact_name = StringField('何人', validators=[Length(max=100)]) + contact_time = StringField('何时', validators=[Length(max=64)]) + contact_type = StringField('何方式(编码)', validators=[Length(max=64)]) + contact_type_name = StringField('何方式(名称)', validators=[Length(max=100)]) + reply_script = TextAreaField('答复脚本') + file_id_str = TextAreaField('OA文件id') + order_no = StringField('工单号', validators=[Length(max=64)]) + process_instance_id = StringField('流程实例ID', validators=[Length(max=64)]) + request_delay_time = StringField('申请延时时长(字符串)', validators=[Length(max=64)]) + save_id = StringField('提交数据ID', validators=[Length(max=64)]) + order_id = StringField('工单ID', validators=[Length(max=64)]) + save_status = IntegerField('提交状态') + oa_feedback_status = IntegerField('OA反馈状态') + flow_token = StringField('流令牌', validators=[Length(max=256)]) + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64)]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsApplicationForDelayBase(TD3iGovsApplicationForDelay, CommonModel): + """延时申请业务基类""" + + FieldMapping = { + # 主键与关联 + 'id': 'id', + 'master_id': 'gdId', + 'order_id': 'orderId', + 'order_no': 'orderNo', + 'process_instance_id': 'processInstanceId', + 'gd_id': 'gdId', + + # 延时核心字段 + 'finally_time_after_approve': 'finallyTimeAfterApprove', + 'finally_time_before_approve': 'finallyTimeBeforeApprove', + 'request_delay': 'requestDelay', + 'is_nature_day': 'isNatureDay', + 'request_delay_time': 'requestDelayTime', + + # 沟通告知字段 + 'already_notify_order_user': 'alreadyNotifyOrderUser', + 'contact_name': 'contactName', + 'contact_time': 'contactTime', + 'contact_type': 'contactType', + 'contact_type_name': 'contactTypeName', + 'reply_script': 'replyScript', + + # 原因与附件 + 'request_reason': 'requestReason', + 'remarks': 'remarks', + 'file_id_str': 'fileIdStr' + } + + @classmethod + async def exist_other(cls, id: Union[str, int], master_id: Union[str, int] = None, order_id: str = None, + order_no: str = None): + """检查是否存在除当前记录外的其他同唯一标识延时申请""" + _query = select(cls).where(cls.id != id) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查找延时申请""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, master_id: Union[str, int] = None, order_id: str = None, order_no: str = None): + """检查延时申请是否已经存在""" + _query = select(cls) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """按参数搜索延时申请的基础方法""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + _name_likes = { + cls.master_id.key: '%{}%', + cls.order_no.key: '%{}%' + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """按参数搜索延时申请,返回分页格式数据""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_master_id(cls, data_df: pd.DataFrame): + """根据 master_id 判断数据是否存在""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + master_ids = data_df[cls.master_id.key].unique().tolist() + if not master_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.master_id).where(cls.master_id.in_(master_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + master_id_to_id_map = dict(zip(existing_df[cls.master_id.key], existing_df[cls.id.key])) + + mask_exists = data_df[cls.master_id.key].isin(existing_df[cls.master_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.master_id.key].map(master_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主表ID查找延时申请""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单ID查找延时申请""" + _query = select(cls).where(cls.order_id == order_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_ids(cls, master_ids: list[Union[str, int]]): + """根据主表ID列表批量查找延时申请""" + _query = select(cls).where(cls.master_id.in_(master_ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + +@register_swagger_model +class GovsApplicationForDelay(GovsApplicationForDelayBase): + """延时申请业务类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """创建新延时申请""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsApplicationForDelayForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + master_id=_form.master_id.data, + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _existing is None, "该任务已存在申请延期记录,不能重复创建。" + + _delay = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _delay.created_by = user.username + _delay.updated_by = user.username + await _delay.async_save() + return _delay + + @classmethod + async def delete(cls, delay_id: Union[str, int]): + """删除延时申请""" + _delay: cls = await cls.async_find_by_id(delay_id) + assert _delay, f"根据 ID {delay_id} 未找到延时申请。" + + _del_query = delete(cls).where(cls.id == _delay.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除延时申请(工单ID:{_delay.master_id},ID:{_delay.id}).') + return _delay + + @classmethod + async def modify(cls, delay_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改延时申请信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsApplicationForDelayForm(formdata=kwargs) + _form.validate_form() + + _delay: cls = await cls.async_find_by_id(delay_id) + assert _delay, f'查无此延时申请信息。' + + _delay.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _delay.updated_by = user.username + await _delay.async_save() + return _delay + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量创建延时申请""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + delays = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(delays) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(delays)} 条延时申请。") + return len(delays) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改延时申请""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条延时申请。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存数据,自动处理新建和更新""" + _exists_df, _latest_df = await cls.exists_master_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govs_create_reply.py b/models/govs_create_reply.py new file mode 100644 index 0000000..e1e81dd --- /dev/null +++ b/models/govs_create_reply.py @@ -0,0 +1,348 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.db_models import TD3iGovsReplyFormal +from models.common_model import CommonModel +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsReplyFormalForm(ModelForm): + """答复办结表单验证类""" + + id = IntegerField('主键') + master_id = IntegerField('主表ID') + master_id = StringField('代签收唯一标志', validators=[Length(max=64)]) + is_contact = StringField('是否联系服务对象', validators=[Length(max=10)]) + contact_name = StringField('联系人员', validators=[Length(max=100)]) + contact_time = StringField('联系时间', validators=[Length(max=64)]) + contact_type = StringField('联系情况', validators=[Length(max=255)]) + advice = TextAreaField('处理意见(面向群众公开)') + reason = TextAreaField('处理意见(面向群众公开2)') + remarks = StringField('备注', validators=[Length(max=500)]) + file_id_str = TextAreaField('OA文件id') + save_id = StringField('提交数据ID', validators=[Length(max=64)]) + process_instance_id = StringField('流程实例ID', validators=[Length(max=64)]) + business_key = StringField('业务键', validators=[Length(max=64)]) + order_no = StringField('工单号', validators=[Length(max=64)]) + action_name = StringField('操作名称', validators=[Length(max=255)]) + case_accord_type_one_name = StringField('诉求归口一级名称', validators=[Length(max=255)]) + case_accord_type_two_name = StringField('诉求归口二级名称', validators=[Length(max=255)]) + case_accord_type_three_name = StringField('诉求归口三级名称', validators=[Length(max=255)]) + save_status = IntegerField('提交状态') + oa_feedback_status = IntegerField('OA反馈状态') + flow_token = StringField('流令牌', validators=[Length(max=256)]) + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64)]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj=None, **kwargs) + + +class GovsReplyFormalBase(TD3iGovsReplyFormal, CommonModel): + """答复办结业务基类""" + + FieldMapping = { + # 主键 & 关联ID + 'id': 'id', + 'master_id': 'masterId', + 'gd_id': 'gdId', + 'order_no': 'orderNo', + 'process_instance_id': 'processInstanceId', + 'business_key': 'businessKey', + 'save_id': 'saveId', + 'action_name': 'actionName', + + # 联系服务对象信息 + 'is_contact': 'isContact', + 'contact_name': 'contactName', + 'contact_time': 'contactTime', + 'contact_type': 'contactType', + + # 处理意见 & 备注 + 'advice': 'advice', + 'reason': 'reason', + 'remarks': 'remarks', + 'file_id_str': 'fileIdStr', + + # 诉求归口分类 + 'case_accord_type_one_name': 'caseAccordTypeOneName', + 'case_accord_type_two_name': 'caseAccordTypeTwoName', + 'case_accord_type_three_name': 'caseAccordTypeThreeName', + + # 流令牌 + 'flow_token': 'flowToken', + } + + @classmethod + async def exist_other(cls, id: Union[str, int], master_id: Union[str, int] = None, order_no: str = None, + business_key: str = None): + """检查是否存在除当前记录外的同唯一标识答复办结记录""" + _query = select(cls).where(cls.id != id) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + if business_key: + _query = _query.where(cls.business_key == business_key) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查询答复办结记录""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, master_id: Union[str, int] = None, order_no: str = None, business_key: str = None): + """检查答复办结记录是否已存在""" + _query = select(cls) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + if business_key: + _query = _query.where(cls.business_key == business_key) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """答复办结基础搜索(分页/不分页)""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段配置 + _name_likes = { + cls.master_id.key: '%{}%', + cls.order_no.key: '%{}%', + cls.master_id.key: '%{}%', + cls.contact_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + # 排序处理 + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """分页搜索答复办结记录,返回标准分页结构""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_master_id(cls, data_df: pd.DataFrame): + """根据 master_id 区分已有数据/新增数据(批量保存用)""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + master_ids = data_df[cls.master_id.key].unique().tolist() + if not master_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.master_id).where(cls.master_id.in_(master_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + master_id_to_id_map = dict(zip(existing_df[cls.master_id.key], existing_df[cls.id.key])) + mask_exists = data_df[cls.master_id.key].isin(existing_df[cls.master_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.master_id.key].map(master_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主表ID查询单条答复办结记录""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_business_key(cls, business_key: str): + """根据业务键查询答复办结记录""" + _query = select(cls).where(cls.business_key == business_key) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_ids(cls, master_ids: list[Union[str, int]]): + """根据主表ID列表批量查询答复办结记录""" + _query = select(cls).where(cls.master_id.in_(master_ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + +@register_swagger_model +class GovsReplyFormal(GovsReplyFormalBase): + """答复办结业务操作类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """新增答复办结记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsReplyFormalForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + master_id=_form.master_id.data, + order_no=_form.order_no.data, + business_key=_form.business_key.data + ) + assert _existing is None, "该任务已存在答复办结记录,无法重复创建。" + + _reply_info = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _reply_info.created_by = user.username + _reply_info.updated_by = user.username + await _reply_info.async_save() + return _reply_info + + @classmethod + async def delete(cls, reply_id: Union[str, int]): + """删除答复办结记录""" + _reply_info: cls = await cls.async_find_by_id(reply_id) + assert _reply_info, f"根据 ID {reply_id} 未找到答复办结记录。" + + _del_query = delete(cls).where(cls.id == _reply_info.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除答复办结记录(工单ID:{_reply_info.master_id},ID:{_reply_info.id}).') + return _reply_info + + @classmethod + async def modify(cls, reply_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改答复办结记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsReplyFormalForm(formdata=kwargs) + _form.validate_form() + + _reply_info: cls = await cls.async_find_by_id(reply_id) + assert _reply_info, f'查无此答复办结记录。' + + _reply_info.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _reply_info.updated_by = user.username + await _reply_info.async_save() + return _reply_info + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量新增答复办结记录""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + reply_list = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(reply_list) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(reply_list)} 条答复办结记录。") + return len(reply_list) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改答复办结记录""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条答复办结记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存(自动区分新增/更新)""" + _exists_df, _latest_df = await cls.exists_master_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govs_create_return.py b/models/govs_create_return.py new file mode 100644 index 0000000..6d5d1df --- /dev/null +++ b/models/govs_create_return.py @@ -0,0 +1,353 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.db_models import TD3iGovsWorkOrderReturnFormal +from models.common_model import CommonModel +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsWorkOrderReturnFormalForm(ModelForm): + """工单退回表单验证类""" + + id = IntegerField('主键') + master_id = IntegerField('主表ID') + master_id = StringField('代签收唯一标志', validators=[Length(max=64)]) + return_reason = StringField('退回原因', validators=[Length(max=255)]) + return_reason_name = StringField('退回原因名称', validators=[Length(max=255)]) + return_auditor_name = StringField('退回审核人', validators=[Length(max=100)]) + return_auditor_id = StringField('退回审核人ID', validators=[Length(max=64)]) + deal_opinion = TextAreaField('处理意见') + reason = TextAreaField('处理意见2') + remark = StringField('备注', validators=[Length(max=500)]) + file_id_str = TextAreaField('OA文件id') + process_instance_id = StringField('流程实例ID', validators=[Length(max=64)]) + action_name = StringField('操作名称', validators=[Length(max=255)]) + order_id = StringField('工单ID', validators=[Length(max=64)]) + task_id = StringField('任务ID', validators=[Length(max=64)]) + order_no = StringField('工单号', validators=[Length(max=64)]) + case_accord_type_one_name = StringField('诉求归口一级名称', validators=[Length(max=255)]) + case_accord_type_two_name = StringField('诉求归口二级名称', validators=[Length(max=255)]) + case_accord_type_three_name = StringField('诉求归口三级名称', validators=[Length(max=255)]) + save_status = IntegerField('提交状态') + oa_feedback_status = IntegerField('OA反馈状态') + flow_token = StringField('流令牌', validators=[Length(max=256)]) + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64)]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsWorkOrderReturnFormalBase(TD3iGovsWorkOrderReturnFormal, CommonModel): + """工单退回业务基类""" + + FieldMapping = { + # 主键 & 关联ID + 'id': 'id', + 'master_id': 'gdId', + 'gd_id': 'gdId', + 'order_id': 'orderId', + 'order_no': 'orderNo', + 'process_instance_id': 'processInstanceId', + 'task_id': 'taskId', + + # 退回核心信息 + 'return_reason': 'returnReason', + 'return_reason_name': 'returnReasonName', + 'return_auditor_name': 'returnAuditorName', + 'return_auditor_id': 'returnAuditorId', + + # 处理意见 & 备注 + 'deal_opinion': 'dealOpinion', + 'reason': 'reason', + 'remark': 'remark', + 'file_id_str': 'fileIdStr', + + # 流程 & 分类信息 + 'action_name': 'actionName', + 'case_accord_type_one_name': 'caseAccordTypeOneName', + 'case_accord_type_two_name': 'caseAccordTypeTwoName', + 'case_accord_type_three_name': 'caseAccordTypeThreeName', + + # 状态 & 令牌 + 'flow_token': 'flowToken', + } + + @classmethod + async def exist_other(cls, id: Union[str, int], master_id: Union[str, int] = None, order_id: str = None, + order_no: str = None): + """检查是否存在除当前记录外的同唯一标识工单退回记录""" + _query = select(cls).where(cls.id != id) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查询工单退回记录""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, master_id: Union[str, int] = None, order_id: str = None, order_no: str = None): + """检查工单退回记录是否已存在""" + _query = select(cls) + if master_id: + _query = _query.where(cls.master == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """工单退回基础搜索(分页/不分页)""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段配置 + _name_likes = { + cls.master_id.key: '%{}%', + cls.order_no.key: '%{}%', + cls.return_reason.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + # 排序处理 + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """分页搜索工单退回记录,返回标准分页结构""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_master_id(cls, data_df: pd.DataFrame): + """根据 master_id 区分已有数据/新增数据(批量保存用)""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + master_ids = data_df[cls.master_id.key].unique().tolist() + if not master_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.master_id).where(cls.master_id.in_(master_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + master_id_to_id_map = dict(zip(existing_df[cls.master_id.key], existing_df[cls.id.key])) + mask_exists = data_df[cls.master_id.key].isin(existing_df[cls.master_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.master_id.key].map(master_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主表ID查询单条退回记录""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_id(cls, master_id: str): + """根据代签收唯一标志查询退回记录""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单ID查询退回记录""" + _query = select(cls).where(cls.order_id == order_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_ids(cls, master_ids: list[Union[str, int]]): + """根据主表ID列表批量查询退回记录""" + _query = select(cls).where(cls.master_id.in_(master_ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + +@register_swagger_model +class GovsWorkOrderReturnFormal(GovsWorkOrderReturnFormalBase): + """工单退回业务操作类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """新增工单退回记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsWorkOrderReturnFormalForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + master_id=_form.master_id.data, + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _existing is None, "该任务已存在工单退回记录,无法重复创建。" + + _return_info = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _return_info.created_by = user.username + _return_info.updated_by = user.username + await _return_info.async_save() + return _return_info + + @classmethod + async def delete(cls, return_id: Union[str, int]): + """删除工单退回记录""" + _return_info: cls = await cls.async_find_by_id(return_id) + assert _return_info, f"根据 ID {return_id} 未找到工单退回记录。" + + _del_query = delete(cls).where(cls.id == _return_info.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除工单退回记录(工单ID:{_return_info.master_id},ID:{_return_info.id}).') + return _return_info + + @classmethod + async def modify(cls, return_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改工单退回记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsWorkOrderReturnFormalForm(formdata=kwargs) + _form.validate_form() + + _return_info: cls = await cls.async_find_by_id(return_id) + assert _return_info, f'查无此工单退回记录。' + + _return_info.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _return_info.updated_by = user.username + await _return_info.async_save() + return _return_info + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量新增工单退回记录""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + return_list = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(return_list) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(return_list)} 条工单退回记录。") + return len(return_list) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改工单退回记录""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单退回记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存(自动区分新增/更新)""" + _exists_df, _latest_df = await cls.exists_master_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govs_order_attachment.py b/models/govs_order_attachment.py new file mode 100644 index 0000000..b37cbee --- /dev/null +++ b/models/govs_order_attachment.py @@ -0,0 +1,290 @@ +# coding: utf-8 +import datetime +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +from models.common_model import CommonModel +from models.db_models import TD3iGovsOrderAttachment +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.web.form import ModelForm + + +class GovsOrderAttachmentForm(ModelForm): + """工单附件表单验证类""" + + id = IntegerField('附件唯一ID') + master_id = IntegerField('关联工单主表ID') + order_id = StringField('工单编号', validators=[Length(max=50)]) + file_path = StringField('文件路径(内网地址)', validators=[Length(max=500)]) + out_file_path = StringField('外网文件路径', validators=[Length(max=500)]) + attach_name = StringField('附件名称', validators=[Length(max=200)]) + to_tenant_id = StringField('目标租户ID', validators=[Length(max=50)]) + create_date = StringField('记录创建时间') + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsOrderAttachmentBase(TD3iGovsOrderAttachment, CommonModel): + """工单附件业务基类""" + + FieldMapping = { + # ==================== 主键与关联 ==================== + 'id': 'id', # 附件唯一ID + + # ==================== 文件信息 ==================== + 'file_path': 'filePath', # 文件路径(内网地址) + 'out_file_path': 'outFilePath', # 外网文件路径 + 'attach_name': 'attachName', # 附件名称 + 'to_tenant_id': 'toTenantId', # 目标租户ID + } + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单编号查找附件""" + _query = select(cls).where(cls.order_id == order_id) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主工单ID查找附件""" + _query = select(cls).where(cls.master_id == master_id) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def find_by_attach_name(cls, attach_name: str): + """根据附件名称查找""" + _query = select(cls).where(cls.attach_name == attach_name) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def exists_order_id(cls, data_df: pd.DataFrame): + """根据 order_id 判断数据是否存在 + :param data_df: 输入的数据框架,必须包含 id 和 order_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录(附带数据库中的 id) + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 (id, order_id) 组合 + pairs = data_df[[cls.id.key, cls.order_id.key]].drop_duplicates().values.tolist() + if not pairs: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录(使用 IN 批量查询) + _query = select(cls.id, cls.order_id).where( + (cls.id.in_([p[0] for p in pairs])) & + (cls.order_id.in_([p[1] for p in pairs])) + ) + exists_db_df = await cls.query_as_df(_query) + + if exists_db_df.empty: + return pd.DataFrame(), data_df.copy() + + exists_db_df[cls.id.key] = exists_db_df[cls.id.key].astype(str) + exists_db_df[cls.order_id.key] = exists_db_df[cls.order_id.key].astype(str) + # 构建 (id, order_id) -> id 的映射(用于快速查找) + key_to_id_map = dict(zip( + zip(exists_db_df[cls.id.key], exists_db_df[cls.order_id.key]), + exists_db_df[cls.id.key] + )) + + # 标记 data_df 中哪些行在数据库中存在 + mask_exists = data_df.apply( + lambda row: (row[cls.id.key], row[cls.order_id.key]) in key_to_id_map, + axis=1 + ) + # 提取存在的记录,并补充数据库中的 id(虽然输入中已有 id,但为一致性保留) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df.apply( + lambda row: key_to_id_map[(row[cls.id.key], row[cls.order_id.key])], + axis=1 + ) + # 提取不存在的记录 + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + +@register_swagger_model +class GovsOrderAttachment(GovsOrderAttachmentBase): + """工单附件业务类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """创建附件记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderAttachmentForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在(根据 id) + _existing = await cls.async_find_by_id(_form.id.data) + if _existing: + # 更新已有记录 + _existing.copy_from_dict(_form.data, skip_none=True) + if user: + _existing.updated_by = user.username + await _existing.async_save() + return _existing + + _obj = cls().copy_from_dict(_form.data, skip_none=True) + if user: + _obj.created_by = user.username + _obj.updated_by = user.username + await _obj.async_save() + return _obj + + @classmethod + async def delete(cls, obj_id: Union[str, int]): + """删除附件记录""" + _obj: cls = await cls.async_find_by_id(obj_id) + assert _obj, f"根据 ID {obj_id} 未找到附件记录。" + + _del_query = delete(cls).where(cls.id == _obj.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除附件记录(order_id:{_obj.order_id},ID:{_obj.id}).') + return _obj + + @classmethod + async def modify(cls, obj_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改附件记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderAttachmentForm(formdata=kwargs) + _form.validate_form() + + _obj: cls = await cls.async_find_by_id(obj_id) + assert _obj, f'查无此附件记录。' + + _obj.copy_from_dict(_form.data, skip_none=True) + if user: + _obj.updated_by = user.username + await _obj.async_save() + return _obj + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量创建附件记录""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + objs = [cls().copy_from_dict(record, skip_none=True) for record in records] + + session = cls.get_aio_session() + try: + session.add_all(objs) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(objs)} 条附件记录。") + return len(objs) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改附件记录""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条附件记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存数据,自动处理新建和更新""" + _exists_df, _latest_df = await cls.exists_order_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count + + @classmethod + async def create_or_update_by_id(cls, user: RbacUser = None, **kwargs): + """根据 id 创建或更新附件记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderAttachmentForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.async_find_by_id(_form.id.data) + if _existing: + _existing.copy_from_dict(_form.data, skip_none=True) + if user: + _existing.updated_by = user.username + await _existing.async_save() + return _existing + + _obj = cls().copy_from_dict(_form.data, skip_none=True) + if user: + _obj.created_by = user.username + _obj.updated_by = user.username + await _obj.async_save() + return _obj + + @classmethod + async def delete_by_master_id(cls, master_id: Union[str, int]): + """根据主工单ID删除附件记录""" + attachments = await cls.find_by_master_id(master_id) + if attachments: + for att in attachments: + await cls.delete(att.id) + return attachments + + @classmethod + async def delete_by_order_id(cls, order_id: str): + """根据工单编号删除附件记录""" + attachments = await cls.find_by_order_id(order_id) + if attachments: + for att in attachments: + await cls.delete(att.id) + return attachments \ No newline at end of file diff --git a/models/govs_order_detail.py b/models/govs_order_detail.py new file mode 100644 index 0000000..2e52012 --- /dev/null +++ b/models/govs_order_detail.py @@ -0,0 +1,601 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovsOrderDetail +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsOrderDetailForm(ModelForm): + """省12345工单详情表单验证类""" + + id = IntegerField('详情记录唯一ID') + master_id = IntegerField('关联工单主表ID') + order_id = StringField('工单编号', validators=[Length(max=50)]) + order_no = StringField('工单号', validators=[Length(max=50)]) + master_id_origin = StringField('原始主表ID', validators=[Length(max=50)]) + tenant_id = StringField('租户ID', validators=[Length(max=50)]) + tenant_name = StringField('租户名称', validators=[Length(max=50)]) + order_status = StringField('工单状态码', validators=[Length(max=10)]) + order_status_for_view = StringField('工单状态显示值', validators=[Length(max=50)]) + claim_status = StringField('签收状态', validators=[Length(max=10)]) + over_due = StringField('是否超期', validators=[Length(max=10)]) + is_supervise = StringField('是否督办', validators=[Length(max=10)]) + first_order_status = StringField('一级状态编码', validators=[Length(max=10)]) + secord_order_status = StringField('二级状态编码', validators=[Length(max=10)]) + atomic_order_status = StringField('原子状态编码', validators=[Length(max=10)]) + order_invalid_type = TextAreaField('工单作废原因') + order_finish_time = DateTimeField('工单完成时间') + case_content = TextAreaField('诉求内容') + case_goal = TextAreaField('诉求目的') + title = StringField('工单标题', validators=[Length(max=500)]) + case_labels = TextAreaField('工单标签列表') + case_public = StringField('是否公开', validators=[Length(max=10)]) + hotspot = StringField('是否热点事件', validators=[Length(max=10)]) + case_is_urgent = StringField('紧急程度', validators=[Length(max=10)]) + case_is_visit = StringField('是否回访', validators=[Length(max=10)]) + info_protect = StringField('信息保护', validators=[Length(max=10)]) + case_accord_type_one_name = StringField('诉求归口一级', validators=[Length(max=50)]) + case_accord_type_two_name = StringField('诉求归口二级', validators=[Length(max=50)]) + case_accord_type_three_name = StringField('诉求归口三级', validators=[Length(max=50)]) + case_accord_type_four_name = StringField('诉求归口四级', validators=[Length(max=50)]) + case_accord_type_five_name = StringField('诉求归口五级', validators=[Length(max=50)]) + case_accord_code = StringField('事项编码', validators=[Length(max=50)]) + first_level_affiliation = TextAreaField('一级归属单位') + second_level_affiliation = TextAreaField('二级归属单位') + third_level_affiliation = TextAreaField('三级归属单位') + fourth_level_affiliation = TextAreaField('四级归属单位') + fifth_level_affiliation = TextAreaField('五级归属单位') + sixth_level_affiliation = TextAreaField('六级归属单位') + seventh_level_affiliation = TextAreaField('七级归属单位') + appeal_dept = StringField('诉求部门', validators=[Length(max=100)]) + order_source = StringField('诉求来源', validators=[Length(max=50)]) + order_source_detail = StringField('诉求来源详情', validators=[Length(max=50)]) + order_source_for_view = StringField('诉求来源显示值', validators=[Length(max=50)]) + belong_platform = StringField('所属平台代码', validators=[Length(max=50)]) + belong_platform_name = StringField('受理平台名称', validators=[Length(max=50)]) + current_processing_platform = TextAreaField('当前处理平台') + service_object_type = StringField('服务对象类型', validators=[Length(max=50)]) + order_type = StringField('表单类型', validators=[Length(max=50)]) + form_type = StringField('表单类型代码', validators=[Length(max=50)]) + area_code = StringField('区域代码', validators=[Length(max=10)]) + area_code_city = StringField('市区域代码', validators=[Length(max=50)]) + area_code_area = StringField('区区域代码', validators=[Length(max=50)]) + area_code_street = StringField('街道区域代码', validators=[Length(max=50)]) + address_detail = StringField('详细地址', validators=[Length(max=500)]) + case_lnglat = StringField('地理坐标', validators=[Length(max=100)]) + call_number = StringField('来电号码', validators=[Length(max=20)]) + call_number_for_dh = StringField('来电号码(脱敏)', validators=[Length(max=20)]) + raw_call_numer = StringField('原始来电号码', validators=[Length(max=20)]) + contact_number = StringField('联系电话', validators=[Length(max=20)]) + raw_contact_number = StringField('原始联系电话', validators=[Length(max=20)]) + contact_number_for_dh = StringField('联系电话(脱敏)', validators=[Length(max=20)]) + call_time = DateTimeField('来电时间') + order_sound_record_id = StringField('通话记录ID', validators=[Length(max=50)]) + create_date = DateTimeField('创建日期') + update_date = DateTimeField('更新日期') + plan_finish_time = DateTimeField('计划完成时间') + plan_sign_time = DateTimeField('计划签收时间') + judgment_flag = StringField('判定标志', validators=[Length(max=10)]) + is_coordination = StringField('是否协调', validators=[Length(max=10)]) + coordination_time = DateTimeField('协调时间') + thrid_order_id = TextAreaField('第三方工单ID') + relate_order_ids = TextAreaField('关联工单ID列表') + relate_order_count = IntegerField('关联工单数量') + order_user_id = StringField('用户ID', validators=[Length(max=50)]) + user_word = TextAreaField('用户反馈') + show_flag = StringField('显示标志', validators=[Length(max=10)]) + origin_show = IntegerField('原始显示标志') + order_user = TextAreaField('诉求人信息(JSON对象)') + order_phone_dto = TextAreaField('电话号码信息(JSON对象)') + order_attachment_list = TextAreaField('附件列表(JSON数组)') + pre_process_list = TextAreaField('预处理流程列表(JSON数组)') + tripartite_call_records = TextAreaField('三方通话记录(JSON对象)') + tripartite_call_records_list = TextAreaField('三方通话记录列表(JSON数组)') + order_custom_form_fields = TextAreaField('自定义表单字段(JSON数组)') + knowledge_references = TextAreaField('知识参考(JSON对象)') + sound_recording_address_list = TextAreaField('录音文件路径列表(JSON数组)') + active_dept_ids = TextAreaField('当前处理部门ID列表') + attachment_ids = TextAreaField('附件ID列表') + attachment_list = TextAreaField('附件列表JSON') + contactor_list = TextAreaField('联系人列表(JSON数组)') + tsjb_entry_info = TextAreaField('投诉举报入口信息(JSON对象)') + order_erge_revoke_plug_dto_list = TextAreaField('撤销插件信息(JSON数组)') + order_environmental = TextAreaField('环境信息(JSON对象)') + order_demands_dto = TextAreaField('诉求DTO(JSON对象)') + order_appeal_list = TextAreaField('申诉列表(JSON数组)') + torder_process_list = TextAreaField('流程列表(JSON数组)') + pre_process = TextAreaField('预处理信息(JSON对象)') + extension = TextAreaField('扩展字段') + remark = TextAreaField('备注') + file_exist = IntegerField('是否存在附件') + exist_quoto_info = TextAreaField('是否存在引用信息') + residue_date = TextAreaField('剩余天数') + whether_approval = StringField('是否审批', validators=[Length(max=10)]) + over_time_warning_flag = StringField('超时预警标志', validators=[Length(max=10)]) + create_no = StringField('创建编号', validators=[Length(max=20)]) + return_visit_reason = TextAreaField('回访原因') + back_count = StringField('回退次数', validators=[Length(max=100)]) + visit_adv_content = TextAreaField('走访建议内容') + is_dispatch_accurate = StringField('是否精准分派', validators=[Length(max=10)]) + process_instance_id = StringField('流程实例ID', validators=[Length(max=100)]) + knowledge_quote = TextAreaField('知识引用') + special_type = TextAreaField('特殊类型') + supervise_type = TextAreaField('监督类型') + leader_indicate = TextAreaField('领导批示') + case_solve = TextAreaField('处理结果') + result_satisfied = TextAreaField('结果满意度') + first_vist_satisfied = TextAreaField('首次走访满意度') + contact_timely = StringField('是否及时联系', validators=[Length(max=50)]) + distribute_type = StringField('分派类型', validators=[Length(max=50)]) + dept_type = TextAreaField('部门类型') + dept_name = TextAreaField('部门名称') + active_dept_name = StringField('当前处理部门名称', validators=[Length(max=50)]) + org_id = StringField('组织ID', validators=[Length(max=50)]) + org_name = TextAreaField('组织名称') + snapshot_time = DateTimeField('快照抓取时间') + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsOrderDetailBase(TD3iGovsOrderDetail, CommonModel): + """省12345工单详情业务基类""" + + FieldMapping = { + # ==================== 主键与关联 ==================== + 'master_id': 'masterId', # 关联工单主表ID(从外部传入) + 'order_id': 'orderId', # 工单编号 + 'order_no': 'orderNo', # 工单号 + 'tenant_id': 'tenantId', # 租户ID + + # ==================== 工单状态 ==================== + 'order_status': 'orderStatus', # 工单状态码 + 'order_status_for_view': 'orderStatusForView', # 工单状态显示值 + 'first_order_status': 'firstOrderStatus', # 一级状态编码 + 'secord_order_status': 'secordOrderStatus', # 二级状态编码 + 'atomic_order_status': 'atomicOrderStatus', # 原子状态编码 + 'order_invalid_type': 'orderInvalidType', # 工单作废原因 + 'order_finish_time': 'orderFinishTime', # 工单完成时间 + + # ==================== 诉求内容 ==================== + 'case_content': 'caseContent', # 诉求内容 + 'case_goal': 'caseGoal', # 诉求目的 + 'title': 'title', # 工单标题 + 'case_labels': 'caseLabels', # 工单标签列表 + 'case_public': 'casePublic', # 是否公开 + 'hotspot': 'hotspot', # 是否热点事件 + + # ==================== 紧急与保护 ==================== + 'case_is_urgent': 'caseIsUrgent', # 紧急程度(一般/紧急/特急) + 'case_is_visit': 'caseIsVisit', # 是否回访(是/否) + 'info_protect': 'infoProtect', # 信息保护(是/否) + + # ==================== 诉求归口分类 ==================== + 'case_accord_type_one_name': 'caseAccordTypeOneName', # 诉求归口一级 + 'case_accord_type_two_name': 'caseAccordTypeTwoName', # 诉求归口二级 + 'case_accord_type_three_name': 'caseAccordTypeThreeName', # 诉求归口三级 + 'case_accord_type_four_name': 'caseAccordTypeFourName', # 诉求归口四级 + 'case_accord_type_five_name': 'caseAccordTypeFiveName', # 诉求归口五级 + 'case_accord_code': 'caseAccordCode', # 事项编码 + + # ==================== 归属单位 ==================== + 'first_level_affiliation': 'firstLevelAffiliation', # 一级归属单位 + 'second_level_affiliation': 'secondLevelAffiliation', # 二级归属单位 + 'third_level_affiliation': 'thirdLevelAffiliation', # 三级归属单位 + 'fourth_level_affiliation': 'fourthLevelAffiliation', # 四级归属单位 + 'fifth_level_affiliation': 'fifthLevelAffiliation', # 五级归属单位 + 'sixth_level_affiliation': 'sixthLevelAffiliation', # 六级归属单位 + 'seventh_level_affiliation': 'seventhLevelAffiliation', # 七级归属单位 + 'appeal_dept': 'appealDept', # 诉求部门 + + # ==================== 来源与平台 ==================== + 'order_source': 'orderSource', # 诉求来源(电话/互联网) + 'order_source_detail': 'orderSourceDetail', # 诉求来源详情(12345/随手拍) + 'order_source_for_view': 'orderSourceForView', # 诉求来源显示值 + 'belong_platform': 'belongPlatform', # 所属平台代码 + 'belong_platform_name': 'belongPlatformName', # 受理平台名称 + 'current_processing_platform': 'currentProcessingPlatform', # 当前处理平台 + + # ==================== 服务对象与类型 ==================== + 'service_object_type': 'serviceObjectType', # 服务对象类型(投诉举报/咨询/建议等) + 'order_type': 'orderType', # 表单类型(个人/企业/其他) + 'form_type': 'formType', # 表单类型代码 + + # ==================== 区域信息 ==================== + 'area_code_city': 'areaCodeCity', # 市区域代码 + 'area_code_area': 'areaCodeArea', # 区区域代码 + 'area_code_street': 'areaCodeStreet', # 街道区域代码 + 'address_detail': 'addressDetail', # 详细地址 + 'case_lnglat': 'caseLnglat', # 地理坐标 + + # ==================== 联系方式 ==================== + 'call_number': 'callNumber', # 来电号码 + 'call_number_for_dh': 'callNumberForDH', # 来电号码(脱敏) + 'raw_call_numer': 'rawCallNumer', # 原始来电号码 + 'contact_number': 'contactNumber', # 联系电话 + 'raw_contact_number': 'rawContactNumber', # 原始联系电话 + 'contact_number_for_dh': 'contactNumberForDH', # 联系电话(脱敏) + 'call_time': 'callTime', # 来电时间 + 'order_sound_record_id': 'orderSoundRecordId', # 通话记录ID + + # ==================== 时间节点 ==================== + 'create_date': 'createDate', # 创建日期 + 'update_date': 'updateDate', # 更新日期 + 'plan_finish_time': 'planFinishTime', # 计划完成时间 + 'plan_sign_time': 'planSignTime', # 计划签收时间 + + # ==================== 判定与协调 ==================== + 'judgment_flag': 'judgmentFlag', # 判定标志 + 'is_coordination': 'isCoordination', # 是否协调 + 'coordination_time': 'coordinationTime', # 协调时间 + + # ==================== 第三方关联 ==================== + 'thrid_order_id': 'thridOrderId', # 第三方工单ID + 'relate_order_ids': 'relateOrderIds', # 关联工单ID列表 + 'relate_order_count': 'relateOrderCount', # 关联工单数量 + + # ==================== 用户相关 ==================== + 'order_user_id': 'orderUserId', # 用户ID(身份证号) + 'user_word': 'userWord', # 用户反馈 + 'show_flag': 'showFlag', # 显示标志 + 'origin_show': 'originShow', # 原始显示标志 + + # ==================== JSON 对象字段(需序列化存储) ==================== + 'order_user': 'orderUser', # 诉求人信息(JSON对象) + 'order_phone_dto': 'orderPhoneDTO', # 电话号码信息(JSON对象) + 'order_attachment_list': 'orderAttachmentList', # 附件列表(JSON数组) + 'pre_process_list': 'preProcessList', # 预处理流程列表(JSON数组) + 'tripartite_call_records': 'tripartiteCallRecords', # 三方通话记录(JSON对象) + 'tripartite_call_records_list': 'tripartiteCallRecordsList', # 三方通话记录列表(JSON数组) + 'order_custom_form_fields': 'orderCustomFormFields', # 自定义表单字段(JSON数组) + 'knowledge_references': 'knowledgeReferences', # 知识参考(JSON对象) + 'sound_recording_address_list': 'soundRecordingAddressList', # 录音文件路径列表(JSON数组) + 'active_dept_ids': 'activeDeptIds', # 当前处理部门ID列表 + 'attachment_ids': 'attachmentIds', # 附件ID列表 + 'attachment_list': 'attachmentList', # 附件列表JSON + 'contactor_list': 'contactorList', # 联系人列表(JSON数组) + 'tsjb_entry_info': 'tsjbEntryInfo', # 投诉举报入口信息(JSON对象) + 'order_erge_revoke_plug_dto_list': 'orderErgeRevokePlugDTOList', # 撤销插件信息(JSON数组) + 'order_environmental': 'orderEnvironmental', # 环境信息(JSON对象) + 'order_demands_dto': 'orderDemandsDTO', # 诉求DTO(JSON对象) + 'order_appeal_list': 'orderAppealList', # 申诉列表(JSON数组) + 'torder_process_list': 'torderProcessList', # 流程列表(JSON数组) + 'pre_process': 'preProcess', # 预处理信息(JSON对象) + 'extension': 'extension', # 扩展字段 + 'remark': 'remark', # 备注 + + # ==================== 其他字段 ==================== + 'file_exist': None, # 是否存在附件(根据 orderAttachmentList 计算) + 'exist_quoto_info': 'existQuotoInfo', # 是否存在引用信息 + 'residue_date': 'residueDate', # 剩余天数 + 'whether_approval': 'whetherApproval', # 是否审批 + 'over_time_warning_flag': 'overTimeWarningFlag', # 超时预警标志 + 'create_no': 'createNo', # 创建编号 + 'return_visit_reason': 'returnVisitReason', # 回访原因 + 'back_count': 'backCount', # 回退次数 + 'visit_adv_content': 'visitAdvContent', # 走访建议内容 + 'is_dispatch_accurate': 'isDispatchAccurate', # 是否精准分派 + 'process_instance_id': 'processInstanceId', # 流程实例ID + 'knowledge_quote': 'knowledgeQuote', # 知识引用 + 'special_type': 'specialType', # 特殊类型 + 'supervise_type': 'superviseType', # 监督类型 + 'leader_indicate': 'leaderIndicate', # 领导批示 + 'case_solve': 'caseSolve', # 处理结果 + 'result_satisfied': 'resultSatisfied', # 结果满意度 + 'first_vist_satisfied': 'firstVistSatisfied', # 首次走访满意度 + 'contact_timely': 'contactTimely', # 是否及时联系 + 'distribute_type': 'distributeType', # 分派类型 + 'dept_type': 'deptType', # 部门类型 + 'dept_name': 'deptName', # 部门名称 + 'active_dept_name': 'activeDeptName', # 当前处理部门名称 + 'org_id': 'orgId', # 组织ID + 'org_name': 'orgName', # 组织名称 + } + + @classmethod + async def exist_other(cls, id: Union[str, int], order_id: str = None, order_no: str = None): + """检查是否存在除当前详情外的其他同编号工单详情""" + _query = select(cls).where(cls.id != id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查找工单详情""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, order_id: str = None, order_no: str = None): + """检查工单详情是否已经存在""" + _query = select(cls) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """按参数搜索工单详情的基础方法""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + _name_likes = { + cls.order_status.key: '%{}%', + cls.order_source.key: '%{}%', + cls.case_accord_type_one_name.key: '%{}%', + cls.belong_platform_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.create_date.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """按参数搜索工单详情,返回分页格式数据""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_order_id(cls, data_df: pd.DataFrame): + """根据 order_id 判断数据是否存在""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + order_ids = data_df[cls.order_id.key].unique().tolist() + if not order_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.order_id).where(cls.order_id.in_(order_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + order_id_to_id_map = dict(zip(existing_df[cls.order_id.key], existing_df[cls.id.key])) + + mask_exists = data_df[cls.order_id.key].isin(existing_df[cls.order_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.order_id.key].map(order_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主表ID查找工单详情""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单编号查找详情""" + _query = select(cls).where(cls.order_id == order_id) + return await cls.query_first(_query) + + @classmethod + async def find_latest_snapshot(cls, order_id: str): + """查找最新的快照""" + _query = select(cls).where(cls.order_id == order_id).order_by(cls.snapshot_time.desc()) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_ids(cls, master_ids: list[Union[str, int]]): + """根据主表ID列表批量查找工单详情""" + _query = select(cls).where(cls.master_id.in_(master_ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + +@register_swagger_model +class GovsOrderDetail(GovsOrderDetailBase): + """省12345工单详情业务类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """创建新工单详情""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderDetailForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _existing is None, "工单编号或工单号已存在,不能重复创建。" + + _detail = cls().copy_from_dict(_form.data, skip_none=True).before_save() + _detail.snapshot_time = datetime.datetime.now() + if user: + _detail.created_by = user.username + _detail.updated_by = user.username + await _detail.async_save() + return _detail + + @classmethod + async def delete(cls, detail_id: Union[str, int]): + """删除工单详情""" + _detail: cls = await cls.async_find_by_id(detail_id) + assert _detail, f"根据 ID {detail_id} 未找到工单详情。" + + _del_query = delete(cls).where(cls.id == _detail.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除工单详情(工单号:{_detail.order_no},ID:{_detail.id}).') + return _detail + + @classmethod + async def modify(cls, detail_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改工单详情信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderDetailForm(formdata=kwargs) + _form.validate_form() + + _other = await cls.exist_other( + detail_id, + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _other is None, "工单编号或工单号已存在,不能重复修改。" + + _detail: cls = await cls.async_find_by_id(detail_id) + assert _detail, f'查无此工单详情信息。' + + _detail.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _detail.updated_by = user.username + await _detail.async_save() + return _detail + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量创建工单详情""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + details = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(details) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(details)} 条工单详情。") + return len(details) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改工单详情""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单详情。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存数据,自动处理新建和更新""" + _exists_df, _latest_df = await cls.exists_order_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count + + @classmethod + async def create_or_update(cls, user: RbacUser = None, **kwargs): + """创建或更新工单详情(单条)""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderDetailForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.find_by_order_id(_form.order_id.data) + if _existing: + _existing.copy_from_dict(_form.data, skip_none=True).before_save() + _existing.snapshot_time = datetime.datetime.now() + if user: + _existing.updated_by = user.username + await _existing.async_save() + return _existing + + _detail = cls().copy_from_dict(_form.data, skip_none=True).before_save() + _detail.snapshot_time = datetime.datetime.now() + if user: + _detail.created_by = user.username + _detail.updated_by = user.username + await _detail.async_save() + return _detail \ No newline at end of file diff --git a/models/govs_order_master.py b/models/govs_order_master.py new file mode 100644 index 0000000..bd4ef79 --- /dev/null +++ b/models/govs_order_master.py @@ -0,0 +1,481 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovsOrderMaster +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsOrderMasterForm(ModelForm): + """省12345工单主表表单验证类""" + + id = IntegerField('工单唯一ID') + belong_dept = StringField('所属部门', validators=[Length(max=100)]) + order_id = StringField('工单编号', validators=[Length(max=50)]) + order_no = StringField('工单号', validators=[Length(max=50)]) + order_type = IntegerField('表单类型') + order_source = StringField('诉求来源', validators=[Length(max=30)]) + order_source_detail = StringField('诉求来源详情', validators=[Length(max=30)]) + order_status = StringField('工单状态', validators=[Length(max=50)]) + order_user_id = StringField('用户ID', validators=[Length(max=50)]) + order_user_name = StringField('来电人姓名', validators=[Length(max=50)]) + order_user_sex = StringField('来电人性别', validators=[Length(max=50)]) + order_user_phone2 = StringField('备用联系电话', validators=[Length(max=20)]) + order_handle_way = StringField('处理方式', validators=[Length(max=50)]) + order_invalid_type = StringField('工单作废原因', validators=[Length(max=50)]) + master_id = IntegerField('工单主表ID') + call_number = StringField('来电号码', validators=[Length(max=20)]) + contact_number = StringField('联系电话', validators=[Length(max=20)]) + title = StringField('工单标题', validators=[Length(max=100)]) + call_time = DateTimeField('来电时间') + first_order_status = StringField('一级状态编码', validators=[Length(max=10)]) + secord_order_status = StringField('二级状态编码', validators=[Length(max=10)]) + atomic_order_status = StringField('原子状态编码', validators=[Length(max=10)]) + area_code = StringField('区域代码', validators=[Length(max=10)]) + area_code_city = StringField('市区域代码', validators=[Length(max=50)]) + area_code_area = StringField('区区域代码', validators=[Length(max=50)]) + area_code_street = StringField('街道区域代码', validators=[Length(max=50)]) + address_detail = TextAreaField('详细地址') + case_lnglat = StringField('地理坐标', validators=[Length(max=50)]) + case_accord_type_one_name = StringField('诉求归口一级', validators=[Length(max=50)]) + case_accord_type_two_name = StringField('诉求归口二级', validators=[Length(max=50)]) + case_accord_type_three_name = StringField('诉求归口三级', validators=[Length(max=50)]) + case_accord_type_four_name = StringField('四级事项分类', validators=[Length(max=50)]) + case_accord_type_five_name = StringField('五级事项分类', validators=[Length(max=50)]) + case_content = TextAreaField('诉求内容') + case_goal = TextAreaField('诉求目的') + case_labels = TextAreaField('工单标签列表') + case_public = StringField('是否公开', validators=[Length(max=10)]) + case_is_urgent = IntegerField('紧急程度') + case_comple_time = DateTimeField('案件办结时间') + first_level_affiliation = StringField('一级归属单位', validators=[Length(max=50)]) + second_level_affiliation = StringField('二级归属单位', validators=[Length(max=50)]) + third_level_affiliation = StringField('三级归属单位', validators=[Length(max=50)]) + fourth_level_affiliation = StringField('四级归属单位', validators=[Length(max=50)]) + fifth_level_affiliation = StringField('五级归属单位', validators=[Length(max=50)]) + case_accord_code = StringField('事项编码', validators=[Length(max=50)]) + sixth_level_affiliation = StringField('六级归属单位', validators=[Length(max=50)]) + seventh_level_affiliation = StringField('七级归属单位', validators=[Length(max=50)]) + info_protect = IntegerField('信息保护') + case_is_visit = IntegerField('是否回访') + service_object_type = IntegerField('服务对象类型') + hotspot = StringField('是否热点事件', validators=[Length(max=10)]) + result_satisfied = StringField('结果满意度', validators=[Length(max=10)]) + first_vist_satisfied = StringField('首次走访满意度', validators=[Length(max=10)]) + contact_timely = StringField('是否及时联系', validators=[Length(max=50)]) + distribute_type = StringField('分派类型', validators=[Length(max=50)]) + active_dept_ids = StringField('当前处理部门ID列表', validators=[Length(max=255)]) + active_dept_name = StringField('当前处理部门名称', validators=[Length(max=50)]) + case_solve = TextAreaField('处理结果') + supervise_type = StringField('监督类型', validators=[Length(max=30)]) + leader_indicate = TextAreaField('领导批示') + extension = TextAreaField('扩展字段') + org_id = StringField('组织ID', validators=[Length(max=50)]) + org_name = StringField('组织名称', validators=[Length(max=50)]) + knowledge_quote = TextAreaField('知识引用') + special_type = StringField('特殊类型', validators=[Length(max=30)]) + attachment_ids = TextAreaField('附件ID列表') + file_exist = IntegerField('是否存在附件') + record_id = StringField('通话记录ID', validators=[Length(max=50)]) + call_end_time = DateTimeField('通话结束时间') + call_total_time = StringField('通话总时长', validators=[Length(max=20)]) + plan_finish_time = DateTimeField('计划完成时间') + remark = TextAreaField('备注') + tenant_id = IntegerField('租户ID') + process_instance_id = StringField('流程实例ID', validators=[Length(max=100)]) + visit_count = IntegerField('走访次数') + residue_date = StringField('剩余天数', validators=[Length(max=30)]) + whether_approval = StringField('是否审批', validators=[Length(max=10)]) + over_time_warning_flag = StringField('超时预警标志', validators=[Length(max=10)]) + create_no = StringField('创建编号', validators=[Length(max=20)]) + belong_platform = StringField('所属平台', validators=[Length(max=50)]) + next_task_id = StringField('下一个任务ID', validators=[Length(max=64)]) + return_visit_reason = TextAreaField('回访原因') + back_count = StringField('回退次数', validators=[Length(max=100)]) + visit_adv_content = TextAreaField('走访建议内容') + current_processing_platform = StringField('当前处理平台', validators=[Length(max=100)]) + judgment_flag = StringField('判定标志', validators=[Length(max=10)]) + thrid_order_id = StringField('第三方工单ID', validators=[Length(max=50)]) + is_dispatch_accurate = StringField('是否精准分派', validators=[Length(max=10)]) + is_coordination = StringField('是否协调', validators=[Length(max=10)]) + coordination_time = DateTimeField('协调时间') + govs_sign = IntegerField('是否已在省12345签收,1:签收,0:未签收') + creator_id = IntegerField('创建人ID') + create_by = StringField('创建人姓名', validators=[Length(max=50)]) + updator_id = IntegerField('更新人ID') + update_by = StringField('更新人姓名', validators=[Length(max=50)]) + create_date = DateTimeField('工单创建时间') + update_date = DateTimeField('工单更新时间') + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsOrderMasterBase(TD3iGovsOrderMaster, CommonModel): + """省12345工单主表业务基类""" + + FieldMapping = { + "id": "id", + 'plan_sign_time': 'planSignTime', + 'claim_status': 'claimStatus', + 'plan_back_time': 'planBackTime', + 'handle_time': 'handleTime', + 'back_time': 'backTime', + 'complete_time': 'completeTime', + "belong_dept": "belongDept", + "order_id": "orderId", + "order_no": "orderNo", + "order_type": "orderType", + "order_source": "orderSource", + "order_source_detail": "orderSourceDetail", + "order_status": "orderStatus", + "order_user_id": "orderUserId", + "order_user_name": "orderUserName", + "order_user_sex": "orderUserSex", + "order_user_phone2": "orderUserPhone2", + "order_handle_way": "orderHandleWay", + "order_invalid_type": "orderInvalidType", + "next_task_id": "nextTaskId", + "master_id": "masterId", + "call_number": "callNumber", + "contact_number": "contactNumber", + "title": "title", + "call_time": "callTime", + "first_order_status": "firstOrderStatus", + "secord_order_status": "secordOrderStatus", + "atomic_order_status": "atomicOrderStatus", + "area_code": "areaCode", + "area_code_city": "areaCodeCity", + "area_code_area": "areaCodeArea", + "area_code_street": "areaCodeStreet", + "address_detail": "addressDetail", + "case_lnglat": "caseLnglat", + "case_accord_type_one_name": "caseAccordTypeOneName", + "case_accord_type_two_name": "caseAccordTypeTwoName", + "case_accord_type_three_name": "caseAccordTypeThreeName", + "case_accord_type_four_name": "caseAccordTypeFourName", + "case_accord_type_five_name": "caseAccordTypeFiveName", + "case_accord_ext": "caseAccordExt", + "case_content": "caseContent", + "case_goal": "caseGoal", + "case_labels": "caseLabels", + "case_public": "casePublic", + "case_type": "caseType", + "case_is_urgent": "caseIsUrgent", + "case_comple_time": "caseCompleTime", + "first_level_affiliation": "firstLevelAffiliation", + "second_level_affiliation": "secondLevelAffiliation", + "third_level_affiliation": "thirdLevelAffiliation", + "fourth_level_affiliation": "fourthLevelAffiliation", + "fifth_level_affiliation": "fifthLevelAffiliation", + "case_accord_code": "caseAccordCode", + "sixth_level_affiliation": "sixthLevelAffiliation", + "seventh_level_affiliation": "seventhLevelAffiliation", + "info_protect": "infoProtect", + "case_is_visit": "caseIsVisit", + "service_object_type": "serviceObjectType", + "hotspot": "hotspot", + "result_satisfied": "resultSatisfied", + "first_vist_satisfied": "firstVistSatisfied", + "contact_timely": "contactTimely", + "distribute_type": "distributeType", + "dept_type": "deptType", + "dept_name": "deptName", + "active_dept_ids": "activeDeptIds", + "active_dept_name": "activeDeptName", + "case_solve": "caseSolve", + "supervise_type": "superviseType", + "leader_indicate": "leaderIndicate", + "extension": "extension", + "org_id": "orgId", + "org_name": "orgName", + "knowledge_quote": "knowledgeQuote", + "special_type": "specialType", + "attachment_ids": "attachmentIds", + "attachment_list": "attachmentList", + "file_exist": "fileExist", + "record_id": "recordId", + "call_end_time": "callEndTime", + "call_total_time": "callTotalTime", + "plan_finish_time": "planFinishTime", + "remark": "remark", + "tenant_id": "tenantId", + "erge_revoke_plug": "ergeRevokePlug", + "exist_quoto_info": "existQuotoInfo", + "process_instance_id": "processInstanceId", + "sound_recording_address_list": "soundRecordingAddressList", + "visit_count": "visitCount", + "residue_date": "residueDate", + "whether_approval": "whetherApproval", + "over_time_warning_flag": "overTimeWarningFlag", + "create_no": "createNo", + "belong_platform": "belongPlatform", + "return_visit_reason": "returnVisitReason", + "back_count": "backCount", + "visit_adv_content": "visitAdvContent", + "tripartite_call_record_info": "tripartiteCallRecordInfo", + "knowledge_references": "knowledgeReferences", + "current_processing_platform": "currentProcessingPlatform", + "judgment_flag": "judgmentFlag", + "thrid_order_id": "thridOrderId", + "is_dispatch_accurate": "isDispatchAccurate", + "is_coordination": "isCoordination", + "coordination_time": "coordinationTime", + "creator_id": "creatorId", + "create_by": "createBy", + "updator_id": "updatorId", + "update_by": "updateBy" + } + + @classmethod + async def exist_other(cls, id: Union[str, int], order_id: str = None, order_no: str = None): + """检查是否存在除当前工单外的其他同编号工单""" + _query = select(cls).where(cls.id != id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查找工单""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, order_id: str = None, order_no: str = None): + """检查工单是否已经存在""" + _query = select(cls) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """按参数搜索工单的基础方法""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + _name_likes = { + cls.order_status.key: '%{}%', + cls.order_source.key: '%{}%', + cls.case_accord_type_one_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.create_date.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """按参数搜索工单,返回分页格式数据""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_order_id(cls, data_df: pd.DataFrame): + """根据 order_id 判断数据是否存在""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + order_ids = data_df[cls.order_id.key].unique().tolist() + if not order_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.order_id).where(cls.order_id.in_(order_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + order_id_to_id_map = dict(zip(existing_df[cls.order_id.key], existing_df[cls.id.key])) + + mask_exists = data_df[cls.order_id.key].isin(existing_df[cls.order_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.order_id.key].map(order_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + +@register_swagger_model +class GovsOrderMaster(GovsOrderMasterBase): + """省12345工单主表业务类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """创建新工单""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderMasterForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _existing is None, "工单编号或工单号已存在,不能重复创建。" + + _task = cls().copy_from_dict(_form.data, skip_none=True) + if user: + _task.created_by = user.username + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def delete(cls, task_id: Union[str, int]): + """删除工单""" + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f"根据 ID {task_id} 未找到工单。" + + _del_query = delete(cls).where(cls.id == _task.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除工单(工单号:{_task.order_no},ID:{_task.id}).') + return _task + + @classmethod + async def modify(cls, task_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改工单信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderMasterForm(formdata=kwargs) + _form.validate_form() + + _other = await cls.exist_other( + task_id, + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _other is None, "工单编号或工单号已存在,不能重复修改。" + + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f'查无此工单信息。' + + _task.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量创建工单""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + tasks = [cls().copy_from_dict(record, skip_none=True) for record in records] + + session = cls.get_aio_session() + try: + session.add_all(tasks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(tasks)} 条工单。") + return len(tasks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改工单""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存数据,自动处理新建和更新""" + _exists_df, _latest_df = await cls.exists_order_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govs_order_process.py b/models/govs_order_process.py new file mode 100644 index 0000000..eae30b4 --- /dev/null +++ b/models/govs_order_process.py @@ -0,0 +1,519 @@ +# coding: utf-8 +import datetime +import random +from typing import Union,Optional,Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovsOrderProces +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsOrderProcessForm(ModelForm): + """省12345工单处理流程表单验证类""" + + id = IntegerField('工单处理记录唯一ID') + master_id = IntegerField('主工单ID') + tenant_id = IntegerField('租户ID') + plan_sign_time = DateTimeField('计划签收时间') + plan_finish_time = DateTimeField('计划完成时间') + plan_back_time = DateTimeField('计划退回时间') + deal_date = DateTimeField('实际处理时间') + hand_over_time = StringField('交接时间', validators=[Length(max=20)]) + sign_over_time = StringField('签收超时时间', validators=[Length(max=20)]) + origin_plan_finish_time = DateTimeField('原始计划完成时间') + origin_plan_sign_time = DateTimeField('原始计划签收时间') + order_id = StringField('工单编号', validators=[Length(max=50)]) + order_no = StringField('工单流水号', validators=[Length(max=100)]) + process_instance_id = StringField('流程实例ID', validators=[Length(max=64)]) + order_status = StringField('工单状态编码', validators=[Length(max=10)]) + is_over_time = IntegerField('是否超期') + is_sign_over_time = IntegerField('是否签收超时') + action_name = StringField('当前操作动作名称', validators=[Length(max=100)]) + deal_type = StringField('处理类型', validators=[Length(max=100)]) + task_id = StringField('当前任务ID', validators=[Length(max=64)]) + next_task_id = StringField('下一任务ID', validators=[Length(max=64)]) + next_action_name = StringField('下一处理动作名称', validators=[Length(max=100)]) + next_handle = StringField('下一处理动作名称', validators=[Length(max=50)]) + next_handle_name = StringField('下一处理动作详细名称', validators=[Length(max=100)]) + handler_user_ids = StringField('当前处理人ID列表', validators=[Length(max=500)]) + handler_user_names = StringField('当前处理人姓名列表', validators=[Length(max=500)]) + handler_org_ids = StringField('当前处理部门ID列表', validators=[Length(max=1000)]) + handler_org_names = StringField('当前处理部门名称列表', validators=[Length(max=500)]) + next_handler_user_ids = StringField('下一处理人ID列表', validators=[Length(max=500)]) + next_handler_user_names = StringField('下一处理人姓名列表', validators=[Length(max=500)]) + next_org_ids = StringField('下一处理部门ID列表', validators=[Length(max=500)]) + next_org_names = StringField('下一处理部门名称列表', validators=[Length(max=500)]) + dispatch_order_id = StringField('派发工单ID', validators=[Length(max=100)]) + to_master_id = IntegerField('目标主表ID') + to_tenant_id = IntegerField('目标租户ID') + to_area_code = StringField('目标区域代码', validators=[Length(max=20)]) + to_dept_id = IntegerField('目标部门ID') + dispatch_value = StringField('派发值', validators=[Length(max=20)]) + has_dispatch_process = IntegerField('是否有派发流程') + contact_name = StringField('联系人姓名', validators=[Length(max=100)]) + contact_time = DateTimeField('联系时间') + contact_type = StringField('联系类型', validators=[Length(max=20)]) + adv_content = TextAreaField('处理建议') + remarks = TextAreaField('备注信息') + formal_reply = TextAreaField('正式回复内容') + reply_to_people = StringField('回复对象', validators=[Length(max=100)]) + return_reason = StringField('退回原因', validators=[Length(max=500)]) + notice_org_id = IntegerField('通知组织ID') + line_key = StringField('线路标识', validators=[Length(max=100)]) + current_task_status = StringField('当前任务状态', validators=[Length(max=50)]) + visit_type = StringField('访问类型', validators=[Length(max=50)]) + attachment_dto_list = TextAreaField('附件列表JSON') + child_order_processes = TextAreaField('子流程处理记录JSON') + order_process_index_list = TextAreaField('工单流程索引列表JSON') + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64)]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsOrderProcessBase(TD3iGovsOrderProces, CommonModel): + """省12345工单处理流程业务基类""" + + FieldMapping = { + # ==================== 主键与关联 ==================== + 'id': 'id', + 'master_id': 'masterId', + 'tenant_id': 'tenantId', + + # ==================== 时间节点 ==================== + 'plan_sign_time': 'planSignTime', + 'plan_finish_time': 'planFinishTime', + 'plan_back_time': 'planBackTime', + 'deal_date': 'dealDate', + 'hand_over_time': 'handOverTime', + 'sign_over_time': 'signOverTime', + 'origin_plan_finish_time': 'originPlanFinishTime', + 'origin_plan_sign_time': 'originPlanSignTime', + + # ==================== 工单标识 ==================== + 'order_id': 'orderId', + 'order_no': 'orderNo', + 'process_instance_id': 'processInstanceId', + + # ==================== 工单状态 ==================== + 'order_status': 'orderStatus', + 'is_over_time': 'isOverTime', + 'is_sign_over_time': 'isSignOverTime', + + # ==================== 处理动作 ==================== + 'action_name': 'actionName', + 'deal_type': 'dealType', + 'task_id': 'taskId', + 'next_task_id': 'nextTaskId', + 'next_action_name': 'nextActionName', + 'next_handle': 'nextHandle', + 'next_handle_name': 'nextHandleName', + + # ==================== 当前处理人/部门 ==================== + 'handler_user_ids': 'handlerUserIds', + 'handler_user_names': 'handlerUserNames', + 'handler_org_ids': 'handlerOrgIds', + 'handler_org_names': 'handlerOrgNames', + + # ==================== 下一处理人/部门 ==================== + 'next_handler_user_ids': 'nextHandlerUserIds', + 'next_handler_user_names': 'nextHandlerUserNames', + 'next_org_ids': 'nextOrgIds', + 'next_org_names': 'nextOrgNames', + + # ==================== 派发信息 ==================== + 'dispatch_order_id': 'dispatchOrderId', + 'to_master_id': 'toMasterId', + 'to_tenant_id': 'toTenantId', + 'to_area_code': 'toAreaCode', + 'to_dept_id': 'toDeptId', + 'dispatch_value': 'dispatchValue', + 'has_dispatch_process': 'hasDispatchProcess', + + # ==================== 联系信息 ==================== + 'contact_name': 'contactName', + 'contact_time': 'contactTime', + 'contact_type': 'contactType', + + # ==================== 内容字段 ==================== + 'adv_content': 'advContent', + 'remarks': 'remarks', + 'formal_reply': 'formalReply', + 'reply_to_people': 'replyToPeople', + 'return_reason': 'returnReason', + + # ==================== 其他字段 ==================== + 'notice_org_id': 'noticeOrgId', + 'line_key': 'lineKey', + 'current_task_status': 'currentTaskStatus', + 'visit_type': 'visitType', + + # ==================== JSON 嵌套字段 ==================== + 'attachment_dto_list': 'attachmentDTOList', + 'child_order_processes': 'childOrderProcesses', + } + + @classmethod + async def exist_other(cls, id: Union[str, int], order_id: str = None, order_no: str = None): + """检查是否存在除当前记录外的其他同编号处理流程""" + _query = select(cls).where(cls.id != id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查找处理流程""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, order_id: str = None, order_no: str = None): + """检查处理流程是否已经存在""" + _query = select(cls) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """按参数搜索处理流程的基础方法""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + _name_likes = { + cls.order_status.key: '%{}%', + cls.action_name.key: '%{}%', + cls.deal_type.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """按参数搜索处理流程,返回分页格式数据""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_master_id(cls, data_df: pd.DataFrame): + """根据 master_id 判断数据是否存在 + :param data_df: 输入的数据框架,必须包含 id 和 master_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录(附带数据库中的 id) + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 (id, master_id) 组合 + pairs = data_df[[cls.id.key, cls.master_id.key]].drop_duplicates().values.tolist() + if not pairs: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录(使用 IN 批量查询) + _query = select(cls.id, cls.master_id).where( + (cls.id.in_([p[0] for p in pairs])) & + (cls.master_id.in_([p[1] for p in pairs])) + ) + exists_db_df = await cls.query_as_df(_query) + + if exists_db_df.empty: + return pd.DataFrame(), data_df.copy() + + exists_db_df[cls.id.key] = exists_db_df[cls.id.key].astype(str) + exists_db_df[cls.master_id.key] = exists_db_df[cls.master_id.key].astype(str) + # 构建 (id, master_id) -> id 的映射(用于快速查找) + key_to_id_map = dict(zip( + zip(exists_db_df[cls.id.key], exists_db_df[cls.master_id.key]), + exists_db_df[cls.id.key] + )) + + # 标记 data_df 中哪些行在数据库中存在 + mask_exists = data_df.apply( + lambda row: (row[cls.id.key], row[cls.master_id.key]) in key_to_id_map, + axis=1 + ) + # 提取存在的记录,并补充数据库中的 id(虽然输入中已有 id,但为一致性保留) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df.apply( + lambda row: key_to_id_map[(row[cls.id.key], row[cls.master_id.key])], + axis=1 + ) + # 提取不存在的记录 + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单编号查找处理流程""" + _query = select(cls).where(cls.order_id == order_id) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主工单ID查找处理流程""" + _query = select(cls).where(cls.master_id == master_id) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def find_latest_by_order_id(cls, order_id: str): + """根据工单编号查找最新的处理流程""" + _query = select(cls).where(cls.order_id == order_id).order_by(cls.deal_date.desc()) + return await cls.query_first(_query) + + @classmethod + async def fill_process_info(cls, data_df: pd.DataFrame, index_field: str = 'id', + column_name: str = 'process_infos', + preprocessing: Optional[Callable] = None): + """ + 填充办理过程数据到数据框架。 + + 用于在查询结果中添加关联的办理过程信息。 + + :param pandas.DataFrame data_df: 待填充的数据框架 + :param str index_field: 索引字段,一般是任务ID + :param str column_name: 填充时,新增加的列名称,默认为`process_infos` + :param preprocessing: 预处理,注意预处理必须要返回处理后的结果 + :return: 办理过程数据框架(已填充) + :rtype: pandas.DataFrame + """ + if data_df.empty: + return pd.DataFrame() + + _task_ids = list(set(data_df[index_field].unique().tolist())) + if not _task_ids: + return pd.DataFrame() + + _query = select(cls).where(cls.master_id.in_(_task_ids)) + _info_df: pd.DataFrame = await cls.query_as_df(_query) + if not _info_df.empty: + _info_df.replace(models.EmptyInDF+models.EmptyDatetimeInDF, '', inplace=True) + # 整理输出数据类型 + _info_df[cls.id.key] = _info_df[cls.id.key].astype(str) + _info_df[cls.master_id.key] = _info_df[cls.master_id.key].astype(str) + + # 设置索引 + _info_df['index_id'] = _info_df[cls.master_id.key] + _info_df.set_index(['index_id'], inplace=True) + # 对数据进行预处理 + if isinstance(preprocessing, Callable): + _info_df = preprocessing(_info_df) + # 增加数据填充列 + data_df[column_name] = data_df[index_field].apply( + lambda x: _info_df.query(f"{cls.master_id.key}=='{x}'").to_dict('records') + ) + else: + data_df[column_name] = [[] for _ in range(len(data_df))] + + return _info_df + + +@register_swagger_model +class GovsOrderProcess(GovsOrderProcessBase): + """省12345工单处理流程业务类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """创建新处理流程记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderProcessForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + if _existing: + # 如果存在相同 order_id 的记录,更新它 + return await cls.modify(_existing.id, user, **kwargs) + + _task = cls().copy_from_dict(_form.data, skip_none=True) + if user: + _task.created_by = user.username + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def delete(cls, task_id: Union[str, int]): + """删除处理流程记录""" + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f"根据 ID {task_id} 未找到处理流程记录。" + + _del_query = delete(cls).where(cls.id == _task.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除处理流程记录(工单号:{_task.order_no},ID:{_task.id}).') + return _task + + @classmethod + async def modify(cls, task_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改处理流程信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderProcessForm(formdata=kwargs) + _form.validate_form() + + _other = await cls.exist_other( + task_id, + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + if _other: + # 如果存在其他相同编号的记录,不重复创建 + echo_log(f"处理流程记录已存在(工单号:{_form.order_no.data}),跳过创建。") + return _other + + _task: cls = await cls.async_find_by_id(task_id) + assert _task, f'查无此处理流程信息。' + + _task.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _task.updated_by = user.username + await _task.async_save() + return _task + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量创建处理流程记录""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + tasks = [cls().copy_from_dict(record, skip_none=True) for record in records] + + session = cls.get_aio_session() + try: + session.add_all(tasks) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(tasks)} 条处理流程记录。") + return len(tasks) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改处理流程记录""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条处理流程记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存数据,自动处理新建和更新""" + _exists_df, _latest_df = await cls.exists_master_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count + + @classmethod + async def delete_by_order_id(cls, order_id: str): + """根据工单编号删除处理流程记录""" + _del_query = delete(cls).where(cls.order_id == order_id) + result = await cls.raw_execute(_del_query) + echo_log(f'已删除工单 {order_id} 的处理流程记录,共 {result.rowcount} 条.') + return result.rowcount + + @classmethod + async def delete_by_master_id(cls, master_id: Union[str, int]): + """根据主工单ID删除处理流程记录""" + _del_query = delete(cls).where(cls.master_id == master_id) + result = await cls.raw_execute(_del_query) + echo_log(f'已删除主工单 {master_id} 的处理流程记录,共 {result.rowcount} 条.') + return result.rowcount diff --git a/models/govs_order_user.py b/models/govs_order_user.py new file mode 100644 index 0000000..b25a5e1 --- /dev/null +++ b/models/govs_order_user.py @@ -0,0 +1,329 @@ +# coding: utf-8 +import datetime +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +from models.common_model import CommonModel +from models.db_models import TD3iGovsOrderUser +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.web.form import ModelForm + + +class GovsOrderUserForm(ModelForm): + """服务对象信息表单验证类""" + + id = IntegerField('服务对象唯一ID') + master_id = IntegerField('关联工单主表ID') + order_id = StringField('工单编号', validators=[Length(max=50)]) + order_no = StringField('工单号', validators=[Length(max=50)]) + customer_id = StringField('服务对象ID', validators=[Length(max=50)]) + customer_name = StringField('姓名', validators=[Length(max=50)]) + customer_sex = StringField('性别', validators=[Length(max=10)]) + customer_connect_phone = StringField('联系电话', validators=[Length(max=20)]) + customer_credentials_type = StringField('证件类型', validators=[Length(max=20)]) + customer_credentials_no = StringField('证件号码', validators=[Length(max=50)]) + customer_address = StringField('联系地址', validators=[Length(max=200)]) + customer_email = StringField('电子邮箱', validators=[Length(max=100)]) + customer_age = IntegerField('年龄') + customer_occupation = StringField('职业', validators=[Length(max=50)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsOrderUserBase(TD3iGovsOrderUser, CommonModel): + """服务对象信息业务基类""" + + FieldMapping = { + # ==================== 主键与关联 ==================== + 'id': 'id', # 服务对象唯一ID + 'master_id': 'masterId', # 关联工单主表ID(需从外部传入) + 'order_id': 'orderId', # 工单编号 + 'tenant_id': 'tenantId', # 租户ID + 'area_code': 'areaCode', # 区域代码 + + # ==================== 基本信息 ==================== + 'customer_name': 'customerName', # 姓名 + 'raw_customer_name': 'rawCustomerName', # 原始姓名 + 'customer_sex': 'customerSex', # 性别(男/女/未知) + 'customer_type': 'customerType', # 客户类型(个人/企业) + 'customer_age_range': 'customerAgeRange', # 年龄段 + + # ==================== 联系方式 ==================== + 'customer_connect_phone': 'customerConnectPhone', # 联系电话 + 'raw_customer_connect_phone': 'rawCustomerConnectPhone', # 原始联系电话 + 'customer_incoming_phone': 'customerIncomingPhone', # 来电号码 + 'raw_customer_incoming_phone': 'rawCustomerIncomingPhone', # 原始来电号码 + 'customer_phone_backup': 'customerPhoneBackup', # 备用电话 + 'raw_customer_phone_backup': 'rawCustomerPhoneBackup', # 原始备用电话 + 'customer_phone_backup_for_dh': 'customerPhoneBackupForDH', # 备用电话(脱敏) + 'customer_internet_nickname': 'customerInternetNickname', # 网名 + 'customer_email': 'customerEmail', # 电子邮箱 + + # ==================== 证件信息 ==================== + 'customer_credentials_type': 'customerCredentialsType', # 证件类型(如:身份证、护照) + 'customer_credentials_no': 'customerCredentialsNo', # 证件号码 + 'raw_customer_credentials_no': 'rawCustomerCredentialsNo', # 原始证件号码 + + # ==================== 企业信息 ==================== + 'enterprise_type': 'enterpriseType', # 企业类型 + 'enterprise_name': 'enterpriseName', # 企业名称 + 'enterprise_register_address': 'enterpriseRegisterAddress', # 企业注册地址 + 'enterprise_address': 'enterpriseAddress', # 企业地址 + 'enterprise_credit_code': 'enterpriseCreditCode', # 企业信用代码 + + # ==================== 系统字段 ==================== + 'delete_flag': 'deleteFlag', # 删除标志(0-未删除,1-已删除) + + # ==================== 系统信息 ==================== + 'created_at': 'createDate', + 'created_by': 'createBy', + 'updated_at': 'updateDate', + 'updated_by': 'updateBy', + } + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单编号查找服务对象""" + _query = select(cls).where(cls.order_id == order_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主工单ID查找服务对象""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_customer_id(cls, customer_id: str): + """根据服务对象ID查找""" + _query = select(cls).where(cls.customer_id == customer_id) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def find_by_phone(cls, phone: str): + """根据联系电话查找""" + _query = select(cls).where(cls.customer_connect_phone == phone) + return (await cls.orm_execute_scalars(_query)).all() + + @classmethod + async def exists_order_id(cls, data_df: pd.DataFrame): + """根据 order_id 判断数据是否存在 + :param data_df: 输入的数据框架,必须包含 id 和 order_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录(附带数据库中的 id) + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 (id, order_id) 组合 + pairs = data_df[[cls.id.key, cls.order_id.key]].drop_duplicates().values.tolist() + if not pairs: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录(使用 IN 批量查询) + _query = select(cls.id, cls.order_id).where( + (cls.id.in_([p[0] for p in pairs])) & + (cls.order_id.in_([p[1] for p in pairs])) + ) + exists_db_df = await cls.query_as_df(_query) + + if exists_db_df.empty: + return pd.DataFrame(), data_df.copy() + + exists_db_df[cls.id.key] = exists_db_df[cls.id.key].astype(str) + exists_db_df[cls.order_id.key] = exists_db_df[cls.order_id.key].astype(str) + # 构建 (id, order_id) -> id 的映射(用于快速查找) + key_to_id_map = dict(zip( + zip(exists_db_df[cls.id.key], exists_db_df[cls.order_id.key]), + exists_db_df[cls.id.key] + )) + + # 标记 data_df 中哪些行在数据库中存在 + mask_exists = data_df.apply( + lambda row: (row[cls.id.key], row[cls.order_id.key]) in key_to_id_map, + axis=1 + ) + # 提取存在的记录,并补充数据库中的 id(虽然输入中已有 id,但为一致性保留) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df.apply( + lambda row: key_to_id_map[(row[cls.id.key], row[cls.order_id.key])], + axis=1 + ) + # 提取不存在的记录 + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + +@register_swagger_model +class GovsOrderUser(GovsOrderUserBase): + """服务对象信息业务类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """创建服务对象信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderUserForm(formdata=kwargs) + _form.validate_form() + + # 检查是否已存在(根据 order_id) + _existing = await cls.find_by_order_id(_form.order_id.data) + if _existing: + # 更新已有记录 + _existing.copy_from_dict(_form.data, skip_none=True) + if user: + _existing.updated_by = user.username + await _existing.async_save() + return _existing + + _obj = cls().copy_from_dict(_form.data, skip_none=True) + if user: + _obj.created_by = user.username + _obj.updated_by = user.username + await _obj.async_save() + return _obj + + @classmethod + async def delete(cls, obj_id: Union[str, int]): + """删除服务对象信息""" + _obj: cls = await cls.async_find_by_id(obj_id) + assert _obj, f"根据 ID {obj_id} 未找到服务对象信息。" + + _del_query = delete(cls).where(cls.id == _obj.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除服务对象信息(order_id:{_obj.order_id},ID:{_obj.id}).') + return _obj + + @classmethod + async def modify(cls, obj_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改服务对象信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderUserForm(formdata=kwargs) + _form.validate_form() + + _obj: cls = await cls.async_find_by_id(obj_id) + assert _obj, f'查无此服务对象信息。' + + _obj.copy_from_dict(_form.data, skip_none=True) + if user: + _obj.updated_by = user.username + await _obj.async_save() + return _obj + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量创建服务对象信息""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + objs = [cls().copy_from_dict(record, skip_none=True) for record in records] + + session = cls.get_aio_session() + try: + session.add_all(objs) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(objs)} 条服务对象信息。") + return len(objs) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改服务对象信息""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条服务对象信息。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存数据,自动处理新建和更新""" + _exists_df, _latest_df = await cls.exists_order_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count + + @classmethod + async def create_or_update_by_order_id(cls, user: RbacUser = None, **kwargs): + """根据 order_id 创建或更新服务对象信息""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsOrderUserForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.find_by_order_id(_form.order_id.data) + if _existing: + _existing.copy_from_dict(_form.data, skip_none=True) + if user: + _existing.updated_by = user.username + await _existing.async_save() + return _existing + + _obj = cls().copy_from_dict(_form.data, skip_none=True) + if user: + _obj.created_by = user.username + _obj.updated_by = user.username + await _obj.async_save() + return _obj + + @classmethod + async def delete_by_order_id(cls, order_id: str): + """根据工单编号删除服务对象信息""" + _obj = await cls.find_by_order_id(order_id) + if _obj: + await cls.delete(_obj.id) + return _obj \ No newline at end of file diff --git a/models/govs_phase_wise_completion.py b/models/govs_phase_wise_completion.py new file mode 100644 index 0000000..7cf951b --- /dev/null +++ b/models/govs_phase_wise_completion.py @@ -0,0 +1,340 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, TextAreaField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.db_models import TD3iGovsPhaseWiseCompletion +from models.common_model import CommonModel +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsPhaseWiseCompletionForm(ModelForm): + """阶段性办结表单验证类""" + + id = IntegerField('主键') + master_id = IntegerField('主表ID') + master_id = StringField('代签收唯一标志', validators=[Length(max=64)]) + is_contact = StringField('联系诉求人情况', validators=[Length(max=10)]) + contact_name = StringField('联系人员', validators=[Length(max=100)]) + contact_time = StringField('联系时间', validators=[Length(max=64)]) + contact_type = StringField('联系情况', validators=[Length(max=255)]) + next_feedback_time = StringField('下一次反馈时间', validators=[Length(max=64)]) + advice = TextAreaField('处理意见') + reason = TextAreaField('处理意见1') + remark = StringField('备注', validators=[Length(max=500)]) + file_id_str = TextAreaField('OA文件id') + action_name = StringField('操作名称', validators=[Length(max=255)]) + case_accord_type_one_name = StringField('诉求归口一级名称', validators=[Length(max=255)]) + case_accord_type_two_name = StringField('诉求归口二级名称', validators=[Length(max=255)]) + case_accord_type_three_name = StringField('诉求归口三级名称', validators=[Length(max=255)]) + order_id = StringField('工单ID', validators=[Length(max=64)]) + task_id = StringField('任务ID', validators=[Length(max=64)]) + save_status = IntegerField('提交状态') + oa_feedback_status = IntegerField('OA反馈状态') + flow_token = StringField('流令牌', validators=[Length(max=256)]) + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64)]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj=None, **kwargs) + + +class GovsPhaseWiseCompletionBase(TD3iGovsPhaseWiseCompletion, CommonModel): + """阶段性办结业务基类""" + + FieldMapping = { + # 主键 & 关联ID + 'id': 'id', + 'master_id': 'masterId', + 'gd_id': 'gdId', + 'order_id': 'orderId', + 'task_id': 'taskId', + 'action_name': 'actionName', + + # 联系诉求人信息 + 'is_contact': 'isContact', + 'contact_name': 'contactName', + 'contact_time': 'contactTime', + 'contact_type': 'contactType', + 'next_feedback_time': 'nextFeedbackTime', + + # 处理意见 & 备注 + 'advice': 'advice', + 'reason': 'reason', + 'remark': 'remark', + 'file_id_str': 'fileIdStr', + + # 诉求归口分类 + 'case_accord_type_one_name': 'caseAccordTypeOneName', + 'case_accord_type_two_name': 'caseAccordTypeTwoName', + 'case_accord_type_three_name': 'caseAccordTypeThreeName', + + # 令牌 + 'flow_token': 'flowToken' + } + + @classmethod + async def exist_other(cls, id: Union[str, int], master_id: Union[str, int] = None, order_id: str = None): + """检查是否存在除当前记录外的同唯一标识阶段性办结记录""" + _query = select(cls).where(cls.id != id) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查询阶段性办结记录""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, master_id: Union[str, int] = None, order_id: str = None): + """检查阶段性办结记录是否已存在""" + _query = select(cls) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """阶段性办结基础搜索(分页/不分页)""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段配置 + _name_likes = { + cls.master_id.key: '%{}%', + cls.order_id.key: '%{}%', + cls.master_id.key: '%{}%', + cls.contact_name.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + # 排序处理 + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """分页搜索阶段性办结记录,返回标准分页结构""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_master_id(cls, data_df: pd.DataFrame): + """根据 master_id 区分已有数据/新增数据(批量保存用)""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + master_ids = data_df[cls.master_id.key].unique().tolist() + if not master_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.master_id).where(cls.master_id.in_(master_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + master_id_to_id_map = dict(zip(existing_df[cls.master_id.key], existing_df[cls.id.key])) + mask_exists = data_df[cls.master_id.key].isin(existing_df[cls.master_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.master_id.key].map(master_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主表ID查询单条阶段性办结记录""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单ID查询阶段性办结记录""" + _query = select(cls).where(cls.order_id == order_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_ids(cls, master_ids: list[Union[str, int]]): + """根据主表ID列表批量查询阶段性办结记录""" + _query = select(cls).where(cls.master_id.in_(master_ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + +@register_swagger_model +class GovsPhaseWiseCompletion(GovsPhaseWiseCompletionBase): + """阶段性办结业务操作类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """新增阶段性办结记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsPhaseWiseCompletionForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + master_id=_form.master_id.data, + order_id=_form.order_id.data + ) + assert _existing is None, "该任务已存在阶段性办结记录,无法重复创建。" + + _phase_info = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _phase_info.created_by = user.username + _phase_info.updated_by = user.username + await _phase_info.async_save() + return _phase_info + + @classmethod + async def delete(cls, phase_id: Union[str, int]): + """删除阶段性办结记录""" + _phase_info: cls = await cls.async_find_by_id(phase_id) + assert _phase_info, f"根据 ID {phase_id} 未找到阶段性办结记录。" + + _del_query = delete(cls).where(cls.id == _phase_info.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除阶段性办结记录(工单ID:{_phase_info.master_id},ID:{_phase_info.id}).') + return _phase_info + + @classmethod + async def modify(cls, phase_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改阶段性办结记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsPhaseWiseCompletionForm(formdata=kwargs) + _form.validate_form() + + _phase_info: cls = await cls.async_find_by_id(phase_id) + assert _phase_info, f'查无此阶段性办结记录。' + + _phase_info.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _phase_info.updated_by = user.username + await _phase_info.async_save() + return _phase_info + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量新增阶段性办结记录""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + phase_list = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(phase_list) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(phase_list)} 条阶段性办结记录。") + return len(phase_list) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改阶段性办结记录""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条阶段性办结记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存(自动区分新增/更新)""" + _exists_df, _latest_df = await cls.exists_master_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/govs_push_status.py b/models/govs_push_status.py new file mode 100644 index 0000000..f741954 --- /dev/null +++ b/models/govs_push_status.py @@ -0,0 +1,401 @@ +import random +from typing import Union, Optional, Callable + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from paste.core.logging import echo_log +from paste.util.pagination import Pagination + +import models +from models.common_model import CommonModel +from models.db_models import TD3iGovsPushStatu + + +class GovsPushStatuBase(TD3iGovsPushStatu, CommonModel): + """ + 推送状态基础类(完全映射 TD3iGovsPushStatu 字段)。 + + 封装所有与推送OA状态相关的通用操作方法。 + """ + + # 无字段名映射需求,保持原样 + FieldMapping = {} + + @classmethod + async def exist_other(cls, id: Union[str, int], govs_task_id: Union[str, int]): + """ + 检查是否存在除当前记录外的其他同任务ID的推送状态记录。 + + :param id: 当前记录ID + :param govs_task_id: 任务ID(唯一标志) + :return: 存在返回记录对象,不存在返回None + """ + _query = select(cls).where(cls.id != id, cls.master_id == govs_task_id) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """ + 根据ID列表批量查找推送状态数据。 + """ + _query = select(cls).where(cls.id.in_(ids)) + _record_list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _record_list + + @classmethod + async def is_exist(cls, govs_task_id: Union[str, int]): + """ + 检查推送状态是否已经存在(根据任务ID)。 + """ + _query = select(cls).where(cls.master_id == govs_task_id) + _record: cls = await cls.query_first(_query) + return _record + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """ + 按参数搜索推送状态数据的基础方法。 + + 支持字段: + - govs_task_id, push_order_status, push_order_detail_status, ... + - 不支持模糊匹配(均为整型状态码) + + :param is_paging: 是否分页 + :param kwargs: 查询参数 + :key int page_number: 页码(缺省随机1~100) + :key int page_size: 每页数量(缺省20) + :key dict sort_clause: 排序配置,如 {'updated_at': 'desc'} + :key int govs_task_id: 精确匹配任务ID + :key int push_order_status: 精确匹配推送待办工单状态 + :key int push_order_attachment_status: 精确匹配附件状态 + :key int push_order_detail_status: 精确匹配扩展信息状态 + :key int push_order_process_status: 精确匹配文件上传状态 + :return: (DataFrame, Pagination) + """ + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 无模糊字段,仅精确匹配 + _query = select(cls).where( + *cls.search_wheres(**kwargs) + ) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.updated_at.desc()) + + _record_df = await cls.query_as_df(_data_query) + if not _record_df.empty: + _record_df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + + return _record_df, _paging + + @classmethod + async def search(cls, **kwargs): + """ + 按参数搜索推送状态数据,返回分页格式数据。 + """ + _record_df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _record_df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_relation(cls, data_df: pd.DataFrame): + """ + 查找 data_df 中在数据库中已存在和不存在的记录。根据 govs_task_id 判断。 + + :param data_df: 输入的数据框架,必须包含 govs_task_id 列 + :return: (exists_df: pd.DataFrame, latest_df: pd.DataFrame) + - exists_df: 在数据库中存在的记录 + - latest_df: 在数据库中不存在的记录 + """ + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + # 获取待查询的 govs_task_id 组合 + task_ids = data_df[cls.master_id.key].unique().tolist() + if not task_ids: + return pd.DataFrame(), data_df.copy() + + # 查询数据库中已存在的记录 + _query = select(cls.id, cls.master_id).where(cls.master_id.in_(task_ids)) + exists_df = await cls.query_as_df(_query) + exists_df[cls.master_id.key] = exists_df[cls.master_id.key].astype(str) + + if exists_df.empty: + return pd.DataFrame(), data_df.copy() + + # 构建 govs_task_id -> id 的映射 + key_to_id_map = dict(zip(exists_df[cls.master_id.key], exists_df[cls.id.key])) + + # 根据 govs_task_id 是否在数据库中划分数据 + mask_exists = data_df[cls.master_id.key].isin(exists_df[cls.master_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.master_id.key].map(key_to_id_map) + latest_df = data_df[~mask_exists].copy() + + return exists_df, latest_df + + +@register_swagger_model +class GovsPushStatus(GovsPushStatuBase): + """ + 推送状态业务模型类(主业务类,完全继承 TD3iGovsPushStatu 字段)。 + """ + + @classmethod + async def create(cls, **kwargs): + """ + 创建新的推送状态记录。 + + 业务流程: + 1. 使用 kwargs 直接构造对象(无需表单验证,因无前端交互) + 2. 检查是否已存在相同任务ID的记录(避免重复) + 3. 创建新记录对象 + 4. 设置创建者和更新者为 'D3I' + 5. 保存到数据库 + 6. 返回创建的对象 + + :param kwargs: 推送状态参数字典 + :return: 新建推送状态对象 + :rtype: TD3iGovsPushStatu + :raises AssertionError: 当记录已存在时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 检查是否已存在同任务ID的记录 + _record: cls = await cls.is_exist(kwargs.get('govs_task_id')) + assert _record is None, "相同任务ID的推送状态已存在,不能重复创建。" + + # 创建记录对象 + _record = cls().copy_from_dict(kwargs, skip_none=True).before_save() + # 强制设置创建者和更新者为 'D3I' + _record.created_by = 'D3I' + _record.updated_by = 'D3I' + await _record.async_save() + return _record + + @classmethod + async def delete(cls, id: Union[str, int]): + """ + 删除推送状态记录(软删除,不实际删除,仅用于逻辑隔离)。 + + 注意:此系统建议保留历史记录,删除操作仅为标记。 + + 业务流程: + 1. 根据ID查找记录 + 2. 验证存在性 + 3. 执行物理删除(因无软删除字段,此处直接删除) + + :param id: 要删除的记录ID + :return: 删除的记录ID + :rtype: int + :raises AssertionError: 当记录不存在时抛出 + """ + _record: cls = await cls.async_find_by_id(id) + assert _record, f"根据 ID {id} 未找到推送状态记录。" + + # 执行物理删除 + _del_query = delete(cls).where(cls.id == id) + _del_count = (await cls.raw_execute(_del_query)).rowcount + echo_log(f'已删除推送状态记录(ID:{id}).') + return _del_count + + @classmethod + async def modify(cls, id: Union[str, int], **kwargs): + """ + 修改已有推送状态信息。 + + 注意:仅允许更新状态码字段,不允许修改 id、created_at、created_by 等系统字段。 + + 业务流程: + 1. 处理字符串字段去除空格 + 2. 查询原记录 + 3. 验证存在性 + 4. 更新允许字段 + 5. 设置 updated_by = 'D3I' + 6. 保存到数据库 + 7. 返回更新后的对象 + + :param id: 要修改的记录ID + :param kwargs: 需要更新的字段(仅限状态字段) + :return: 修改后的推送状态对象 + :rtype: GovsPushStatus + :raises AssertionError: 当记录不存在时抛出 + """ + # 处理字符串字段去除空格 + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + # 查询原记录 + _record: cls = await cls.async_find_by_id(id) + assert _record, f"根据 ID {id} 未找到推送状态记录。" + + # 允许更新的字段(仅状态码) + allowed_fields = { + 'push_order_status', + 'push_order_detail_status', + 'push_order_attachment_status', + 'push_order_process_status' + } + + # 过滤合法字段 + update_data = {k: v for k, v in kwargs.items() if k in allowed_fields and v is not None} + if not update_data: + return _record + + # 更新字段 + _record.copy_from_dict(update_data, skip_none=True) + _record.updated_by = 'D3I' + await _record.async_save() + return _record + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame): + """ + 批量创建新推送状态记录(传入数据应为全新记录,无需校验是否存在)。 + + :param data_df: 包含推送状态数据的 DataFrame,字段需与模型属性匹配(如 govs_task_id, push_order_status 等) + :return: 成功创建的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 一次性转为字典列表(C 层高效) + records = data_df.to_dict('records') + + # 用列表推导式构造对象 + records = [ + cls().copy_from_dict(record, skip_none=True).before_save() + for record in records + ] + + # 批量插入 + session = cls.get_aio_session() + try: + session.add_all(records) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(records)} 条推送状态记录。") + return len(records) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame): + """ + 批量修改已有推送状态记录。 + + :param data_df: 包含推送状态数据的 DataFrame,必须包含 id 列 + :return: 成功更新的记录数量 + :rtype: int + """ + if data_df.empty: + return 0 + + # 必须包含 id 列 + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列(主键)") + return 0 + + # 转换为字典列表 + update_data = data_df.to_dict('records') + + # 使用 bulk_update_mappings + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条推送状态记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame): + """ + 批量保存数据,自动处理新建和更新。 + + :param data_df: 要保存的数据框架 + :return 新建和更新的数量 + """ + # 筛选数据状态 + _exists_df, _latest_df = await GovsPushStatus.exists_relation(data_df) + # 保存到数据库 + _created_count = await GovsPushStatus.create_batch(_latest_df) + _updated_count = await GovsPushStatus.modify_batch(_exists_df) + return _created_count, _updated_count + + @classmethod + async def set_push_order_status(cls, govs_task_id: Union[str, int], status: int = 1): + govs_task: cls = await cls(master_id=govs_task_id).async_find_first() + if govs_task: + govs_task.push_order_status = status + else: + govs_task = cls(master_id=govs_task_id, push_order_status=status) + # 保存数据 + await govs_task.async_save() + + @classmethod + async def set_push_order_detail_status(cls, govs_task_id: Union[str, int], status: int = 1): + govs_task: cls = await cls(master_id=govs_task_id).async_find_first() + if govs_task: + govs_task.push_order_detail_status = status + else: + govs_task = cls(master_id=govs_task_id, push_order_detail_status=status) + # 保存数据 + await govs_task.async_save() + + @classmethod + async def set_push_order_attachment_status(cls, govs_task_id: Union[str, int], status: int = 1): + govs_task: cls = await cls(master_id=govs_task_id).async_find_first() + if govs_task: + govs_task.push_order_attachment_status = status + else: + govs_task = cls(master_id=govs_task_id, push_order_attachment_status=status) + # 保存数据 + await govs_task.async_save() + + @classmethod + async def set_push_order_process_status(cls, govs_task_id: Union[str, int], status: int = 1): + govs_task: cls = await cls(master_id=govs_task_id).async_find_first() + if govs_task: + govs_task.push_order_process_status = status + else: + govs_task = cls(master_id=govs_task_id, push_order_process_status=status) + # 保存数据 + await govs_task.async_save() diff --git a/models/govs_save_sign.py b/models/govs_save_sign.py new file mode 100644 index 0000000..10998ca --- /dev/null +++ b/models/govs_save_sign.py @@ -0,0 +1,321 @@ +# coding: utf-8 +import datetime +import random +from typing import Union + +import pandas as pd +from sqlalchemy import select, delete +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField, DateTimeField +from wtforms.validators import Length + +import models +from models.db_models import TD3iGovsSaveSign +from models.common_model import CommonModel +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.util.pagination import Pagination +from paste.web.form import ModelForm + + +class GovsSaveSignForm(ModelForm): + """工单签收表单验证类""" + + id = IntegerField('主键') + master_id = IntegerField('主表ID') + master_id = StringField('代签收唯一标志', validators=[Length(max=64)]) + order_id = StringField('工单ID', validators=[Length(max=64)]) + order_no = StringField('工单号', validators=[Length(max=64)]) + order_process_id = StringField('工单流程ID', validators=[Length(max=64)]) + task_id = StringField('任务ID', validators=[Length(max=64)]) + flag = StringField('签收标识', validators=[Length(max=64)]) + save_status = IntegerField('提交状态') + oa_feedback_status = IntegerField('OA反馈状态') + flow_token = StringField('流令牌', validators=[Length(max=256)]) + created_at = DateTimeField('创建时间') + created_by = StringField('创建者', validators=[Length(max=64)]) + updated_at = DateTimeField('更新时间') + updated_by = StringField('更新者', validators=[Length(max=64)]) + + def process(self, formdata=None, obj=None, **kwargs): + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class GovsSaveSignBase(TD3iGovsSaveSign, CommonModel): + """工单签收业务基类""" + + FieldMapping = { + # 主键 & 关联ID + 'id': 'id', + 'master_id': 'gdId', + 'gd_id': 'gdId', + 'order_id': 'orderId', + 'order_no': 'orderNo', + 'order_process_id': 'orderProcessId', + 'task_id': 'taskId', + + # 业务标识 + 'flag': 'flag', + + # 令牌 + 'flow_token': 'flowToken', + } + + @classmethod + async def exist_other(cls, id: Union[str, int], master_id: Union[int, str] = None, order_id: str = None, + order_no: str = None): + """检查是否存在除当前记录外的同唯一标识工单签收记录""" + _query = select(cls).where(cls.id != id) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def find_by_ids(cls, ids: list[Union[str, int]]): + """根据ID列表批量查询工单签收记录""" + _query = select(cls).where(cls.id.in_(ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + @classmethod + async def is_exist(cls, master_id: Union[int, str] = None, order_id: str = None, order_no: str = None): + """检查工单签收记录是否已存在""" + _query = select(cls) + if master_id: + _query = _query.where(cls.master_id == master_id) + if order_id: + _query = _query.where(cls.order_id == order_id) + if order_no: + _query = _query.where(cls.order_no == order_no) + _task: cls = await cls.query_first(_query) + return _task + + @classmethod + async def search_base(cls, is_paging=True, **kwargs): + """工单签收基础搜索(分页/不分页)""" + page_number = kwargs.get('page_number', random.randint(1, 100)) + page_size = kwargs.get('page_size', 20) + kwargs.update({'page_number': page_number, 'page_size': page_size}) + + # 模糊查询字段配置 + _name_likes = { + cls.master_id.key: '%{}%', + cls.order_no.key: '%{}%', + cls.master_id.key: '%{}%', + } + + _query = select(cls).where( + *cls.search_wheres(likes=_name_likes, **kwargs) + ).group_by(cls.id) + + _paging = None + if is_paging: + _row_count = await cls.query_count(_query) + _paging = Pagination(_row_count).paging(page_number, page_size) + _data_query = _query.limit(page_size).offset(_paging.offset_size) + else: + _data_query = _query.where() + + # 排序处理 + _sort_clause = cls.sort_clauses(kwargs.get('sort_clause', {})) + if _sort_clause: + _data_query = _data_query.order_by(*_sort_clause) + else: + _data_query = _data_query.order_by(cls.created_at.desc()) + + _df = await cls.query_as_df(_data_query) + if not _df.empty: + _df.replace(models.EmptyInDF + models.EmptyDatetimeInDF, '', inplace=True) + _df[cls.id.key] = _df[cls.id.key].astype(str) + + return _df, _paging + + @classmethod + async def search(cls, **kwargs): + """分页搜索工单签收记录,返回标准分页结构""" + _df, _paging = await cls.search_base(**kwargs) + return { + 'total': _paging.row_count, + 'rows': _df.to_dict('records'), + 'pagination': { + 'page_number': _paging.page_number, + 'page_count': _paging.page_count, + 'page_size': _paging.page_size, + }, + } + + @classmethod + async def exists_master_id(cls, data_df: pd.DataFrame): + """根据 master_id 区分已有数据/新增数据(批量保存用)""" + if data_df.empty: + return pd.DataFrame(), pd.DataFrame() + + master_ids = data_df[cls.master_id.key].unique().tolist() + if not master_ids: + return pd.DataFrame(), data_df.copy() + + _query = select(cls.id, cls.master_id).where(cls.master_id.in_(master_ids)) + existing_df = await cls.query_as_df(_query) + + if existing_df.empty: + return pd.DataFrame(), data_df.copy() + + master_id_to_id_map = dict(zip(existing_df[cls.master_id.key], existing_df[cls.id.key])) + mask_exists = data_df[cls.master_id.key].isin(existing_df[cls.master_id.key]) + exists_df = data_df[mask_exists].copy() + exists_df[cls.id.key] = exists_df[cls.master_id.key].map(master_id_to_id_map) + latest_df = data_df[~mask_exists].copy() + return exists_df, latest_df + + @classmethod + async def find_by_master_id(cls, master_id: Union[str, int]): + """根据主表ID查询单条签收记录""" + _query = select(cls).where(cls.master_id == master_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_order_id(cls, order_id: str): + """根据工单ID查询签收记录""" + _query = select(cls).where(cls.order_id == order_id) + return await cls.query_first(_query) + + @classmethod + async def find_by_master_ids(cls, master_ids: list[Union[str, int]]): + """根据主表ID列表批量查询签收记录""" + _query = select(cls).where(cls.master_id.in_(master_ids)) + _list: list[cls] = (await cls.orm_execute_scalars(_query)).all() + return _list + + +@register_swagger_model +class GovsSaveSign(GovsSaveSignBase): + """工单签收业务操作类""" + + @classmethod + async def create(cls, user: RbacUser = None, **kwargs): + """新增工单签收记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsSaveSignForm(formdata=kwargs) + _form.validate_form() + + _existing = await cls.is_exist( + master_id=_form.master_id.data, + order_id=_form.order_id.data, + order_no=_form.order_no.data + ) + assert _existing is None, "该任务已存在确认签收记录,无法重复创建。" + + _sign_info = cls().copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _sign_info.created_by = user.username + _sign_info.updated_by = user.username + await _sign_info.async_save() + return _sign_info + + @classmethod + async def delete(cls, sign_id: Union[str, int]): + """删除工单签收记录""" + _sign_info: cls = await cls.async_find_by_id(sign_id) + assert _sign_info, f"根据 ID {sign_id} 未找到工单签收记录。" + + _del_query = delete(cls).where(cls.id == _sign_info.id) + await cls.raw_execute(_del_query) + echo_log(f'已删除工单签收记录(工单ID:{_sign_info.master_id},ID:{_sign_info.id}).') + return _sign_info + + @classmethod + async def modify(cls, sign_id: Union[str, int], user: RbacUser = None, **kwargs): + """修改工单签收记录""" + for _k, _v in kwargs.items(): + if isinstance(_v, str): + kwargs[_k] = _v.strip() + + _form = GovsSaveSignForm(formdata=kwargs) + _form.validate_form() + + _sign_info: cls = await cls.async_find_by_id(sign_id) + assert _sign_info, f'查无此工单签收记录。' + + _sign_info.copy_from_dict(_form.data, skip_none=True).before_save() + if user: + _sign_info.updated_by = user.username + await _sign_info.async_save() + return _sign_info + + @classmethod + async def create_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量新增工单签收记录""" + if data_df.empty: + return 0 + + if user: + data_df['created_by'] = user.username + data_df['updated_by'] = user.username + + records = data_df.to_dict('records') + sign_list = [cls().copy_from_dict(record, skip_none=True).before_save() for record in records] + + session = cls.get_aio_session() + try: + session.add_all(sign_list) + await session.commit() + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + echo_log(f"批量创建成功:创建 {len(sign_list)} 条工单签收记录。") + return len(sign_list) + + @classmethod + async def modify_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量修改工单签收记录""" + if data_df.empty: + return 0 + + if 'id' not in data_df.columns: + echo_log(f"错误:modify_batch 要求输入数据必须包含 '{cls.id.key}' 列") + return 0 + + data_df['updated_at'] = datetime.datetime.now() + if user: + data_df['updated_by'] = user.username + + update_data = data_df.to_dict('records') + session = cls.get_aio_session() + try: + await session.run_sync( + lambda sync_session: sync_session.bulk_update_mappings(cls, update_data) + ) + await session.commit() + updated_count = len(update_data) + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + echo_log(f"批量修改成功:更新 {updated_count} 条工单签收记录。") + return updated_count + + @classmethod + async def save_batch(cls, data_df: pd.DataFrame, user: RbacUser = None): + """批量保存(自动区分新增/更新)""" + _exists_df, _latest_df = await cls.exists_master_id(data_df) + _created_count = await cls.create_batch(_latest_df, user) + _updated_count = await cls.modify_batch(_exists_df, user) + return _created_count, _updated_count diff --git a/models/token.py b/models/token.py new file mode 100644 index 0000000..8495891 --- /dev/null +++ b/models/token.py @@ -0,0 +1,222 @@ +import datetime +from typing import Union + +from sqlalchemy import select +from tornado_swagger.model import register_swagger_model +from wtforms import StringField, IntegerField +from wtforms.validators import Length + +from models.common_model import CommonModel +from models.db_models import TToken +from paste.core.logging import echo_log +from paste.rbac.rbac_user import RbacUser +from paste.web.form import ModelForm + + +class TTokenForm(ModelForm): + """ + Token 表单验证类(完全映射 TToken 字段)。 + + 用于验证和处理认证令牌的创建/修改表单数据。 + 字段完全映射数据库表 t_token 的字段结构。 + """ + + # 主键 + id = IntegerField('主键ID') + + # 基础信息 + platform = StringField('平台', validators=[Length(max=20, message='平台长度不能超过20字符')]) + token = StringField('令牌', validators=[Length(max=500, message='令牌长度不能超过500字符')]) + deleted = IntegerField('是否删除(0未删,1已删)') + + # 创建与更新信息 + creator = StringField('创建者', validators=[Length(max=64, message='创建者长度不能超过64字符')]) + updater = StringField('更新者', validators=[Length(max=64, message='更新者长度不能超过64字符')]) + + def process(self, formdata=None, obj=None, **kwargs): + """ + 处理表单数据,在数据绑定前进行预处理。 + + 主要功能: + - 遍历所有表单字段 + - 对字符串类型的值去除两端空白字符 + - 调用父类的process方法继续处理 + """ + if formdata: + for name, values in formdata.items(): + if isinstance(values, list) and values: + formdata[name] = [v.strip() if isinstance(v, str) else v for v in values] + elif isinstance(values, str): + formdata[name] = values.strip() + super().process(formdata, obj, **kwargs) + + +class TokenBase(TToken, CommonModel): + """ + Token 基础类(完全映射 TToken 字段)。 + + 继承自数据库模型 TToken 和通用模型 CommonModel。 + 封装所有与认证令牌相关的通用操作方法。 + """ + + @classmethod + async def find_by_platform(cls, platform: str): + """ + 根据平台查找 Token 记录。 + + :param platform: 平台 + :return: Token 对象或 None + """ + _query = select(cls).where(cls.platform == platform, cls.deleted == 0) + _token: cls = await cls.query_first(_query) + assert _token, f'未找到可用 Token,平台:{platform}.' + return _token + + +@register_swagger_model +class TokenModel(TokenBase): + """ + Token 业务模型类(主业务类,完全继承 TToken 字段)。 + + --- + description: 认证 Token + type: object + properties: + id: + description: 主键 + type: integer + example: 1001 + readOnly: true + platform: + description: 平台 + type: string + example: "web" + maxLength: 20 + token: + description: 令牌 + type: string + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + maxLength: 500 + deleted: + description: 是否删除(0未删,1已删) + type: integer + example: 0 + creator: + description: 创建者 + type: string + example: "admin" + maxLength: 64 + create_time: + description: 创建时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-15 10:30:00" + readOnly: true + updater: + description: 更新者 + type: string + example: "admin" + maxLength: 64 + update_time: + description: 更新时间,ISO格式的日期时间字符串 + type: string + format: date-time + example: "2024-01-16 14:25:00" + readOnly: true + """ + + @classmethod + async def create(cls, user: RbacUser=None, **kwargs): + """ + 创建新的 Token。 + + 业务流程: + 1. 使用 TTokenForm 验证表单数据完整性 + 2. 创建新 Token 对象 + 3. 设置创建者为当前用户 + 4. 保存到数据库 + 5. 返回创建的 Token 对象 + + :param RbacUser user: 操作用户对象 + :param kwargs: Token 参数字典 + :return: 新建 Token 对象 + :rtype: TokenModel + :raises ValidationError: 当表单验证失败时抛出 + """ + _token_form = TTokenForm(formdata=kwargs) + _token_form.validate_form() + + # 创建 Token 对象 + _token = cls().copy_from_dict(_token_form.data, skip_none=True).before_save() + if user: + _token.creator = user.username + _token.updater = user.username + else: + _token.creator = 'D3I' + _token.updater = 'D3I' + + _token.deleted = 0 + _token.create_time = datetime.datetime.now() + _token.update_time = datetime.datetime.now() + await _token.async_save() + return _token + + @classmethod + async def delete(cls, token_id: Union[str, int]): + """ + 逻辑删除 Token(设置 deleted=1)。 + + 业务流程: + 1. 根据ID查找 Token + 2. 验证存在性 + 3. 设置 deleted=1 + 4. 保存更新 + + :param token_id: 要删除的 Token ID + :return: 更新后的 Token 对象 + :rtype: TokenModel + :raises AssertionError: 当 Token 不存在时抛出 + """ + _token: cls = await cls.async_find_by_id(token_id) + assert _token, f"根据 ID {token_id} 未找到 Token。" + + _token.deleted = 1 + _token.updater = "system" + await _token.async_save() + echo_log(f'已逻辑删除 Token(ID:{_token.id})。') + return _token + + @classmethod + async def refresh(cls, platform: str, token: str, user: RbacUser=None): + """ + 刷新 Token 信息(更新 token、updater、update_time)。 + + 业务流程: + 1. 使用 TTokenForm 验证更新字段 + 2. 查询原 Token 对象 + 3. 更新字段并设置更新者,更新后,删除状态自动变为可用 + 4. 保存到数据库 + + :param platform: 要刷新平台 + :param token: 需要更新的 token + :param RbacUser user: 操作用户对象 + :return: 更新后的 Token 对象 + :rtype: TokenModel + :raises AssertionError: 当 Token 不存在时抛出 + :raises ValidationError: 当表单验证失败时抛出 + """ + # 查询原 Token + _token: cls = await cls(platform=platform).async_find_first() + + # 没找到,创建 + if not _token: + _token = await cls.create(platform=platform, token=token, user=user) + return _token + + # 找到,更新 + _token.token = token + _token.deleted = 0 + _token.update_time = datetime.datetime.now() + _token.updater = user.username if user else _token.updater + await _token.async_save() + return _token \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..12361f9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[tool.ruff.lint] +# 指定要检查的规则 +# 常用规则类别: +# E/Pyflakes: 语法错误和基本逻辑问题(重要) +# F/Pyflakes: 未使用的导入/变量(重要) +# I/isort: 导入排序(团队规范) +# B/flake8-bugbear: 常见陷阱(推荐) +# SIM/flake8-simplify: 代码简化建议(可选) +# C4/flake8-comprehensions: 推导式改进(可选) + +select = [ + "E", # pycodestyle 错误 + "F", # Pyflakes(未使用变量/导入) + # "I", # 导入排序 + # "B", # Bugbear(常见错误模式) + # "UP", # pyupgrade(语法现代化) +] + +# 忽略不需要的规则 +ignore = [ + "F841", # 未使用的变量 - 忽略 + "I001", # import 排序 - 忽略 + "B009", # getattr 警告 - 忽略 + "UP007", # Union 语法 - 忽略 + "F541", # f-string without placeholders + "E501", # line too long(行太长) + "E741", # 允许使用 l、O、I 等变量名 + "D", # pydocstyle(文档字符串要求,太严格) + "ANN", # flake8-annotations(类型注解要求,太严格) + "S", # bandit(安全检查,有些太严格) +] + +[tool.ruff.lint.per-file-ignores] +# 测试文件中更宽松 +"tests/*" = [ + "F541", # 测试里用 f-string 不带 placeholder 也没事 + "S101", # 测试中允许 assert +] \ No newline at end of file diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/__pycache__/__init__.cpython-311.pyc b/service/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92255c3782b93dafb37c0d82d623014aa2b7c750 GIT binary patch literal 162 zcmZ3^%ge<81pm|-vOx4>5CH>>P{wCAAY(d13PUi1CZpd(^b literal 0 HcmV?d00001 diff --git a/service/__pycache__/api_service.cpython-311.pyc b/service/__pycache__/api_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38606b8f109eb8be47e94cfeb1df0d2c0438445e GIT binary patch literal 6595 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 Ke&+%z-hTq>7c4da literal 0 HcmV?d00001 diff --git a/service/__pycache__/dcm_service.cpython-311.pyc b/service/__pycache__/dcm_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00686dcb02038515f3ce0b9c40e19a79d64ff9ad GIT binary patch literal 4763 zcmdT{eQXrR6`#G`yW9I(pA9x&h8%{(T$`LxjB0AY)ZrsBC_sVIaILaB-7fao_u=mD z0j4-Th#?0(z-k*n4Jgp4dS_! zXr=a#&d$DlGxOfe?Azaa^Y*I8<3v#Y{7FOaXTW`d8)jjO%(K5y2%SSeKmrOPff6iH zDoBy1C1@c}I!MEljxsT8&`Mz$CTfe>gZ3C3WJ%i^b;O)OXUr9Jk+v=Bj(LKf7#HM7 z+a9fnd4pcy*rd(OZ6sK{w$a1Z7@d!=M@V= zH{do(a2~AV3eE@W9zwwdf(PhAfdjfor~%q2c!4e!YJn~hUlQv2h_2LaL84EjdzYFs zT><;tfZHg-oIYYX>CJK(Vb1is2y=@wxnZWpE1-76R7PL{%P#a>&H6IUCDk-x#wMYm za!1CT&sz(x=bx9{1zT{A-yIzv{kU-9lz#H^KOSVpe|7EYXQwA_kLqX68l98JPK`et zDV+V_>F7NW(1&m9C$AOKXZ0f&3b%eU{?VuUvAg5H9x{Z#NF8-f;y2sor!;nZzY
YiFPa?$kC*>YKAqk?SxuGdXy5qbOlKWto6~jFV zKAPy(YGqNuOt&cU;Y562q#IUo?FO-3qO?B}77w|M(EwJ}9GLPl3?1^8Sda?zM}*`O z*ks|uJNm72`p2ioM=no{T-MXScse=?b}-&0jNij7R$yqjpZycaIi!@nJwi$eU%;Cl z1X4GtO7w+j#RlEQa;Z$x>;wHWiC(-WDT>?V5MbpdeFb)M1Mh|uszU2OnwK-@fO(m3 zo)-{fO}2r&(+5RQU(~PuT+b9SqTjwh@w?0V{d@YziHT2+>Bs)?^x-cJTVh0D4fv35 z{3*cxd^{8rH5R@Czb_IMBfdt2{Iq5dOJYb#NSb9|T(b&dG&C@Y{ol_@IATd2Oz@5+ z7Z8mN2?BwLR!bB`o~RgIHIf(?5AYahN}^AUYc&S;j2?i+b@U7-L0_+K*^`JSW1`&h z!-Uk=vO`MrieW`=*)gE>B;qX_Tf16{yKXU1OWq*RKcLk{;t_?cMj!_7B`t&*m%%N+ z3nYcUbfVhETaABWRc}|;+m-WnrMkYfAuIPe(=f&~TpY-~)c){i>YjI1rXkDlIfhpm zzM8;0L||`@*{d>pb=O>#nJdkQCBkr*3P#TRa6kJykgX6^&OziX$EiTgj1fu+LkRVj zP+S>f-iI^br}Q3ljy}!+(#mizSZ2;Apk46(ecS+@F0L5{bTuDEkT@{l0Ma zP9P9SHevCROd&n|O@h0x(;6LiY3I)EJ0UhvqKpmZlkiC(;ggGyR$%1wKqlZyp(%^i+VPn6svHTmU}o2^piI%A3r`tub*OC}dg$y7(2wkmoqqa}-wJ$n$nAi)G$1EXgEL__ z3ntZJcNkT|X$J7@u%lw>Erre(=-@bE>XZWO3v|i>PFL;-4pjr-Wnq&5oGN#V!<6DR zR|U6nm^=hnYgK?#?@>j716Kxc+qOx3NF`qeQjT)p#tm<;UDvU_(+4mvoEp)GGlg5f z#Q?{Wh9^{)F~AR!yCn#@$nP}ZjLoTW`$Q$&Q;LrgwwG2zj^%yHXw*2HCHxgMN9lBy zmO@*?ekCmfg3ozl8aZ-El$Jx6jKhkr=m0n|lQ6+yUDkmZ4v?>%liD(6aTLAayIHn9 z$F{3%`(ti?j%&W=%yO+cu2p5LRGnLt`Tip1+(1B9Jf+s zt5o7m)9b?0Q2%S*gk$yb&R08Ep+~EnZ@f%D>S%n!NB_}h1-`&r8Ji3W1&c#=O^)3Af_j?CSQdn{6!V+e%S8oCt0M=%pfa&LI!-~Q~H-_ zcAgg!VV)TV zvLXh;2}ukTQxYT46W3g5j28Dq{*+92H1ceMHIic2@O&s9PbeY$8z=L;#`K4j9*yZu zK;mPA|2H8b85udr&2jo`BsnC|6Hg|%#MZ_J!+Af6*|o8Rkc^5Q(woqS?JNHkC}bxTHD$3-%oHjS8=`7b9KQ0%ks?K>_=H@bO zXV#uvd){)vet}b0v}avwa;`Ng))errOt}r->7nYUP>J|#6l8WKQs=_W5^1zi&3Lp( z>THx3H;V+CHMm(Mb%=4N4~@|aRC+<)?o7$)*39Z7gF}PEKN+*nSMBrjE-tfrxHGfn zeDj!Vsp?vqcP~i4ePVlNdzxH%k2mwqa47Sm^P9&!O{%9U@AjrQp4gn(oF>;3cWru; jTKB!IyEEtRRL%N?txtPS)MRSX IOLoop: + """ + 这里必须采用方法,在适当的时间点创建事件循环对象,否则会导致服务无法启动。 + :return: 事件循环对象 + """ + global current_io_loop + if current_io_loop is None: + current_io_loop = IOLoop.current() + return current_io_loop + + +def daemon(): + """ + 驻守线程,处理诸如 Websocket 推送等事务(已弃用)。 + """ + # 无事发生 + + # 启动回调列表 + for _cb in callbacks: + _cb.start() + echo_log(f"合计启动了 {len(callbacks)} 个驻守任务.") + + +def start_apps(): + """ + 启动配置文件中配置的各种服务。 + """ + apps_config: list[dict] = config.get_config('tornado.api', []) + apps: list[ApplicationSwagger] = [] + + for _app_cfg in apps_config: + handlers_pkg = udict.get_by_path(_app_cfg, 'handlers_pkg') + app = ApplicationSwagger(**_app_cfg) + port = udict.get_by_path(_app_cfg, 'port') + address = udict.get_by_path(_app_cfg, 'address') + app.listen(port, address) + apps.append(app) + echo_log(f"模块 {handlers_pkg} 已加载于:http://127.0.0.1:{port}") + + # 启动驻守线程 + # daemon() + return apps + + +def start_service(): + set_logger_config(logger_config_name) + echo_log(f"正在启动{service_name}...") + + # 启动服务时,重置加密密钥,意味着所有登录 Token 均失效 + # echo_log(json_token.reset_secret_key()) + + try: + # 检测 MySQL 服务是否正常 + echo_log('检测数据库服务...') + # 绑定连接池监听器 + conn_pool.bind_listener() + # 检测数据连接 + BaseAdapter.ping() + echo_log('数据库服务正常.') + + # 启动服务 + start_apps() + echo_log(f"{service_name}启动成功.") + current_loop().start() + except (redis.exceptions.TimeoutError, socket.timeout): + echo_log('Redis 服务异常.', level=logging.ERROR, is_log_exc=True) + echo_log(f"{service_name}启动失败.") + except sqlalchemy.exc.OperationalError: + echo_log('Database 服务异常.', level=logging.ERROR, is_log_exc=True) + echo_log(f"{service_name}启动失败.") + except KeyboardInterrupt: + echo_log(msg='KeyboardInterrupt') + stop_service() + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + + +def stop_service(): + # 停止回调列表 + for _cb in callbacks: + _cb.stop() + current_loop().stop() + echo_log(f"{service_name}已停止.") + + +def start(): + """ + 以驻内存方式启动服务。 + """ + set_logger_config(logger_config_name) + get_logger() + ds = DaemonizeService(pid_file=pid_file, name=service_name) + ds.set_start_callback(start_service) + ds.set_term_callback(stop_service) + ds.start() + + +def stop(): + """ + 停止驻内存服务。 + """ + set_logger_config(logger_config_name) + get_logger() + ds = DaemonizeService(pid_file=pid_file, name=service_name) + ds.set_start_callback(start_service) + ds.set_term_callback(stop_service) + ds.stop() diff --git a/service/dcm_service.py b/service/dcm_service.py new file mode 100644 index 0000000..9b6cdf0 --- /dev/null +++ b/service/dcm_service.py @@ -0,0 +1,110 @@ +""" +系统服务,用于读取服务配置文件,启动或停止相关的服务。 +""" +import logging +import os +import sys +from typing import Optional + +from dock.dcm import dcm_scrape, dcm_security +from dock.oa_dcm import oa_push_order, oa_sign_task +from paste.core.logging import echo_log, set_logger_config +from paste.service.task_service import TaskService + +logger_config_name = 'logger.dcm_service' +""" +配置文件中日志配置字段名称。 +""" + +task_serv: Optional[TaskService] = None +""" +任务服务对象。 +""" + +pid_file = os.path.join(os.path.curdir, 'dcm_service.pid') +""" +PID 文件路径。 +""" + +service_name = '数字城管计划任务服务' +""" +服务名称。 +""" + + +def init_task_service(): + """ + 初始化服务对象并安装具体任务。 + """ + global task_serv + task_serv = TaskService(service_name=service_name, pid_file=pid_file) + + # 每隔 2 小时执行,更新数字城管 Cookies + task_serv.add_task(creator=task_serv.create_delay_task, fn=renew_dcm_token, delay=3600 * 2) + + # 每隔 2 小时执行,抓取 DCM 数据 + task_serv.add_task(creator=task_serv.create_delay_task, fn=scrape_dcm_task, delay=3600 * 2) + + return task_serv + + +async def renew_dcm_token(): + try: + echo_log(f"开始执行数字城管 Cookies 更新...") + await dcm_security.login() + echo_log(f"完成数字城管 Cookies 更新.") + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + + +async def scrape_dcm_task(): + fetch_size = 30 + try: + echo_log(f"开始执行 DCM<=>OA 数据同步...") + await dcm_scrape.fetch_dcm_task(fetch_size) + # 工单推送 OA 平台 + await oa_push_order.push_full_order(fetch_size) + # 推送结束后,签收工单 + await oa_sign_task.sign_task(fetch_size) + echo_log(f"执行 DCM<=>OA 数据同步完成...") + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + + +def start_service(): + """ + 控制台服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start_service(env_check=False) + + +def start(): + """ + 驻内存服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start() + + +def stop(): + """ + 驻内存服务方式停止。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.stop() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + if sys.argv[1] == "start": + start_service() + elif sys.argv[1] == "stop": + stop() + else: + print("用法: python service/task_service.py start") + else: + start_service() diff --git a/service/govs_service.py b/service/govs_service.py new file mode 100644 index 0000000..25cd592 --- /dev/null +++ b/service/govs_service.py @@ -0,0 +1,107 @@ +""" +系统服务,用于读取服务配置文件,启动或停止相关的服务。 +""" +import logging +import os +import sys +from typing import Optional + +from dock.govs import govs_scrape, govs_security +from dock.oa_govs import govs_push_order, oa_sign_task +from paste.core.logging import echo_log, set_logger_config +from paste.service.task_service import TaskService + +logger_config_name = 'logger.govs_service' +""" +配置文件中日志配置字段名称。 +""" + +task_serv: Optional[TaskService] = None +""" +任务服务对象。 +""" + +pid_file = os.path.join(os.path.curdir, 'govs_service.pid') +""" +PID 文件路径。 +""" + +service_name = '省12345计划任务服务' +""" +服务名称。 +""" + + +def init_task_service(): + """ + 初始化服务对象并安装具体任务。 + """ + global task_serv + task_serv = TaskService(service_name=service_name, pid_file=pid_file) + + # 每隔 2 小时执行,更新省12345 Token + task_serv.add_task(creator=task_serv.create_delay_task, fn=renew_govs_token, delay=3600 * 2) + + # 每隔 2 小时执行,抓取 省12345 数据 + task_serv.add_task(creator=task_serv.create_delay_task, fn=scrape_govs_task, delay=3600 * 2) + return task_serv + + +async def renew_govs_token(): + try: + echo_log(f"开始执行省12345 Token 更新...") + await govs_security.login() + echo_log(f"开始执行省12345 Token 更新.") + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + + +async def scrape_govs_task(): + fetch_size = 30 + try: + echo_log(f"开始执行 GOVS<=>OA 数据同步...") + await govs_scrape.fetch_govs_task(num_per_page=fetch_size) + # 工单推送 OA 平台并签收 + await govs_push_order.push_full_order(fetch_size) + echo_log(f"执行 GOVS<=>OA 数据同步完成...") + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + + +def start_service(): + """ + 控制台服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start_service(env_check=False) + + +def start(): + """ + 驻内存服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start() + + +def stop(): + """ + 驻内存服务方式停止。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.stop() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + if sys.argv[1] == "start": + start_service() + elif sys.argv[1] == "stop": + stop() + else: + print("用法: python service/task_service.py start") + else: + start_service() diff --git a/service/oa_service.py b/service/oa_service.py new file mode 100644 index 0000000..05c8b46 --- /dev/null +++ b/service/oa_service.py @@ -0,0 +1,92 @@ +""" +系统服务,用于读取服务配置文件,启动或停止相关的服务。 +""" +import logging +import os +import sys +from typing import Optional + +from dock.oa import oa_security +from paste.core.logging import echo_log, set_logger_config +from paste.service.task_service import TaskService + +logger_config_name = 'logger.oa_service' +""" +配置文件中日志配置字段名称。 +""" + +task_serv: Optional[TaskService] = None +""" +任务服务对象。 +""" + +pid_file = os.path.join(os.path.curdir, 'oa_service.pid') +""" +PID 文件路径。 +""" + +service_name = 'OA计划任务服务' +""" +服务名称。 +""" + + +def init_task_service(): + """ + 初始化服务对象并安装具体任务。 + """ + global task_serv + task_serv = TaskService(service_name=service_name, pid_file=pid_file) + + # 每隔 10 分钟执行,更新 OA Token + task_serv.add_task(creator=task_serv.create_delay_task, fn=renew_oa_token, delay=60 * 10) + + return task_serv + + +async def renew_oa_token(): + try: + echo_log(f"开始执行 OA Token 更新...") + await oa_security.login() + echo_log(f"完成 OA Token 更新.") + except Exception as e: + echo_log(msg=e, level=logging.ERROR, is_log_exc=True) + + +def start_service(): + """ + 控制台服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start_service(env_check=False) + + +def start(): + """ + 驻内存服务方式启动。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.start() + + +def stop(): + """ + 驻内存服务方式停止。 + """ + set_logger_config(logger_config_name) + _ts = init_task_service() + _ts.stop() + + +if __name__ == "__main__": + if len(sys.argv) > 1: + if sys.argv[1] == "start": + start_service() + elif sys.argv[1] == "stop": + stop() + else: + print("用法: python service/task_service.py start") + else: + start_service() diff --git a/tp.py b/tp.py new file mode 100644 index 0000000..5891cb7 --- /dev/null +++ b/tp.py @@ -0,0 +1,6 @@ +import apps + +if __name__ == "__main__": + from paste.core import aio_pool + _runner = aio_pool.get_aio_runner() + print(apps.get_version()) \ No newline at end of file