Separate cross-team selection from attribution maintenance and apply the same scope to options, history filters and writes. Add an opt-in tenant permission without granting existing roles. Protect authorization mutation paths against self-grants, validate tenant targets and links, and include menu ancestors atomically. Cover role union, revocation, mobile writes and administration boundaries with regression tests.
416 lines
23 KiB
Python
416 lines
23 KiB
Python
"""项目资料、权限内统计和关联归属事务。"""
|
|
from datetime import date, datetime, timedelta
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import or_, and_, func
|
|
from app.core.data_scope import apply_data_scope
|
|
from app.core.utils import generate_uuid
|
|
from app.crud.child_row_sync import sync_child_rows
|
|
from app.models.project_management import (Project, ProjectTeam, ProjectTeamMember,
|
|
ProjectTeamRegion, ProjectTeamHospital, ProjectAssignmentAudit)
|
|
from app.models.models import (SysUser, BizCollectionRegion, BizCollectionHospital,
|
|
BizPresignOrder, BizCollectionProtocol, BizPilotApplication)
|
|
from app.models.research_donation import BizResearchDonation, BizProtocolDonationConvert
|
|
from app.services.web_auth_roles import iter_user_menu_perm_codes
|
|
|
|
BUSINESS = {
|
|
'presign': (BizPresignOrder, 'protocol_no', 'order_status'),
|
|
'cell_storage': (BizCollectionProtocol, 'protocol_no', 'protocol_status'),
|
|
'research_donation': (BizResearchDonation, 'donation_no', 'donation_status'),
|
|
'pilot_service': (BizPilotApplication, 'application_no', 'application_status'),
|
|
'basic_experiment': (BizPilotApplication, 'application_no', 'application_status'),
|
|
}
|
|
COUNT_TYPES = tuple(k for k in BUSINESS if k != 'presign')
|
|
ALL_PROJECTS_PERMISSION = 'project:project:all'
|
|
|
|
|
|
def scoped(db, model, tenant):
|
|
return db.query(model).filter(model.tenant_no == tenant, model.del_flag == 0)
|
|
|
|
|
|
def get_row(db, model, tenant, row_id, lock=False):
|
|
query = scoped(db, model, tenant).filter(model.id == row_id)
|
|
row = (query.populate_existing().with_for_update() if lock else query).first()
|
|
if not row:
|
|
raise HTTPException(404, '记录不存在或无权访问')
|
|
return row
|
|
|
|
|
|
def has_permission(db, tenant, user, code):
|
|
return user.get('role') == 'super_admin' or code in iter_user_menu_perm_codes(db, user.get('user_id', ''), tenant_no=tenant)
|
|
|
|
|
|
def check_permission(db, tenant, user, *codes):
|
|
if not any(has_permission(db, tenant, user, code) for code in codes):
|
|
raise HTTPException(403, '无对应菜单权限')
|
|
|
|
|
|
def project_brief(db, tenant_no, project_id):
|
|
result = dict(projectId=project_id, projectName=None, teamId=None, teamName=None)
|
|
if project_id:
|
|
row = scoped(db, Project, tenant_no).filter(Project.id == project_id).first()
|
|
if row:
|
|
team = scoped(db, ProjectTeam, tenant_no).filter(ProjectTeam.id == row.team_id).first()
|
|
result.update(projectName=row.project_name, teamId=row.team_id, teamName=team.team_name if team else None)
|
|
return result
|
|
|
|
|
|
def validate_assignment(db, tenant_no, user, project_id, biz_type, *, current_project_id=None, required=False):
|
|
project_id = project_id or None
|
|
if current_project_id:
|
|
if project_id != current_project_id:
|
|
raise HTTPException(400, '请通过项目归属维护调整')
|
|
return current_project_id
|
|
if not project_id:
|
|
if required:
|
|
raise HTTPException(400, '请选择项目')
|
|
return None
|
|
row = get_row(db, Project, tenant_no, project_id, lock=True)
|
|
team = get_row(db, ProjectTeam, tenant_no, row.team_id, lock=True)
|
|
if row.status != 'active' or not team.enabled:
|
|
raise HTTPException(400, '项目或项目组已停用或尚未启用')
|
|
effective_type = 'cell_storage' if biz_type == 'presign' else biz_type
|
|
if effective_type not in row.biz_types.split(','):
|
|
raise HTTPException(400, '项目不支持该业务类型')
|
|
uid = user.get('user_id')
|
|
member = uid == team.leader_user_id or scoped(db, ProjectTeamMember, tenant_no).filter(ProjectTeamMember.team_id == team.id, ProjectTeamMember.user_id == uid).with_for_update().first()
|
|
if not member and not has_permission(db, tenant_no, user, ALL_PROJECTS_PERMISSION):
|
|
raise HTTPException(403, '仅可选择所在项目组的项目,跨组选择需全部项目权限')
|
|
return project_id
|
|
|
|
|
|
def business_query(db, tenant, user, kind):
|
|
if kind not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
model = BUSINESS[kind][0]
|
|
query = scoped(db, model, tenant)
|
|
if model is BizPilotApplication:
|
|
query = query.filter(model.biz_type == kind)
|
|
return apply_data_scope(query, model, user, db, tenant)
|
|
|
|
|
|
def filtered_query(db, tenant, user, kind, filters):
|
|
model, number, status = BUSINESS[kind]
|
|
q = business_query(db, tenant, user, kind)
|
|
if filters.get('projectId'):
|
|
q = q.filter(model.project_id == filters['projectId'])
|
|
if filters.get('teamId'):
|
|
q = q.filter(model.project_id.in_(scoped(db, Project, tenant).filter(Project.team_id == filters['teamId']).with_entities(Project.id)))
|
|
if filters.get('unassigned'):
|
|
q = q.filter(model.project_id.is_(None))
|
|
if filters.get('keyword'):
|
|
q = q.filter(getattr(model, number).contains(filters['keyword']))
|
|
for key, op in [('startDate', 'start'), ('endDate', 'end')]:
|
|
if filters.get(key):
|
|
try:
|
|
day = date.fromisoformat(str(filters[key]))
|
|
except ValueError:
|
|
raise HTTPException(400, '日期格式应为 YYYY-MM-DD')
|
|
q = q.filter(model.create_time >= datetime.combine(day, datetime.min.time())) if op == 'start' else q.filter(model.create_time < datetime.combine(day + timedelta(days=1), datetime.min.time()))
|
|
return q
|
|
|
|
|
|
def record_dict(db, tenant, kind, row):
|
|
return dict(id=row.id, bizType=kind, bizNo=getattr(row, BUSINESS[kind][1]), status=getattr(row, BUSINESS[kind][2]), createTime=row.create_time, **project_brief(db, tenant, row.project_id))
|
|
|
|
|
|
def records(db, tenant, user, biz_type, page=1, pageSize=20, **filters):
|
|
if biz_type not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
model, _, status = BUSINESS[biz_type]
|
|
q = filtered_query(db, tenant, user, biz_type, filters)
|
|
status_column = getattr(model, status)
|
|
status_counts = {str(value or ''): count for value, count in
|
|
q.with_entities(status_column, func.count(model.id)).group_by(status_column).all()}
|
|
if filters.get('status'):
|
|
q = q.filter(getattr(model, status) == filters['status'])
|
|
total = q.count()
|
|
rows = q.order_by(model.create_time.desc(), model.id).offset((page-1)*pageSize).limit(pageSize).all()
|
|
return dict(list=[record_dict(db, tenant, biz_type, r) for r in rows], total=total, statusCounts=status_counts)
|
|
|
|
|
|
def statistics(db, tenant, user, groupBy='project', **filters):
|
|
if groupBy not in ('project', 'team'):
|
|
raise HTTPException(400, '分组方式不支持')
|
|
totals = dict.fromkeys(COUNT_TYPES, 0)
|
|
unassigned = dict.fromkeys(COUNT_TYPES, 0)
|
|
counts = {}
|
|
for kind in COUNT_TYPES:
|
|
model = BUSINESS[kind][0]
|
|
counts[kind] = filtered_query(db, tenant, user, kind, filters).with_entities(
|
|
model.project_id, func.count(model.id)).group_by(model.project_id).all()
|
|
project_ids = {pid for values in counts.values() for pid, _ in values if pid}
|
|
metadata = {}
|
|
if project_ids:
|
|
rows = scoped(db, Project, tenant).outerjoin(ProjectTeam, and_(
|
|
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant,
|
|
ProjectTeam.del_flag == 0)).filter(Project.id.in_(project_ids)).with_entities(
|
|
Project.id, Project.project_name, Project.team_id, ProjectTeam.team_name).all()
|
|
metadata = {r.id: r for r in rows}
|
|
items = {}
|
|
for kind, values in counts.items():
|
|
for project_id, count in values:
|
|
totals[kind] += count
|
|
if not project_id:
|
|
unassigned[kind] += count
|
|
continue
|
|
info = metadata.get(project_id)
|
|
team_id = info.team_id if info else None
|
|
team_name = info.team_name if info else None
|
|
key = team_id if groupBy == 'team' else project_id
|
|
item = items.setdefault(key, dict(id=key,
|
|
name=team_name if groupBy == 'team' else (info.project_name if info else None),
|
|
teamName=team_name, **dict.fromkeys(COUNT_TYPES, 0)))
|
|
item[kind] += count
|
|
return dict(items=list(items.values()), totals=totals, unassignedTotals=unassigned,
|
|
presignTotal=filtered_query(db, tenant, user, 'presign', filters).count())
|
|
|
|
|
|
def linked_records(db, tenant, user, kind, row_id, lock=False, current_read=False):
|
|
"""构建双向来源链;无权关联记录导致整次操作拒绝。"""
|
|
def graph_query(model):
|
|
query = scoped(db, model, tenant).populate_existing()
|
|
return query.with_for_update() if current_read else query
|
|
|
|
seen, pending, rows = set(), [(kind, row_id)], []
|
|
while pending:
|
|
current, rid = pending.pop()
|
|
if (current, rid) in seen:
|
|
continue
|
|
seen.add((current, rid))
|
|
model = BUSINESS[current][0]
|
|
row = get_row(db, model, tenant, rid, lock=current_read)
|
|
if not business_query(db, tenant, user, current).filter(model.id == rid).first():
|
|
raise HTTPException(403, '关联记录不存在或无权访问')
|
|
rows.append((current, row))
|
|
if current == 'presign':
|
|
pending += [('cell_storage', r.id) for r in graph_query(BizCollectionProtocol).filter(BizCollectionProtocol.source_presign_order_id == rid).all()]
|
|
if row.converted_protocol_id:
|
|
pending.append(('cell_storage', row.converted_protocol_id))
|
|
if current == 'cell_storage':
|
|
if row.source_presign_order_id:
|
|
pending.append(('presign', row.source_presign_order_id))
|
|
pending += [('presign', r.id) for r in graph_query(BizPresignOrder).filter(BizPresignOrder.converted_protocol_id == rid).all()]
|
|
pending += [('research_donation', r.id) for r in graph_query(BizResearchDonation).filter(BizResearchDonation.source_protocol_id == rid).all()]
|
|
if current == 'research_donation' and row.source_protocol_id:
|
|
pending.append(('cell_storage', row.source_protocol_id))
|
|
if current in ('cell_storage', 'research_donation'):
|
|
col = BizProtocolDonationConvert.protocol_id if current == 'cell_storage' else BizProtocolDonationConvert.research_donation_id
|
|
for link in graph_query(BizProtocolDonationConvert).filter(col == rid).all():
|
|
pending.extend([('cell_storage', link.protocol_id), ('research_donation', link.research_donation_id)])
|
|
rows.sort(key=lambda pair: (pair[0], pair[1].id))
|
|
if lock:
|
|
rows = [(k, get_row(db, BUSINESS[k][0], tenant, r.id, lock=True)) for k, r in rows]
|
|
return rows
|
|
|
|
|
|
def preview_assignment(db, tenant, user, kind, row_id, project_id):
|
|
check_permission(db, tenant, user, 'project:assignment:edit')
|
|
rows = linked_records(db, tenant, user, kind, row_id)
|
|
for k, row in rows:
|
|
validate_assignment(db, tenant, user, project_id, k, required=True)
|
|
return {'records': [record_dict(db, tenant, k, r) for k, r in rows]}
|
|
|
|
|
|
def assign(db, tenant, user, kind, row_id, project_id, reason, expected):
|
|
check_permission(db, tenant, user, 'project:assignment:edit')
|
|
if not reason or not reason.strip():
|
|
raise HTTPException(400, '请填写调整原因')
|
|
rows = linked_records(db, tenant, user, kind, row_id, lock=True)
|
|
actual = {(k, r.id, r.project_id) for k, r in rows}
|
|
refreshed = linked_records(db, tenant, user, kind, row_id, current_read=True)
|
|
snapshot = {(r['bizType'], r['id'], r.get('projectId')) for r in expected}
|
|
if actual != snapshot or actual != {(k, r.id, r.project_id) for k, r in refreshed} or len(expected) != len(actual):
|
|
raise HTTPException(409, '关联记录或归属已变更,请重新预览')
|
|
for k, row in rows:
|
|
validate_assignment(db, tenant, user, project_id, k, required=True)
|
|
new_name = project_brief(db, tenant, project_id)['projectName']
|
|
operator = scoped(db, SysUser, tenant).filter(SysUser.id == user.get('user_id')).first()
|
|
for k, row in rows:
|
|
db.add(ProjectAssignmentAudit(id=generate_uuid(), tenant_no=tenant, biz_type=k, biz_id=row.id, old_project_id=row.project_id, new_project_id=project_id, old_project_name=project_brief(db, tenant, row.project_id)['projectName'], new_project_name=new_name, reason=reason.strip(), operator_name=operator.nick_name if operator else user.get('user_id'), create_by=user.get('user_id'), create_dept_id=user.get('dept_id') or ''))
|
|
row.project_id = project_id
|
|
row.update_by = user.get('user_id')
|
|
row.update_time = datetime.now()
|
|
db.commit()
|
|
return {'updated': len(rows)}
|
|
|
|
|
|
def assignment_history(db, tenant, user, kind, row_id):
|
|
model = BUSINESS.get(kind, (None,))[0]
|
|
if model is None or not business_query(db, tenant, user, kind).filter(model.id == row_id).first():
|
|
raise HTTPException(404, '记录不存在或无权访问')
|
|
rows = scoped(db, ProjectAssignmentAudit, tenant).filter(ProjectAssignmentAudit.biz_type == kind, ProjectAssignmentAudit.biz_id == row_id).order_by(ProjectAssignmentAudit.create_time.desc()).all()
|
|
return {'list': [dict(id=r.id, oldProjectName=r.old_project_name, newProjectName=r.new_project_name, reason=r.reason, operatorName=r.operator_name, createTime=r.create_time) for r in rows]}
|
|
|
|
|
|
CHILDREN = (
|
|
('members', ProjectTeamMember, 'userId', 'user_id', SysUser, 'nick_name'),
|
|
('regions', ProjectTeamRegion, 'regionId', 'region_id', BizCollectionRegion, 'region_name'),
|
|
('hospitals', ProjectTeamHospital, 'hospitalId', 'hospital_id', BizCollectionHospital, 'hospital_name'),
|
|
)
|
|
|
|
|
|
def serialize_entity(db, tenant, row):
|
|
leader = scoped(db, SysUser, tenant).filter(SysUser.id == row.leader_user_id).first()
|
|
result = dict(id=row.id, leaderUserId=row.leader_user_id, leaderName=leader.nick_name if leader else '', remark=row.remark, createTime=row.create_time)
|
|
if isinstance(row, ProjectTeam):
|
|
result.update(teamName=row.team_name, enabled=bool(row.enabled))
|
|
for key, model, public, attr, ref_model, name in CHILDREN:
|
|
children = scoped(db, model, tenant).filter(model.team_id == row.id).order_by(model.order_num).all()
|
|
result[key] = []
|
|
for child in children:
|
|
ref = scoped(db, ref_model, tenant).filter(ref_model.id == getattr(child, attr)).first()
|
|
result[key].append(dict(id=child.id, **{public: getattr(child, attr)}, name=getattr(ref, name) if ref else ''))
|
|
else:
|
|
result.update(projectNo=row.project_no, projectName=row.project_name, teamId=row.team_id, teamName=project_brief(db, tenant, row.id)['teamName'], bizTypes=row.biz_types.split(','), startDate=row.start_date, endDate=row.end_date, status=row.status)
|
|
return result
|
|
|
|
|
|
def list_entities(db, tenant, model, page=1, pageSize=20, **filters):
|
|
q = scoped(db, model, tenant)
|
|
name = model.team_name if model is ProjectTeam else model.project_name
|
|
if filters.get('keyword'):
|
|
q = q.filter(name.contains(filters['keyword']))
|
|
for public, attr in [('leaderUserId', 'leader_user_id'), ('teamId', 'team_id'), ('status', 'status'), ('enabled', 'enabled')]:
|
|
if filters.get(public) not in (None, '') and hasattr(model, attr):
|
|
q = q.filter(getattr(model, attr) == filters[public])
|
|
return dict(total=q.count(), list=[serialize_entity(db, tenant, r) for r in q.order_by(model.create_time.desc(), model.id).offset((page-1)*pageSize).limit(pageSize).all()])
|
|
|
|
|
|
def references(db, tenant):
|
|
return dict(users=[dict(id=r.id, name=r.nick_name) for r in scoped(db, SysUser, tenant).filter(SysUser.status == '1').all()], regions=[dict(id=r.id, name=r.region_name) for r in scoped(db, BizCollectionRegion, tenant).all()], hospitals=[dict(id=r.id, name=r.hospital_name, regionId=r.region_id) for r in scoped(db, BizCollectionHospital, tenant).all()])
|
|
|
|
|
|
def has_business(db, tenant, project_id):
|
|
# Deleted business is still a historical reference and prevents structural changes.
|
|
return any(db.query(model.id).filter(model.tenant_no == tenant, model.project_id == project_id).with_for_update().first() for model in {v[0] for v in BUSINESS.values()})
|
|
|
|
|
|
def save_entity(db, tenant, user, model, body, row_id=None):
|
|
row = get_row(db, model, tenant, row_id, lock=True) if row_id else model(id=generate_uuid(), tenant_no=tenant, create_by=user.get('user_id'), create_dept_id=user.get('dept_id') or '')
|
|
name_key = 'teamName' if model is ProjectTeam else 'projectName'
|
|
if not str(body.get(name_key) or '').strip():
|
|
raise HTTPException(400, '名称不能为空')
|
|
leader = get_row(db, SysUser, tenant, body.get('leaderUserId'))
|
|
if leader.status != '1':
|
|
raise HTTPException(400, '负责人已停用')
|
|
row.leader_user_id = leader.id
|
|
row.remark = body.get('remark')
|
|
row.update_by = user.get('user_id')
|
|
row.update_time = datetime.now()
|
|
if model is ProjectTeam:
|
|
row.team_name = body[name_key].strip()
|
|
row.enabled = int(bool(body.get('enabled', True)))
|
|
db.add(row)
|
|
for key, child_model, public, attr, ref_model, _ in CHILDREN:
|
|
children = body.get(key, [])
|
|
seen = set()
|
|
for child in children:
|
|
ref_id = child.get(public)
|
|
if ref_id in seen:
|
|
raise HTTPException(400, '关联资料不能重复')
|
|
seen.add(ref_id)
|
|
get_row(db, ref_model, tenant, ref_id)
|
|
if child.get('id'):
|
|
existing = db.query(child_model).filter(child_model.id == child['id']).first()
|
|
if existing and (existing.tenant_no != tenant or existing.team_id != row.id or existing.del_flag):
|
|
raise HTTPException(400, '子行不属于当前项目组')
|
|
try:
|
|
sync_child_rows(db, child_model, parent_attr='team_id', parent_id=row.id, tenant_no=tenant, user_id=user.get('user_id'), items=children,
|
|
build_row=lambda rid, cm=child_model: cm(id=rid, tenant_no=tenant, team_id=row.id, create_by=user.get('user_id'), create_dept_id=user.get('dept_id') or ''),
|
|
apply_fields=lambda child, item, index, now, a=attr, p=public: (setattr(child, a, item[p]), setattr(child, 'order_num', index)),
|
|
natural_key=lambda item, p=public: item[p], row_natural_key=lambda child, a=attr: getattr(child, a))
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
else:
|
|
team = get_row(db, ProjectTeam, tenant, body.get('teamId'), lock=True)
|
|
if row_id and row.team_id != team.id and has_business(db, tenant, row.id):
|
|
raise HTTPException(400, '项目已有业务,不可更换项目组')
|
|
kinds = body.get('bizTypes') or []
|
|
if not kinds or any(k not in COUNT_TYPES for k in kinds):
|
|
raise HTTPException(400, '请选择有效业务类型')
|
|
status = body.get('status', 'pending')
|
|
if status not in ('pending', 'active', 'completed', 'disabled'):
|
|
raise HTTPException(400, '项目状态无效')
|
|
row.project_name, row.team_id, row.biz_types, row.status = body[name_key].strip(), team.id, ','.join(dict.fromkeys(kinds)), status
|
|
if not row_id:
|
|
row.project_no = 'XM' + datetime.now().strftime('%Y%m%d') + generate_uuid().replace('-', '')[:16].upper()
|
|
try:
|
|
row.start_date = date.fromisoformat(body['startDate']) if body.get('startDate') else None
|
|
row.end_date = date.fromisoformat(body['endDate']) if body.get('endDate') else None
|
|
except ValueError:
|
|
raise HTTPException(400, '日期格式应为 YYYY-MM-DD')
|
|
if row.start_date and row.end_date and row.start_date > row.end_date:
|
|
raise HTTPException(400, '结束日期不能早于开始日期')
|
|
db.add(row)
|
|
db.commit()
|
|
return serialize_entity(db, tenant, row)
|
|
|
|
|
|
def delete_entity(db, tenant, user, model, row_id):
|
|
row = get_row(db, model, tenant, row_id, lock=True)
|
|
if model is Project and has_business(db, tenant, row.id):
|
|
raise HTTPException(400, '项目已被业务引用,请停用')
|
|
if model is ProjectTeam and db.query(Project.id).filter(Project.tenant_no == tenant, Project.team_id == row.id).with_for_update().first():
|
|
raise HTTPException(400, '项目组已被项目引用,请停用')
|
|
row.del_flag, row.update_by = 1, user.get('user_id')
|
|
if model is ProjectTeam:
|
|
for _, child_model, *_ in CHILDREN:
|
|
for child in scoped(db, child_model, tenant).filter(child_model.team_id == row.id).all():
|
|
child.del_flag, child.update_by = 1, user.get('user_id')
|
|
db.commit()
|
|
return {'id': row.id}
|
|
|
|
|
|
def apply_project_choice_scope(query, db, tenant, user):
|
|
"""Apply team membership or the explicit tenant-wide project-choice permission.
|
|
|
|
The caller joins the tenant-scoped, non-deleted ProjectTeam table. Business
|
|
data scope and project availability remain separate caller constraints.
|
|
"""
|
|
if has_permission(db, tenant, user, ALL_PROJECTS_PERMISSION):
|
|
return query
|
|
memberships = scoped(db, ProjectTeamMember, tenant).filter(
|
|
ProjectTeamMember.user_id == user.get('user_id')).with_entities(ProjectTeamMember.team_id)
|
|
return query.filter(or_(ProjectTeam.leader_user_id == user.get('user_id'),
|
|
Project.team_id.in_(memberships)))
|
|
|
|
|
|
def options(db, tenant, user, biz_type, keyword=None):
|
|
if biz_type not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
effective_type = 'cell_storage' if biz_type == 'presign' else biz_type
|
|
q = scoped(db, Project, tenant).join(ProjectTeam, and_(
|
|
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant,
|
|
ProjectTeam.del_flag == 0, ProjectTeam.enabled == 1)).filter(Project.status == 'active')
|
|
q = apply_project_choice_scope(q, db, tenant, user)
|
|
if keyword:
|
|
q = q.filter(or_(Project.project_name.contains(keyword), Project.project_no.contains(keyword)))
|
|
rows = q.with_entities(Project.id, Project.project_name, Project.project_no,
|
|
Project.team_id, Project.biz_types, ProjectTeam.team_name).order_by(
|
|
Project.create_time.desc(), Project.id).all()
|
|
result = [dict(id=r.id, projectName=r.project_name, projectNo=r.project_no,
|
|
teamId=r.team_id, teamName=r.team_name, disabled=False)
|
|
for r in rows if effective_type in r.biz_types.split(',')]
|
|
from app.core.config import settings
|
|
return {'list': result, 'required': settings.PROJECT_ASSIGNMENT_REQUIRED}
|
|
|
|
|
|
def filter_options(db, tenant, user, biz_type, keyword=None, *, business_scope=None):
|
|
"""Intersect project-choice scope with visible history; include stopped projects."""
|
|
if biz_type not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
model = BUSINESS[biz_type][0]
|
|
visible = business_scope if business_scope is not None else business_query(db, tenant, user, biz_type)
|
|
ids = visible.with_entities(model.project_id).filter(model.project_id.isnot(None)).distinct()
|
|
q = scoped(db, Project, tenant).join(ProjectTeam, and_(
|
|
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant,
|
|
ProjectTeam.del_flag == 0)).filter(Project.id.in_(ids))
|
|
q = apply_project_choice_scope(q, db, tenant, user)
|
|
if keyword:
|
|
q = q.filter(or_(Project.project_name.contains(keyword), Project.project_no.contains(keyword)))
|
|
rows = q.with_entities(Project.id, Project.project_name, Project.project_no,
|
|
Project.team_id, ProjectTeam.team_name).order_by(Project.create_time.desc(), Project.id).all()
|
|
return {'list': [dict(id=r.id, projectName=r.project_name, projectNo=r.project_no,
|
|
teamId=r.team_id, teamName=r.team_name, disabled=False) for r in rows],
|
|
'required': False}
|