Files
d3i-szct/models/govc_task_contact.py
T
2026-06-02 17:46:38 +08:00

498 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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