# 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