"""样本编码池 CRUD(biz_sample_code)""" 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 BizSampleCode, SysUser def _resolve_issued_user_display_name( db: Session, user_id: Optional[str], fallback_name: Optional[str] = None, ) -> Optional[str]: 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 _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 _row_to_dict(r: BizSampleCode) -> Dict[str, Any]: return { "id": r.id, "sampleCode": r.sample_code, "sampleCodeType": r.sample_code_type or "", "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 sample_code_exists(db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool: v = (code or "").strip() if not v: return False q = db.query(BizSampleCode).filter( and_(BizSampleCode.sample_code == v, BizSampleCode.tenant_no == tenant_no, BizSampleCode.del_flag == 0) ) if exclude_id: q = q.filter(BizSampleCode.id != exclude_id) return q.first() is not None def list_sample_codes( 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(BizSampleCode).filter(BizSampleCode.del_flag == 0) if tenant_no: q0 = q0.filter(BizSampleCode.tenant_no == tenant_no) kw = (keyword or "").strip() if kw: like = f"%{kw}%" kw_conds = [ BizSampleCode.sample_code.like(like), BizSampleCode.remark.like(like), ] if tenant_no: from app.crud import sample_reception as reception_svc lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword( db, tenant_no, kw ) if lab_codes: kw_conds.append(BizSampleCode.sample_code.in_(lab_codes)) q0 = q0.filter(or_(*kw_conds)) return q0 total = _filtered().order_by(None).with_entities(func.count(BizSampleCode.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( _filtered() .order_by(BizSampleCode.create_time.desc(), BizSampleCode.sample_code.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) items = [_row_to_dict(x) for x in rows] if items and tenant_no: from app.crud import sample_reception as reception_svc codes = [i["sampleCode"] for i in items if i.get("sampleCode")] code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(db, tenant_no, codes) for item in items: sc = (item.get("sampleCode") or "").strip() item["labStorageCode"] = code_map.get(sc, "") if sc else "" return items, int(total) def get_sample_code(db: Session, row_id: str, tenant_no: Optional[str] = None) -> Optional[BizSampleCode]: q = db.query(BizSampleCode).filter(and_(BizSampleCode.id == row_id, BizSampleCode.del_flag == 0)) if tenant_no: q = q.filter(BizSampleCode.tenant_no == tenant_no) return q.first() def create_sample_code( db: Session, *, sample_code: str, sample_code_type: Optional[str] = None, 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, ) -> BizSampleCode: sc = (sample_code or "").strip() if not sc: raise ValueError("样本编码不能为空") if sample_code_exists(db, sc, tenant_no): raise ValueError("样本编码已存在") code_type = (sample_code_type or "").strip() or None if code_type and code_type not in ("zxbm", "qdbm"): raise ValueError("样本编码类型无效,仅支持 zxbm/qdbm") 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 = BizSampleCode( id=generate_uuid(), sample_code=sc, sample_code_type=code_type, 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_sample_code( db: Session, row_id: str, *, sample_code: 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, ) -> BizSampleCode: obj = get_sample_code(db, row_id, tenant_no=tenant_no) if not obj: raise ValueError("记录不存在") if sample_code is not None: sc = sample_code.strip() if not sc: raise ValueError("样本编码不能为空") if sc != obj.sample_code and sample_code_exists(db, sc, obj.tenant_no, exclude_id=row_id): raise ValueError("样本编码已存在") obj.sample_code = sc if issued is not None: obj.issued = 1 if issued else 0 if not obj.issued: obj.issued_user_id = None obj.issued_user_name = None obj.issued_time = None 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_sample_code(db: Session, row_id: str, tenant_no: Optional[str] = None) -> None: obj = get_sample_code(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_sample_codes( db: Session, *, sample_code_type: 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]: """批量生成样本编码: 类型前缀 + 中间段 + 补零序号""" t = (sample_code_type or "").strip() if t not in ("zxbm", "qdbm"): raise ValueError("样本编码类型无效,仅支持 zxbm/qdbm") m = (middle or "").strip() if not m: 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条") prefix_map = {"zxbm": "ZKZX", "qdbm": "ZKQD"} prefix = prefix_map[t] generated_codes = [] for i in range(seq_start, seq_end + 1): seq_str = str(i).zfill(digit_count) full_code = f"{prefix}{m}{seq_str}" generated_codes.append(full_code) existing = set( row.sample_code for row in db.query(BizSampleCode.sample_code) .filter( BizSampleCode.tenant_no == tenant_no, BizSampleCode.del_flag == 0, BizSampleCode.sample_code.in_(generated_codes), ) .all() ) now = datetime.now() created = [] skipped = [] for code in generated_codes: if code in existing: skipped.append(code) continue obj = BizSampleCode( id=generate_uuid(), sample_code=code, sample_code_type=t, 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), "skippedCodes": skipped, } def batch_issue_sample_codes( 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_sample_code(db, row_id, tenant_no=tenant_no) if not obj: continue if obj.issued == 1: already_issued.append(obj.sample_code) continue obj.issued = 1 obj.issued_user_id = uid obj.issued_user_name = (issued_user_name or "").strip() or None 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_sample_codes( 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_sample_code(db, row_id, tenant_no=tenant_no) if not obj: continue if obj.issued != 1: not_issued.append(obj.sample_code) 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)))