"""采集协议号池 CRUD(biz_collection_protocol_no)""" from __future__ import annotations import math from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.models.models import BizCollectionProtocolNo, SysUser def _resolve_issued_user_display_name( db: Session, user_id: Optional[str], fallback_name: Optional[str] = None, ) -> Optional[str]: """issued_user_name 为空时,按 issued_user_id 从用户表补全展示名。""" name = (fallback_name or "").strip() if name: return name uid = (user_id or "").strip() if not uid: return None u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first() if not u: return None return (u.nick_name or u.user_no or "").strip() or None def enrich_issued_user_names(db: Session, rows: List[Dict[str, Any]]) -> None: """列表/详情:issuedUserName 为空但 issuedUserId 有值时补全展示名(原地修改)。""" missing_uids = { (r.get("issuedUserId") or "").strip() for r in rows if (r.get("issuedUserId") or "").strip() and not (r.get("issuedUserName") or "").strip() } if not missing_uids: return users = ( db.query(SysUser.id, SysUser.nick_name, SysUser.user_no) .filter(SysUser.id.in_(missing_uids), SysUser.del_flag == 0) .all() ) name_map = {(u.id or ""): ((u.nick_name or u.user_no or "").strip()) for u in users} for r in rows: uid = (r.get("issuedUserId") or "").strip() if uid and not (r.get("issuedUserName") or "").strip(): r["issuedUserName"] = name_map.get(uid, "") def _row_to_dict(r: BizCollectionProtocolNo) -> Dict[str, Any]: return { "id": r.id, "protocolNo": r.protocol_no, "issued": int(r.issued or 0), "issuedUserId": r.issued_user_id or "", "issuedUserName": r.issued_user_name or "", "issuedTime": r.issued_time.strftime("%Y-%m-%d %H:%M:%S") if r.issued_time else "", "tenantNo": r.tenant_no, "orderNum": r.order_num if r.order_num is not None else 0, "remark": r.remark or "", "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "", "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "", } def protocol_no_exists(db: Session, no: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool: v = (no or "").strip() if not v: return False q = db.query(BizCollectionProtocolNo).filter( and_( BizCollectionProtocolNo.protocol_no == v, BizCollectionProtocolNo.tenant_no == tenant_no, BizCollectionProtocolNo.del_flag == 0, ) ) if exclude_id: q = q.filter(BizCollectionProtocolNo.id != exclude_id) return q.first() is not None def list_protocol_nos( db: Session, *, page: int = 1, page_size: int = 10, keyword: Optional[str] = None, tenant_no: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): q0 = db.query(BizCollectionProtocolNo).filter(BizCollectionProtocolNo.del_flag == 0) if tenant_no: q0 = q0.filter(BizCollectionProtocolNo.tenant_no == tenant_no) kw = (keyword or "").strip() if kw: like = f"%{kw}%" q0 = q0.filter(or_(BizCollectionProtocolNo.protocol_no.like(like), BizCollectionProtocolNo.remark.like(like))) return q0 total = _filtered().order_by(None).with_entities(func.count(BizCollectionProtocolNo.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( _filtered() .order_by(BizCollectionProtocolNo.create_time.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) return [_row_to_dict(x) for x in rows], int(total) def get_protocol_no(db: Session, row_id: str, tenant_no: Optional[str] = None) -> Optional[BizCollectionProtocolNo]: q = db.query(BizCollectionProtocolNo).filter(and_(BizCollectionProtocolNo.id == row_id, BizCollectionProtocolNo.del_flag == 0)) if tenant_no: q = q.filter(BizCollectionProtocolNo.tenant_no == tenant_no) return q.first() def _parse_issued_time(value: Optional[str]) -> Optional[datetime]: raw = (value or "").strip() if not raw: return None for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"): try: return datetime.strptime(raw, fmt) except ValueError: continue return None def create_protocol_no( db: Session, *, protocol_no: str, issued: int = 0, tenant_no: str = "zkxw", order_num: Optional[int] = None, remark: Optional[str] = None, user_id: Optional[str] = None, issued_user_id: Optional[str] = None, issued_user_name: Optional[str] = None, issued_time: Optional[str] = None, ) -> BizCollectionProtocolNo: pn = (protocol_no or "").strip() if not pn: raise ValueError("采集协议号不能为空") if protocol_no_exists(db, pn, tenant_no): raise ValueError("采集协议号已存在") is_issued = 1 if issued else 0 issue_uid: Optional[str] = None issue_uname: Optional[str] = None issue_time: Optional[datetime] = None if is_issued: issue_uid = (issued_user_id or "").strip() or None if not issue_uid: raise ValueError("请选择下发用户") issue_uname = _resolve_issued_user_display_name(db, issue_uid, issued_user_name) issue_time = _parse_issued_time(issued_time) or datetime.now() obj = BizCollectionProtocolNo( id=generate_uuid(), protocol_no=pn, issued=is_issued, issued_user_id=issue_uid, issued_user_name=issue_uname, issued_time=issue_time, tenant_no=tenant_no, order_num=order_num if order_num is not None else 0, remark=(remark or "").strip() or None, create_by=user_id, create_dept_id="", create_time=datetime.now(), update_by=user_id, update_time=datetime.now(), del_flag=0, ) db.add(obj) db.commit() db.refresh(obj) return obj def update_protocol_no( db: Session, row_id: str, *, protocol_no: Optional[str] = None, issued: Optional[int] = None, order_num: Optional[int] = None, remark: Optional[str] = None, user_id: Optional[str] = None, tenant_no: Optional[str] = None, issued_user_id: Optional[str] = None, issued_user_name: Optional[str] = None, issued_time: Optional[str] = None, ) -> BizCollectionProtocolNo: obj = get_protocol_no(db, row_id, tenant_no=tenant_no) if not obj: raise ValueError("记录不存在") if protocol_no is not None: pn = protocol_no.strip() if not pn: raise ValueError("采集协议号不能为空") if pn != obj.protocol_no and protocol_no_exists(db, pn, obj.tenant_no, exclude_id=row_id): raise ValueError("采集协议号已存在") obj.protocol_no = pn if issued is not None: obj.issued = 1 if issued else 0 if order_num is not None: obj.order_num = order_num if remark is not None: obj.remark = remark.strip() or None if issued_user_id is not None: obj.issued_user_id = issued_user_id or None if issued_user_name is not None: obj.issued_user_name = issued_user_name or None if obj.issued == 1 and obj.issued_user_id and not (obj.issued_user_name or "").strip(): obj.issued_user_name = _resolve_issued_user_display_name(db, obj.issued_user_id) if issued_time is not None: obj.issued_time = _parse_issued_time(issued_time) if (issued_time or "").strip() else None if obj.issued == 1 and obj.issued_user_id and not obj.issued_time: obj.issued_time = datetime.now() obj.update_by = user_id obj.update_time = datetime.now() db.add(obj) db.commit() db.refresh(obj) return obj def soft_delete_protocol_no(db: Session, row_id: str, tenant_no: Optional[str] = None) -> None: obj = get_protocol_no(db, row_id, tenant_no=tenant_no) if not obj: raise ValueError("记录不存在") obj.del_flag = 1 obj.update_time = datetime.now() db.add(obj) db.commit() def batch_generate_protocol_nos( db: Session, *, prefix: str, middle: str, seq_start: int, seq_end: int, digit_count: int = 6, tenant_no: str = "zkxw", user_id: Optional[str] = None, ) -> Dict[str, Any]: """批量生成协议号: 前缀 + 中间段 + 补零序号""" p = (prefix or "").strip() m = (middle or "").strip() if not p: raise ValueError("固定前缀不能为空") if seq_start < 0 or seq_end < 0: raise ValueError("序号不能为负数") if seq_start > seq_end: raise ValueError("开始序号不能大于结束序号") if digit_count < 1: raise ValueError("序号位数不能小于1") count = seq_end - seq_start + 1 if count > 10000: raise ValueError("单次批量生成不能超过10000条") generated_nos = [] for i in range(seq_start, seq_end + 1): seq_str = str(i).zfill(digit_count) full_no = f"{p}{m}{seq_str}" generated_nos.append(full_no) # Batch check existing existing = set( row.protocol_no for row in db.query(BizCollectionProtocolNo.protocol_no) .filter( BizCollectionProtocolNo.tenant_no == tenant_no, BizCollectionProtocolNo.del_flag == 0, BizCollectionProtocolNo.protocol_no.in_(generated_nos), ) .all() ) now = datetime.now() created = [] skipped = [] for no in generated_nos: if no in existing: skipped.append(no) continue obj = BizCollectionProtocolNo( id=generate_uuid(), protocol_no=no, issued=0, tenant_no=tenant_no, order_num=0, remark=None, create_by=user_id, create_dept_id="", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) created.append(obj) if created: db.bulk_save_objects(created) db.commit() return { "created": len(created), "skipped": len(skipped), "skippedNos": skipped, } def batch_issue_protocol_nos( db: Session, *, ids: List[str], issued_user_id: str, issued_user_name: Optional[str] = None, tenant_no: Optional[str] = None, user_id: Optional[str] = None, ) -> Dict[str, Any]: """批量下发协议号""" if not ids: raise ValueError("请选择要下发的协议号") uid = (issued_user_id or "").strip() if not uid: raise ValueError("请选择下发用户") now = datetime.now() issued_count = 0 already_issued = [] for row_id in ids: obj = get_protocol_no(db, row_id, tenant_no=tenant_no) if not obj: continue if obj.issued == 1: already_issued.append(obj.protocol_no) continue obj.issued = 1 obj.issued_user_id = uid obj.issued_user_name = _resolve_issued_user_display_name(db, uid, issued_user_name) obj.issued_time = now obj.update_by = user_id obj.update_time = now db.add(obj) issued_count += 1 if issued_count > 0: db.commit() return { "issuedCount": issued_count, "alreadyIssued": already_issued, } def batch_revoke_protocol_nos( db: Session, *, ids: List[str], tenant_no: Optional[str] = None, user_id: Optional[str] = None, ) -> Dict[str, Any]: if not ids: raise ValueError("请选择要收回的协议号") now = datetime.now() revoked_count = 0 not_issued = [] for row_id in ids: obj = get_protocol_no(db, row_id, tenant_no=tenant_no) if not obj: continue if obj.issued != 1: not_issued.append(obj.protocol_no) continue obj.issued = 0 obj.issued_user_id = None obj.issued_user_name = None obj.issued_time = None obj.update_by = user_id obj.update_time = now db.add(obj) revoked_count += 1 if revoked_count > 0: db.commit() return { "revokedCount": revoked_count, "notIssued": not_issued, } def total_pages(total: int, page_size: int) -> int: if total <= 0: return 0 return int(math.ceil(total / float(page_size)))