| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198 |
- """市场移动端统一样本查询与轻量办理。"""
- from __future__ import annotations
- import uuid
- from datetime import datetime
- from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple
- from fastapi import HTTPException
- from sqlalchemy import and_, literal, or_
- from sqlalchemy.orm import Session
- from app.models.models import (
- BizCellSampleType,
- BizCollectionProtocol,
- BizProtocolCollectionInfo,
- BizCollectionRegionStaff,
- BizProtocolFullmoonTodo,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizProtocolStorageCert,
- BizQcWorkOrder,
- BizSampleReception,
- BizStorageReleaseReview,
- SysUser,
- )
- from app.models.research_donation import (
- BizResearchDonation,
- BizResearchDonationCollection,
- BizResearchDonationParty,
- BizResearchDonationSample,
- )
- from app.services.market_field_scope import (
- MarketFieldContext,
- apply_cell_protocol_scope,
- apply_donation_scope,
- leader_region_ids_query,
- )
- from app.services.market_support import validate_attachment_refs
- from app.services.sample_info_progress import (
- apply_handle_payload,
- insurance_handled,
- storage_cert_handled,
- )
- CELL_SOURCE = 'cell_storage'
- RESEARCH_SOURCE = 'research_donation'
- VALID_SOURCES = (CELL_SOURCE, RESEARCH_SOURCE)
- def _fmt_dt(value: Optional[datetime]) -> str:
- return value.strftime('%Y-%m-%d %H:%M:%S') if value else ''
- def _fmt_date(value: Any) -> str:
- if not value:
- return ''
- if hasattr(value, 'strftime'):
- return value.strftime('%Y-%m-%d')
- text = str(value)
- return f'{text[:4]}-{text[4:6]}-{text[6:8]}' if len(text) == 8 and text.isdigit() else text
- def _parse_dt(value: Any) -> Optional[datetime]:
- if value in (None, ''):
- return None
- if isinstance(value, datetime):
- return value
- text = str(value).strip()
- for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S'):
- try:
- return datetime.strptime(text, fmt)
- except ValueError:
- continue
- raise ValueError('办理时间格式无效')
- def _status_meta(code: str, label: str) -> Dict[str, str]:
- normalized = (code or '').strip()
- if normalized in (
- 'ycj',
- 'collected',
- 'js',
- 'complete',
- 'tg',
- 'yrk',
- 'handled',
- ):
- tag_type = 'success'
- elif normalized in ('jcz', 'incomplete', 'processing'):
- tag_type = 'warning'
- elif normalized in ('yzf', 'voided', 'bj', 'btg', 'rejected'):
- tag_type = 'danger'
- else:
- tag_type = 'info'
- return {
- 'code': normalized,
- 'label': label,
- 'statusTone': tag_type,
- 'tagType': tag_type,
- }
- def _sample_status(status: str) -> Dict[str, str]:
- status = (status or '').strip()
- label = {
- 'ycj': '已采集',
- 'jcz': '检测中',
- 'yrk': '已入库',
- 'yzf': '已作废',
- }.get(status, '未采集' if not status else status)
- return _status_meta(status or 'none', label)
- def _collection_progress(status: str) -> Dict[str, str]:
- status = (status or '').strip()
- if status == 'yzf':
- return _status_meta('voided', '已作废')
- if status in ('ycj', 'jcz', 'yrk'):
- return _status_meta('collected', '已采集')
- return _status_meta('uncollected', '未采集')
- def _apply_joined_collection_scope(query, model, db: Session, ctx: MarketFieldContext):
- if ctx.scope_kind == 'specialist':
- return query.filter(model.sales_user_id == ctx.user_id)
- return query.filter(
- or_(
- model.region_id.in_(leader_region_ids_query(db, ctx)),
- model.region_manager_user_id == ctx.user_id,
- )
- )
- def _stable_id(source: str, sample_code: str, row_id: str) -> str:
- code = (sample_code or '').strip()
- return f'{source}:{code}' if code else f'{source}:@id:{row_id}'
- def _parse_stable_id(sample_id: str) -> Tuple[str, str, bool]:
- source, separator, token = (sample_id or '').partition(':')
- token = token.strip()
- if not separator or source not in VALID_SOURCES or not token:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- if token.startswith('@id:'):
- row_id = token[4:].strip()
- if not row_id:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- return source, row_id, True
- return source, token, False
- def _materialization_order(query, source: str):
- if source == CELL_SOURCE:
- return query.order_by(
- BizProtocolCollectionInfo.create_time.desc(),
- BizProtocolCollectionInfo.id.desc(),
- BizProtocolPartyCustomer.create_time.desc(),
- BizProtocolPartyCustomer.id.desc(),
- )
- return query.order_by(
- BizResearchDonationCollection.create_time.desc(),
- BizResearchDonationCollection.id.desc(),
- BizResearchDonationParty.create_time.desc(),
- BizResearchDonationParty.id.desc(),
- )
- def _cell_query(db: Session, ctx: MarketFieldContext, user_info: dict):
- query = (
- db.query(
- BizProtocolSample,
- BizCollectionProtocol,
- BizProtocolCollectionInfo,
- BizProtocolPartyCustomer,
- )
- .join(
- BizCollectionProtocol,
- and_(
- BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
- BizCollectionProtocol.tenant_no == ctx.tenant_no,
- BizCollectionProtocol.del_flag == 0,
- ),
- )
- .join(
- BizProtocolCollectionInfo,
- and_(
- BizProtocolCollectionInfo.collection_protocol_id == BizCollectionProtocol.id,
- BizProtocolCollectionInfo.tenant_no == ctx.tenant_no,
- BizProtocolCollectionInfo.del_flag == 0,
- ),
- )
- .outerjoin(
- BizProtocolPartyCustomer,
- and_(
- BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
- BizProtocolPartyCustomer.tenant_no == ctx.tenant_no,
- BizProtocolPartyCustomer.del_flag == 0,
- ),
- )
- .filter(
- BizProtocolSample.tenant_no == ctx.tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- )
- query = apply_cell_protocol_scope(query, db, ctx, user_info=user_info)
- return _apply_joined_collection_scope(
- query, BizProtocolCollectionInfo, db, ctx
- )
- def _research_query(db: Session, ctx: MarketFieldContext, user_info: dict):
- query = (
- db.query(
- BizResearchDonationSample,
- BizResearchDonation,
- BizResearchDonationCollection,
- BizResearchDonationParty,
- )
- .join(
- BizResearchDonation,
- and_(
- BizResearchDonation.id == BizResearchDonationSample.research_donation_id,
- BizResearchDonation.tenant_no == ctx.tenant_no,
- BizResearchDonation.del_flag == 0,
- ),
- )
- .join(
- BizResearchDonationCollection,
- and_(
- BizResearchDonationCollection.research_donation_id == BizResearchDonation.id,
- BizResearchDonationCollection.tenant_no == ctx.tenant_no,
- BizResearchDonationCollection.del_flag == 0,
- ),
- )
- .outerjoin(
- BizResearchDonationParty,
- and_(
- BizResearchDonationParty.research_donation_id == BizResearchDonation.id,
- BizResearchDonationParty.tenant_no == ctx.tenant_no,
- BizResearchDonationParty.del_flag == 0,
- ),
- )
- .filter(
- BizResearchDonationSample.tenant_no == ctx.tenant_no,
- BizResearchDonationSample.del_flag == 0,
- )
- )
- query = apply_donation_scope(query, db, ctx, user_info=user_info)
- return _apply_joined_collection_scope(
- query, BizResearchDonationCollection, db, ctx
- )
- def _apply_filters(query, source: str, keyword: Optional[str], status: Optional[str]):
- if source == CELL_SOURCE:
- sample = BizProtocolSample
- parent = BizCollectionProtocol
- party = BizProtocolPartyCustomer
- else:
- sample = BizResearchDonationSample
- parent = BizResearchDonation
- party = BizResearchDonationParty
- text = (keyword or '').strip()
- if text:
- like = f'%{text}%'
- number_col = (
- BizCollectionProtocol.protocol_no
- if source == CELL_SOURCE
- else BizResearchDonation.donation_no
- )
- query = query.filter(
- or_(sample.sample_code.like(like), number_col.like(like), party.mother_name.like(like))
- )
- normalized_status = (status or '').strip()
- if normalized_status and normalized_status not in ('all', 'pending', 'processing', 'completed'):
- query = query.filter(sample.sample_status == normalized_status)
- elif normalized_status == 'pending':
- query = query.filter(or_(sample.sample_status.is_(None), sample.sample_status == ''))
- elif normalized_status == 'processing':
- query = query.filter(sample.sample_status.in_(('ycj', 'jcz')))
- elif normalized_status == 'completed':
- query = query.filter(sample.sample_status == 'yrk')
- return query
- def _identity_query(query, source: str):
- sample = BizProtocolSample if source == CELL_SOURCE else BizResearchDonationSample
- return query.with_entities(
- literal(source).label('source'),
- sample.id.label('row_id'),
- sample.create_time.label('create_time'),
- ).distinct()
- def _load_rows(
- db: Session,
- identities: Iterable[Any],
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> List[Dict[str, Any]]:
- ordered = [(row.source, row.row_id) for row in identities]
- cell_ids = [row_id for source, row_id in ordered if source == CELL_SOURCE]
- research_ids = [row_id for source, row_id in ordered if source == RESEARCH_SOURCE]
- rows_by_key: Dict[Tuple[str, str], Dict[str, Any]] = {}
- if cell_ids:
- query = _cell_query(db, ctx, user_info).filter(
- BizProtocolSample.id.in_(cell_ids)
- )
- for sample, parent, collection, party in _materialization_order(
- query, CELL_SOURCE
- ).all():
- rows_by_key.setdefault((CELL_SOURCE, sample.id), _raw_item(
- CELL_SOURCE, sample, parent, collection, party
- ))
- if research_ids:
- query = _research_query(db, ctx, user_info).filter(
- BizResearchDonationSample.id.in_(research_ids)
- )
- for sample, parent, collection, party in _materialization_order(
- query, RESEARCH_SOURCE
- ).all():
- rows_by_key.setdefault((RESEARCH_SOURCE, sample.id), _raw_item(
- RESEARCH_SOURCE, sample, parent, collection, party
- ))
- return [rows_by_key[key] for key in ordered if key in rows_by_key]
- def _raw_item(source: str, sample, parent, collection, party) -> Dict[str, Any]:
- number = parent.protocol_no if source == CELL_SOURCE else parent.donation_no
- return {
- 'id': _stable_id(source, sample.sample_code or '', sample.id),
- 'source': source,
- 'sourceLabel': '细胞存储' if source == CELL_SOURCE else '科研捐赠',
- 'sampleRowId': sample.id,
- 'parentId': parent.id,
- 'protocolNo': number or '',
- 'protocolStatus': (
- parent.protocol_status
- if source == CELL_SOURCE
- else parent.donation_status
- ) or '',
- 'sampleCode': sample.sample_code or '',
- 'sampleTypeId': sample.sample_type_id or '',
- 'sampleTypeName': '',
- 'sampleStatus': sample.sample_status or '',
- 'customerName': party.mother_name if party else '',
- 'motherName': party.mother_name if party else '',
- 'newbornName': party.newborn_name if party else '',
- 'expectedDeliveryDate': _fmt_date(
- party.expected_delivery_date if party else None
- ),
- 'regionId': collection.region_id or '',
- 'hospitalId': collection.hospital_id or '',
- 'salesUserId': collection.sales_user_id or '',
- 'regionManagerUserId': collection.region_manager_user_id or '',
- 'sampleCollectTime': _fmt_dt(collection.sample_collect_time),
- 'createTime': _fmt_dt(sample.create_time),
- }
- def _latest_maps(
- db: Session, tenant_no: str, items: List[Dict[str, Any]]
- ) -> Tuple[dict, dict, dict]:
- keys = {
- (item['source'], (item.get('sampleCode') or '').strip())
- for item in items
- if (item.get('sampleCode') or '').strip()
- }
- if not keys:
- return {}, {}, {}
- clauses = [
- and_(
- BizSampleReception.reception_source == source,
- BizSampleReception.sample_code.in_(
- [code for key_source, code in keys if key_source == source]
- ),
- )
- for source in VALID_SOURCES
- if any(key_source == source for key_source, _ in keys)
- ]
- receptions = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- or_(*clauses),
- )
- .order_by(
- BizSampleReception.reception_time.desc(),
- BizSampleReception.create_time.desc(),
- BizSampleReception.id.desc(),
- )
- .all()
- )
- qc_clauses = [
- and_(
- BizQcWorkOrder.reception_source == source,
- BizQcWorkOrder.sample_code.in_(
- [code for key_source, code in keys if key_source == source]
- ),
- )
- for source in VALID_SOURCES
- if any(key_source == source for key_source, _ in keys)
- ]
- qc_rows = (
- db.query(BizQcWorkOrder)
- .filter(
- BizQcWorkOrder.tenant_no == tenant_no,
- BizQcWorkOrder.del_flag == 0,
- BizQcWorkOrder.trigger_source == 'reception',
- or_(*qc_clauses),
- )
- .order_by(
- BizQcWorkOrder.update_time.desc(),
- BizQcWorkOrder.create_time.desc(),
- BizQcWorkOrder.id.desc(),
- )
- .all()
- )
- storage_clauses = [
- and_(
- BizStorageReleaseReview.reception_source == source,
- BizStorageReleaseReview.sample_code.in_(
- [code for key_source, code in keys if key_source == source]
- ),
- )
- for source in VALID_SOURCES
- if any(key_source == source for key_source, _ in keys)
- ]
- storage_rows = (
- db.query(BizStorageReleaseReview)
- .filter(
- BizStorageReleaseReview.tenant_no == tenant_no,
- BizStorageReleaseReview.del_flag == 0,
- or_(*storage_clauses),
- )
- .order_by(
- BizStorageReleaseReview.storage_time.desc(),
- BizStorageReleaseReview.update_time.desc(),
- BizStorageReleaseReview.create_time.desc(),
- BizStorageReleaseReview.id.desc(),
- )
- .all()
- )
- reception_map = {}
- qc_map = {}
- storage_map = {}
- for row in receptions:
- reception_map.setdefault((row.reception_source, (row.sample_code or '').strip()), row)
- for row in qc_rows:
- qc_map.setdefault((row.reception_source, (row.sample_code or '').strip()), row)
- for row in storage_rows:
- storage_map.setdefault((row.reception_source, (row.sample_code or '').strip()), row)
- return reception_map, qc_map, storage_map
- def _handle_maps(db: Session, tenant_no: str, items: List[Dict[str, Any]]) -> Tuple[dict, dict]:
- parent_ids = {
- item['parentId'] for item in items if item['source'] == CELL_SOURCE
- }
- if not parent_ids:
- return {}, {}
- cert_rows = (
- db.query(BizProtocolStorageCert)
- .filter(
- BizProtocolStorageCert.tenant_no == tenant_no,
- BizProtocolStorageCert.del_flag == 0,
- BizProtocolStorageCert.collection_protocol_id.in_(parent_ids),
- )
- .order_by(
- BizProtocolStorageCert.update_time.desc(),
- BizProtocolStorageCert.create_time.desc(),
- BizProtocolStorageCert.id.desc(),
- )
- .all()
- )
- todo_rows = (
- db.query(BizProtocolFullmoonTodo)
- .filter(
- BizProtocolFullmoonTodo.tenant_no == tenant_no,
- BizProtocolFullmoonTodo.del_flag == 0,
- BizProtocolFullmoonTodo.collection_protocol_id.in_(parent_ids),
- )
- .order_by(
- BizProtocolFullmoonTodo.update_time.desc(),
- BizProtocolFullmoonTodo.create_time.desc(),
- BizProtocolFullmoonTodo.id.desc(),
- )
- .all()
- )
- cert_map = {}
- todo_map = {}
- for row in cert_rows:
- cert_map.setdefault(row.collection_protocol_id, row)
- for row in todo_rows:
- todo_map.setdefault(row.collection_protocol_id, row)
- return cert_map, todo_map
- def _handle_summary(
- row, *, insurance: bool = False, handler_user_name: str = ''
- ) -> Dict[str, Any]:
- if row is None:
- return {
- **_status_meta('pending', '未办理'),
- 'handleTime': '',
- 'handlerUserId': '',
- 'handlerUserName': '',
- 'attachments': '',
- 'remark': '',
- }
- attachments = row.insurance_attachments if insurance else row.cert_image_urls
- remark = row.handle_remark if insurance else row.remark
- handled = insurance_handled(row) if insurance else storage_cert_handled(row)
- return {
- **_status_meta('handled' if handled else 'pending', '已办理' if handled else '未办理'),
- 'handleTime': _fmt_dt(row.handle_time),
- 'handlerUserId': row.handler_user_id or '',
- 'handlerUserName': handler_user_name,
- 'attachments': attachments or '',
- 'remark': remark or '',
- }
- def _referenced_user_name_map(
- db: Session, tenant_no: str, user_ids: List[str]
- ) -> Dict[str, str]:
- wanted = {str(user_id or '').strip() for user_id in user_ids if user_id}
- if not wanted:
- return {}
- return {
- str(user_id): str(nick_name or '').strip()
- for user_id, nick_name in db.query(SysUser.id, SysUser.nick_name)
- .filter(
- SysUser.id.in_(wanted),
- SysUser.tenant_no == tenant_no,
- )
- .all()
- }
- def _enrich(db: Session, tenant_no: str, items: List[Dict[str, Any]]) -> None:
- type_ids = {item['sampleTypeId'] for item in items if item['sampleTypeId']}
- type_map = {}
- if type_ids:
- type_map = {
- row.id: row.type_name or ''
- for row in db.query(BizCellSampleType).filter(
- BizCellSampleType.id.in_(type_ids),
- BizCellSampleType.del_flag == 0,
- or_(BizCellSampleType.tenant_no == tenant_no, BizCellSampleType.tenant_no == ''),
- )
- }
- reception_map, qc_map, storage_map = _latest_maps(db, tenant_no, items)
- cert_map, todo_map = _handle_maps(db, tenant_no, items)
- handler_names = _referenced_user_name_map(
- db,
- tenant_no,
- [
- row.handler_user_id
- for row in (*cert_map.values(), *todo_map.values())
- if row.handler_user_id
- ],
- )
- for item in items:
- item['sampleTypeName'] = type_map.get(item['sampleTypeId'], '')
- sample_status = _sample_status(item['sampleStatus'])
- collection_progress = _collection_progress(item['sampleStatus'])
- item.update(
- statusLabel=sample_status['label'],
- statusTone=sample_status['statusTone'],
- tagType=sample_status['tagType'],
- )
- code = (item['sampleCode'] or '').strip()
- key = (item['source'], code)
- reception = reception_map.get(key) if code else None
- qc = qc_map.get(key) if code else None
- storage = storage_map.get(key) if code else None
- reception_code = (reception.accept_conclusion or '').strip() if reception else 'none'
- reception_label = {'js': '已接收', 'bj': '拒收'}.get(reception_code, '未接收')
- qc_code = (qc.conclusion or '').strip() if qc else 'incomplete'
- if qc and qc.work_order_status == 'yzf':
- qc_code = 'yzf'
- qc_label = {'tg': '通过', 'btg': '不通过', 'yzf': '已作废'}.get(qc_code, '未完成')
- if storage and storage.storage_status == 'yrk':
- storage_code, storage_label = 'yrk', '已入库'
- elif storage and storage.review_conclusion == 'btg':
- storage_code, storage_label = 'rejected', '拒绝入库'
- else:
- storage_code, storage_label = 'none', '未入库'
- item['progress'] = {
- 'collection': collection_progress,
- 'reception': {
- **_status_meta(reception_code, reception_label),
- 'recordId': reception.id if reception else '',
- 'time': _fmt_dt(reception.reception_time) if reception else '',
- },
- 'qc': {
- **_status_meta(qc_code, qc_label),
- 'recordId': qc.id if qc else '',
- 'workOrderNo': qc.work_order_no if qc else '',
- },
- 'storage': {
- **_status_meta(storage_code, storage_label),
- 'recordId': storage.id if storage else '',
- 'time': _fmt_dt(storage.storage_time) if storage else '',
- },
- }
- item['labStorageCode'] = (
- getattr(reception, 'lab_storage_code', None) or ''
- )
- if item['source'] == CELL_SOURCE:
- cert = cert_map.get(item['parentId'])
- todo = todo_map.get(item['parentId'])
- item['storageCert'] = _handle_summary(
- cert,
- handler_user_name=handler_names.get(
- getattr(cert, 'handler_user_id', '') or '', ''
- ),
- )
- item['insurance'] = _handle_summary(
- todo,
- insurance=True,
- handler_user_name=handler_names.get(
- getattr(todo, 'handler_user_id', '') or '', ''
- ),
- )
- def list_samples(
- db: Session,
- *,
- ctx: MarketFieldContext,
- user_info: dict,
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- status: Optional[str] = None,
- source: Optional[str] = None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- page = max(1, int(page or 1))
- page_size = min(max(1, int(page_size or 10)), 100)
- identities = []
- if source in (None, '', CELL_SOURCE):
- cell = _apply_filters(_cell_query(db, ctx, user_info), CELL_SOURCE, keyword, status)
- identities.append(_identity_query(cell, CELL_SOURCE))
- if source in (None, '', RESEARCH_SOURCE):
- research = _apply_filters(
- _research_query(db, ctx, user_info), RESEARCH_SOURCE, keyword, status
- )
- identities.append(_identity_query(research, RESEARCH_SOURCE))
- if not identities:
- return [], 0
- combined = identities[0].union_all(*identities[1:]).subquery()
- total = db.query(combined).count()
- page_rows = (
- db.query(combined)
- .order_by(combined.c.create_time.desc(), combined.c.row_id.desc(), combined.c.source.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- items = _load_rows(db, page_rows, ctx, user_info)
- _enrich(db, ctx.tenant_no, items)
- return items, total
- def _cell_sample_lookup_query(
- db: Session,
- *,
- token: str,
- by_row_id: bool,
- ctx: MarketFieldContext,
- user_info: dict,
- for_update: bool = False,
- ):
- query = _cell_query(db, ctx, user_info)
- if by_row_id:
- query = query.filter(
- BizProtocolSample.id == token,
- or_(
- BizProtocolSample.sample_code.is_(None),
- BizProtocolSample.sample_code == '',
- ),
- )
- else:
- query = query.filter(BizProtocolSample.sample_code == token)
- query = query.order_by(
- BizProtocolSample.create_time.desc(),
- BizProtocolSample.id.desc(),
- BizProtocolCollectionInfo.create_time.desc(),
- BizProtocolCollectionInfo.id.desc(),
- BizProtocolPartyCustomer.create_time.desc(),
- BizProtocolPartyCustomer.id.desc(),
- )
- if for_update:
- query = query.populate_existing().with_for_update()
- return query
- def _find_sample(
- db: Session,
- sample_id: str,
- ctx: MarketFieldContext,
- user_info: dict,
- *,
- for_update: bool = False,
- ) -> Dict[str, Any]:
- source, token, by_row_id = _parse_stable_id(sample_id)
- if source == CELL_SOURCE:
- row = _cell_sample_lookup_query(
- db,
- token=token,
- by_row_id=by_row_id,
- ctx=ctx,
- user_info=user_info,
- for_update=for_update,
- ).first()
- else:
- query = _research_query(db, ctx, user_info)
- if by_row_id:
- query = query.filter(
- BizResearchDonationSample.id == token,
- or_(
- BizResearchDonationSample.sample_code.is_(None),
- BizResearchDonationSample.sample_code == '',
- ),
- )
- else:
- query = query.filter(BizResearchDonationSample.sample_code == token)
- query = query.order_by(
- BizResearchDonationSample.create_time.desc(),
- BizResearchDonationSample.id.desc(),
- BizResearchDonationCollection.create_time.desc(),
- BizResearchDonationCollection.id.desc(),
- BizResearchDonationParty.create_time.desc(),
- BizResearchDonationParty.id.desc(),
- )
- if for_update:
- query = query.populate_existing().with_for_update()
- row = query.first()
- if row is None:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- return _raw_item(source, *row)
- def get_sample_detail(
- db: Session,
- *,
- sample_id: str,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> Dict[str, Any]:
- item = _find_sample(db, sample_id, ctx, user_info)
- _enrich(db, ctx.tenant_no, [item])
- progress = item.pop('progress')
- storage_cert = item.pop('storageCert', None)
- insurance = item.pop('insurance', None)
- sections = [
- {
- 'key': 'basic',
- 'title': '基本信息',
- 'fields': [
- {'label': '业务来源', 'value': item['sourceLabel']},
- {'label': '协议编号', 'value': item['protocolNo']},
- {'label': '样本编码', 'value': item['sampleCode']},
- {'label': '样本类型', 'value': item['sampleTypeName']},
- {'label': '实验室入库编码', 'value': item['labStorageCode']},
- ],
- },
- {
- 'key': 'progress',
- 'title': '办理进度',
- 'fields': [
- {'label': '采集', 'value': progress['collection']['label']},
- {'label': '接收', 'value': progress['reception']['label']},
- {'label': '质检', 'value': progress['qc']['label']},
- {'label': '入库', 'value': progress['storage']['label']},
- ],
- },
- ]
- if item['source'] == CELL_SOURCE:
- sections.append(
- {
- 'key': 'market-handles',
- 'title': '市场办理',
- 'storageCert': storage_cert,
- 'insurance': insurance,
- }
- )
- actions = []
- if item['source'] == CELL_SOURCE and item['sampleCode']:
- actions = [
- {'key': 'storageCert', 'label': '办理储存证'},
- {'key': 'insurance', 'label': '办理保险'},
- ]
- return {'id': item['id'], 'card': item, 'progress': progress, 'sections': sections, 'actions': actions}
- def _eligible_region_staff_query(
- db: Session, tenant_no: str, region_id: str
- ):
- return (
- db.query(SysUser)
- .join(
- BizCollectionRegionStaff,
- BizCollectionRegionStaff.user_id == SysUser.id,
- )
- .filter(
- BizCollectionRegionStaff.region_id == region_id,
- BizCollectionRegionStaff.tenant_no == tenant_no,
- BizCollectionRegionStaff.del_flag == 0,
- SysUser.tenant_no == tenant_no,
- SysUser.status == '1',
- SysUser.del_flag == 0,
- )
- )
- def get_sample_form_options(
- db: Session,
- *,
- sample_id: str,
- action: Optional[Literal['storage-cert', 'insurance']] = None,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> Dict[str, List[Dict[str, str]]]:
- item = _find_sample(db, sample_id, ctx, user_info)
- if item['source'] != CELL_SOURCE or not item['sampleCode']:
- raise ValueError('该样本不支持办理人员选择')
- users = (
- _eligible_region_staff_query(db, ctx.tenant_no, item['regionId'])
- .distinct()
- .order_by(SysUser.nick_name.asc(), SysUser.id.asc())
- .all()
- )
- referenced_ids = []
- if action:
- model = (
- BizProtocolStorageCert
- if action == 'storage-cert'
- else BizProtocolFullmoonTodo
- )
- historical = (
- db.query(model)
- .filter(
- model.collection_protocol_id == item['parentId'],
- model.tenant_no == ctx.tenant_no,
- model.del_flag == 0,
- )
- .order_by(
- model.update_time.desc(),
- model.create_time.desc(),
- model.id.desc(),
- )
- .first()
- )
- if historical is not None and historical.handler_user_id:
- referenced_ids.append(historical.handler_user_id)
- referenced_names = _referenced_user_name_map(
- db, ctx.tenant_no, referenced_ids
- )
- staff = [
- {'value': str(user.id), 'label': str(user.nick_name or '')}
- for user in users
- ]
- included = {option['value'] for option in staff}
- for user_id in referenced_ids:
- value = str(user_id)
- if value not in included and value in referenced_names:
- staff.append({'value': value, 'label': referenced_names[value]})
- included.add(value)
- return {
- 'staff': staff
- }
- def _validate_handler(
- db: Session,
- tenant_no: str,
- region_id: str,
- handler_user_id: str,
- ) -> SysUser:
- user = (
- _eligible_region_staff_query(db, tenant_no, region_id)
- .filter(SysUser.id == handler_user_id)
- .first()
- )
- if user is None:
- raise ValueError('办理人员无效')
- return user
- def _handle_identity_changed(row, normalized: Dict[str, Any]) -> bool:
- """全量表单会回传原办理人/时间;只有语义变化时重新校验资格。"""
- return (
- str(normalized['handler_user_id'] or '').strip()
- != str(row.handler_user_id or '').strip()
- or normalized['handle_time'] != row.handle_time
- )
- def _cell_parent_lock_query(
- db: Session, parent_id: str, ctx: MarketFieldContext
- ):
- return (
- db.query(BizCollectionProtocol)
- .populate_existing()
- .filter(
- BizCollectionProtocol.id == parent_id,
- BizCollectionProtocol.tenant_no == ctx.tenant_no,
- BizCollectionProtocol.del_flag == 0,
- )
- .with_for_update()
- )
- def _lock_cell_parent(
- db: Session, parent_id: str, ctx: MarketFieldContext
- ) -> BizCollectionProtocol:
- parent = _cell_parent_lock_query(db, parent_id, ctx).first()
- if parent is None:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- return parent
- def _handle_child_current_query(
- db: Session,
- model,
- parent_id: str,
- ctx: MarketFieldContext,
- ):
- return (
- db.query(model)
- .populate_existing()
- .filter(
- model.collection_protocol_id == parent_id,
- model.tenant_no == ctx.tenant_no,
- model.del_flag == 0,
- )
- .order_by(
- model.update_time.desc(),
- model.create_time.desc(),
- model.id.desc(),
- )
- .with_for_update()
- )
- def _normalize_partial_handle(
- row,
- fields: Dict[str, Any],
- *,
- insurance: bool,
- handle_time: Optional[datetime],
- handler_user_id: str,
- attachments: str,
- remark: str,
- ) -> Dict[str, Any]:
- if 'handleTime' in fields:
- return apply_handle_payload(
- handle_time=handle_time,
- handler_user_id=handler_user_id,
- attachments=attachments,
- remark=remark,
- )
- if 'handlerUserId' in fields:
- if not handler_user_id:
- raise ValueError('办理人员不能为空')
- handled = (
- insurance_handled(row) if insurance else storage_cert_handled(row)
- )
- if not handled:
- raise ValueError('记录尚未办理,不能单独设置办理人员')
- return {
- 'handle_time': row.handle_time,
- 'handler_user_id': handler_user_id,
- 'attachments': attachments,
- 'remark': remark,
- 'make_or_todo_status': '1',
- 'legacy_date': '',
- }
- return {
- 'handle_time': row.handle_time,
- 'handler_user_id': row.handler_user_id,
- 'attachments': attachments,
- 'remark': remark,
- 'make_or_todo_status': '0',
- 'legacy_date': '',
- }
- def _write_handle(
- db: Session,
- *,
- sample_id: str,
- fields: Dict[str, Any],
- ctx: MarketFieldContext,
- user_info: dict,
- kind: str,
- ) -> Dict[str, Any]:
- source, _, by_row_id = _parse_stable_id(sample_id)
- if source != CELL_SOURCE or by_row_id:
- raise ValueError('该样本不支持此办理操作')
- item = _find_sample(db, sample_id, ctx, user_info)
- if not item['sampleCode']:
- raise ValueError('该样本不支持此办理操作')
- initial_parent_id = item['parentId']
- try:
- _lock_cell_parent(db, initial_parent_id, ctx)
- item = _find_sample(
- db,
- sample_id,
- ctx,
- user_info,
- for_update=True,
- )
- if item['parentId'] != initial_parent_id:
- raise HTTPException(
- status_code=404, detail='数据不存在或无权访问'
- )
- except Exception:
- db.rollback()
- raise
- now = datetime.now()
- create_dept_id = str(
- user_info.get('dept_id') or user_info.get('deptId') or user_info.get('region_id') or ''
- )
- try:
- if kind == 'storage_cert':
- row = _handle_child_current_query(
- db, BizProtocolStorageCert, item['parentId'], ctx
- ).first()
- creating_handle = row is None
- if row is None:
- row = BizProtocolStorageCert(
- id=str(uuid.uuid4()),
- collection_protocol_id=item['parentId'],
- tenant_no=ctx.tenant_no,
- create_by=ctx.user_id,
- create_dept_id=create_dept_id,
- create_time=now,
- del_flag=0,
- )
- db.add(row)
- handle_time = (
- _parse_dt(fields.get('handleTime'))
- if 'handleTime' in fields
- else row.handle_time
- )
- handler_user_id = (
- str(fields.get('handlerUserId') or '').strip()
- if 'handlerUserId' in fields
- else str(row.handler_user_id or '').strip()
- )
- attachments = (
- str(fields.get('attachments') or '').strip()
- if 'attachments' in fields
- else str(row.cert_image_urls or '').strip()
- )
- if 'attachments' in fields:
- attachments = validate_attachment_refs(
- db,
- attachments,
- module='sample',
- ctx=ctx,
- creating=creating_handle,
- existing_refs=row.cert_image_urls,
- business_id=row.id,
- )
- remark = (
- str(fields.get('remark') or '').strip()
- if 'remark' in fields
- else str(row.remark or '').strip()
- )
- normalized = _normalize_partial_handle(
- row,
- fields,
- insurance=False,
- handle_time=handle_time,
- handler_user_id=handler_user_id,
- attachments=attachments,
- remark=remark,
- )
- should_validate_handler = creating_handle or _handle_identity_changed(
- row, normalized
- )
- handler = (
- _validate_handler(
- db,
- ctx.tenant_no,
- item['regionId'],
- normalized['handler_user_id'],
- )
- if normalized['handler_user_id'] and should_validate_handler
- else None
- )
- row.handle_time = normalized['handle_time']
- row.handler_user_id = normalized['handler_user_id']
- row.cert_image_urls = normalized['attachments']
- row.remark = normalized['remark']
- if 'handleTime' in fields:
- row.make_status = normalized['make_or_todo_status']
- row.make_date = normalized['legacy_date'] or None
- else:
- row = _handle_child_current_query(
- db, BizProtocolFullmoonTodo, item['parentId'], ctx
- ).first()
- creating_handle = row is None
- if row is None:
- row = BizProtocolFullmoonTodo(
- id=str(uuid.uuid4()),
- collection_protocol_id=item['parentId'],
- tenant_no=ctx.tenant_no,
- create_by=ctx.user_id,
- create_dept_id=create_dept_id,
- create_time=now,
- del_flag=0,
- )
- db.add(row)
- handle_time = (
- _parse_dt(fields.get('handleTime'))
- if 'handleTime' in fields
- else row.handle_time
- )
- handler_user_id = (
- str(fields.get('handlerUserId') or '').strip()
- if 'handlerUserId' in fields
- else str(row.handler_user_id or '').strip()
- )
- attachments = (
- str(fields.get('attachments') or '').strip()
- if 'attachments' in fields
- else str(row.insurance_attachments or '').strip()
- )
- if 'attachments' in fields:
- attachments = validate_attachment_refs(
- db,
- attachments,
- module='sample',
- ctx=ctx,
- creating=creating_handle,
- existing_refs=row.insurance_attachments,
- business_id=row.id,
- )
- remark = (
- str(fields.get('remark') or '').strip()
- if 'remark' in fields
- else str(row.handle_remark or '').strip()
- )
- normalized = _normalize_partial_handle(
- row,
- fields,
- insurance=True,
- handle_time=handle_time,
- handler_user_id=handler_user_id,
- attachments=attachments,
- remark=remark,
- )
- should_validate_handler = creating_handle or _handle_identity_changed(
- row, normalized
- )
- handler = (
- _validate_handler(
- db,
- ctx.tenant_no,
- item['regionId'],
- normalized['handler_user_id'],
- )
- if normalized['handler_user_id'] and should_validate_handler
- else None
- )
- row.handle_time = normalized['handle_time']
- row.handler_user_id = normalized['handler_user_id']
- row.insurance_attachments = normalized['attachments']
- row.handle_remark = normalized['remark']
- if 'handleTime' in fields:
- row.todo_status = normalized['make_or_todo_status']
- row.handle_date = normalized['legacy_date'] or None
- row.update_by = ctx.user_id
- row.update_time = now
- db.flush()
- db.commit()
- db.refresh(row)
- except Exception:
- db.rollback()
- raise
- data = _handle_summary(row, insurance=kind == 'insurance')
- data['handlerName'] = handler.nick_name if handler else ''
- return data
- def update_storage_cert(db: Session, **kwargs) -> Dict[str, Any]:
- return _write_handle(db, kind='storage_cert', **kwargs)
- def update_insurance(db: Session, **kwargs) -> Dict[str, Any]:
- return _write_handle(db, kind='insurance', **kwargs)
|