Files
zkxw/backend/app/services/market_sample.py
T
2026-10-13 15:18:01 +08:00

1199 lines
40 KiB
Python

"""市场移动端统一样本查询与轻量办理。"""
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)