Files
zkxw/backend/app/services/project_assignment_records.py
T
zlwu fa5efa9629 feat(project): show business fields in statistics details
Reuse assignment business columns and keyword searches in the four statistics drill-down lists. Add a statistics-permission read endpoint sharing tenant-scoped detail queries without granting assignment access.

Related to issue #13. Independent static review completed. User self-test arrangement: regression cases authored, no tests or builds executed.
2026-11-04 14:40:04 +08:00

198 lines
10 KiB
Python

"""项目业务明细检索;统计读取与归属维护分别校验菜单权限。"""
from collections import defaultdict
from fastapi import HTTPException
from sqlalchemy import and_, func, or_
from app.models.models import (
BizChannelServiceOrder, BizCollectionHospital,
BizPilotPatient, BizProtocolCollectionInfo, BizProtocolPartyCustomer, BizProtocolSample, SysUser,
)
from app.models.research_donation import (
BizResearchDonationCollection, BizResearchDonationParty, BizResearchDonationSample,
)
from app.models.project_management import Project, ProjectTeam
from app.services import project_management as projects
PARTY_MODELS = {
'cell_storage': (BizProtocolPartyCustomer, BizProtocolCollectionInfo, 'collection_protocol_id'),
'research_donation': (BizResearchDonationParty, BizResearchDonationCollection, 'research_donation_id'),
}
SAMPLE_MODELS = {
'cell_storage': BizProtocolSample,
'research_donation': BizResearchDonationSample,
}
def keyword_condition(db, tenant, kind, keyword):
"""用 EXISTS 检索子表,避免多条患者/采集信息放大主单计数。"""
model, number, _ = projects.BUSINESS[kind]
conditions = [getattr(model, number).contains(keyword, autoescape=True)]
if kind in PARTY_MODELS:
party, collection, parent_key = PARTY_MODELS[kind]
sample = SAMPLE_MODELS[kind]
conditions.append(projects.scoped(db, sample, tenant).filter(
getattr(sample, parent_key) == model.id,
sample.sample_code.contains(keyword, autoescape=True),
).exists())
conditions.append(projects.scoped(db, party, tenant).filter(
getattr(party, parent_key) == model.id,
or_(party.mother_name.contains(keyword, autoescape=True),
party.mother_phone.contains(keyword, autoescape=True)),
).exists())
hospitals = projects.scoped(db, BizCollectionHospital, tenant).filter(
BizCollectionHospital.hospital_name.contains(keyword, autoescape=True),
).with_entities(BizCollectionHospital.id)
sales_users = projects.scoped(db, SysUser, tenant).filter(
SysUser.nick_name.contains(keyword, autoescape=True),
).with_entities(SysUser.id)
conditions.append(projects.scoped(db, collection, tenant).filter(
getattr(collection, parent_key) == model.id,
or_(collection.hospital_id.in_(hospitals), collection.sales_user_id.in_(sales_users)),
).exists())
elif kind == 'pilot_service':
conditions.extend(column.contains(keyword, autoescape=True) for column in (
model.application_name, model.channel_name,
))
conditions.append(projects.scoped(db, BizPilotPatient, tenant).filter(
BizPilotPatient.pilot_application_id == model.id,
or_(BizPilotPatient.patient_name.contains(keyword, autoescape=True),
BizPilotPatient.phone_no.contains(keyword, autoescape=True)),
).exists())
conditions.append(projects.scoped(db, BizChannelServiceOrder, tenant).filter(
BizChannelServiceOrder.id == model.channel_service_order_id,
BizChannelServiceOrder.order_name.contains(keyword, autoescape=True),
).exists())
else:
conditions.extend(column.contains(keyword, autoescape=True) for column in (
model.project_title, model.customer_name, model.customer_contact, model.customer_phone,
))
return or_(*conditions)
def child_rows(db, tenant, model, parent_key, ids, fields):
"""仅批量读取当前页所需字段;稳定排序保留多条有效子记录。"""
result = defaultdict(list)
if ids:
parent = getattr(model, parent_key)
rows = projects.scoped(db, model, tenant).filter(parent.in_(ids)).with_entities(
parent, *(getattr(model, field) for field in fields),
).order_by(model.create_time, model.id).all()
for row in rows:
result[getattr(row, parent_key)].append(row)
return result
def joined_values(rows, field):
return '、'.join(dict.fromkeys(str(getattr(row, field)) for row in rows if getattr(row, field)))
def project_metadata(db, tenant, rows):
ids = {row.project_id for row in rows if row.project_id}
if not ids:
return {}
values = projects.scoped(db, Project, tenant).outerjoin(ProjectTeam, and_(
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant, ProjectTeam.del_flag == 0,
)).filter(Project.id.in_(ids)).with_entities(
Project.id, Project.project_name, Project.team_id, ProjectTeam.team_name,
).all()
return {row.id: dict(projectName=row.project_name, teamId=row.team_id, teamName=row.team_name)
for row in values}
def business_details(db, tenant, kind, rows):
ids = [row.id for row in rows]
result = {row.id: {} for row in rows}
if not ids:
return result
if kind in PARTY_MODELS:
party, collection, parent_key = PARTY_MODELS[kind]
parties = child_rows(db, tenant, party, parent_key, ids, ('mother_name', 'mother_phone'))
samples = child_rows(db, tenant, SAMPLE_MODELS[kind], parent_key, ids, ('sample_code',))
collections = child_rows(db, tenant, collection, parent_key, ids,
('hospital_id', 'sales_user_id', 'sample_collect_time'))
all_collections = [item for values in collections.values() for item in values]
hospital_ids = {item.hospital_id for item in all_collections if item.hospital_id}
sales_ids = {item.sales_user_id for item in all_collections if item.sales_user_id}
hospitals = dict(projects.scoped(db, BizCollectionHospital, tenant).filter(
BizCollectionHospital.id.in_(hospital_ids),
).with_entities(BizCollectionHospital.id, BizCollectionHospital.hospital_name).all()) if hospital_ids else {}
sales_users = dict(projects.scoped(db, SysUser, tenant).filter(
SysUser.id.in_(sales_ids),
).with_entities(SysUser.id, SysUser.nick_name).all()) if sales_ids else {}
for row in rows:
info = collections[row.id]
details = dict(
sampleCodes=joined_values(samples[row.id], 'sample_code'),
motherName=joined_values(parties[row.id], 'mother_name'),
motherPhone=joined_values(parties[row.id], 'mother_phone'),
hospitalName='、'.join(dict.fromkeys(hospitals[item.hospital_id] for item in info
if hospitals.get(item.hospital_id))),
salesUserName='、'.join(dict.fromkeys(sales_users[item.sales_user_id] for item in info
if sales_users.get(item.sales_user_id))),
)
if kind == 'cell_storage':
details['collectionDate'] = '、'.join(dict.fromkeys(
item.sample_collect_time.strftime('%Y-%m-%d') for item in info if item.sample_collect_time))
else:
value = row.donation_date or ''
details['donationDate'] = f'{value[:4]}-{value[4:6]}-{value[6:]}' if len(value) == 8 else value
result[row.id] = details
elif kind == 'pilot_service':
patients = child_rows(db, tenant, BizPilotPatient, 'pilot_application_id', ids,
('patient_name', 'phone_no'))
order_ids = {row.channel_service_order_id for row in rows if row.channel_service_order_id}
orders = dict(projects.scoped(db, BizChannelServiceOrder, tenant).filter(
BizChannelServiceOrder.id.in_(order_ids),
).with_entities(BizChannelServiceOrder.id, BizChannelServiceOrder.order_name).all()) if order_ids else {}
for row in rows:
result[row.id] = dict(
applicationName=row.application_name, channelName=row.channel_name,
serviceOrderName=orders.get(row.channel_service_order_id),
patientName=joined_values(patients[row.id], 'patient_name'),
patientPhone=joined_values(patients[row.id], 'phone_no'),
)
else:
for row in rows:
result[row.id] = dict(projectTitle=row.project_title, customerName=row.customer_name,
customerContact=row.customer_contact, customerPhone=row.customer_phone)
return result
def records(db, tenant, user, biz_type, page=1, pageSize=20, **filters):
projects.check_permission(db, tenant, user, 'project:assignment:edit')
return _records(db, tenant, user, biz_type, page, pageSize, **filters)
def statistics_records(db, tenant, user, biz_type, page=1, pageSize=20, **filters):
projects.check_permission(db, tenant, user, 'project:statistics:list')
return _records(db, tenant, user, biz_type, page, pageSize, **filters)
def _records(db, tenant, user, biz_type, page, pageSize, **filters):
if biz_type not in projects.COUNT_TYPES:
raise HTTPException(400, '业务明细仅支持四类正式业务')
model, number, status = projects.BUSINESS[biz_type]
# 继续使用现有租户、主单数据范围、项目/项目组和日期条件。
query = projects.filtered_query(db, tenant, user, biz_type, {**filters, 'keyword': None})
keyword = (filters.get('keyword') or '').strip()
if keyword:
query = query.filter(keyword_condition(db, tenant, biz_type, keyword))
status_column = getattr(model, status)
status_counts = {str(value or ''): count for value, count in query.with_entities(
status_column, func.count(model.id),
).group_by(status_column).all()}
if filters.get('status'):
query = query.filter(status_column == filters['status'])
total = query.count()
rows = query.order_by(model.create_time.desc(), model.id).offset((page - 1) * pageSize).limit(pageSize).all()
metadata = project_metadata(db, tenant, rows)
details = business_details(db, tenant, biz_type, rows)
return dict(list=[dict(
id=row.id, bizType=biz_type, bizNo=getattr(row, number), status=getattr(row, status),
createTime=row.create_time, projectId=row.project_id,
**metadata.get(row.project_id, dict(projectName=None, teamId=None, teamName=None)),
**details[row.id],
) for row in rows], total=total, statusCounts=status_counts)