feat(pl): filter recharge applies by mineVehicles plates

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-27 18:25:58 +08:00
1 parent a903206dbe
commit ae1d01bf07
2 files changed
+219 -11

No files matched your search

@@ -22,6 +22,7 @@ from app.schemas.pl.vehicle_recharge import (
VehicleRechargeApplyCreate,
VehicleRechargeApplyRead,
VehicleRechargeApplyUpdate,
VehicleRechargeApprovalFlowItem,
VehicleRechargeApprovalRead,
VehicleRechargeDisburseBoardItem,
VehicleRechargeRecordCreate,
@@ -29,6 +30,7 @@ from app.schemas.pl.vehicle_recharge import (
VehicleRechargeRecordUpdate,
)
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
from app.services.pl_vehicle_mine_vehicles import license_plates_for_user_personnel
_EDITABLE_STATUSES = frozenset({"draft", "rejected"})
_BIZ_TYPE_VEHICLE_RECHARGE = "vehicle_recharge"
@@ -115,7 +117,12 @@ def _resolve_applicant_id(
def _get_apply_row(
db: Session, tenant_no: str, apply_id: str, user: DbUserContext
db: Session,
tenant_no: str,
apply_id: str,
user: DbUserContext,
*,
mine_vehicles: bool = False,
) -> PlVehicleRechargeApply:
row = (
db.query(PlVehicleRechargeApply)
@@ -128,7 +135,12 @@ def _get_apply_row(
)
if not row:
raise HTTPException(status_code=404, detail="申请单不存在")
assert_pl_row_in_scope(db, row, PlVehicleRechargeApply, user, tenant_no)
if mine_vehicles:
plates = license_plates_for_user_personnel(db, tenant_no, user.id)
if row.license_plate not in plates:
raise HTTPException(status_code=403, detail="无权查看该申请")
else:
assert_pl_row_in_scope(db, row, PlVehicleRechargeApply, user, tenant_no)
return row
@@ -178,10 +190,10 @@ def approval_to_read(row: PlVehicleRechargeApproval) -> VehicleRechargeApprovalR
)
def _list_approvals(
def _list_approval_rows(
db: Session, tenant_no: str, apply_id: str
) -> list[VehicleRechargeApprovalRead]:
rows = (
) -> list[PlVehicleRechargeApproval]:
return (
db.query(PlVehicleRechargeApproval)
.filter(
PlVehicleRechargeApproval.apply_id == apply_id,
@@ -195,7 +207,93 @@ def _list_approvals(
)
.all()
)
return [approval_to_read(row) for row in rows]
def _list_approvals(
db: Session, tenant_no: str, apply_id: str
) -> list[VehicleRechargeApprovalRead]:
return [approval_to_read(row) for row in _list_approval_rows(db, tenant_no, apply_id)]
def _approval_flow_item_from_row(
row: PlVehicleRechargeApproval,
*,
node_order: int,
) -> VehicleRechargeApprovalFlowItem:
return VehicleRechargeApprovalFlowItem(
id=row.id,
applyId=row.apply_id,
nodeName=row.node_name or "节点",
nodeOrder=node_order,
approvalRound=row.approval_round,
approvalStatus=row.approval_status,
approvalOpinion=row.approval_opinion,
approverName=row.approver_name or "",
approvalTime=row.approval_time,
)
def _build_approval_flow(
db: Session,
row: PlVehicleRechargeApply,
approval_rows: list[PlVehicleRechargeApproval],
) -> list[VehicleRechargeApprovalFlowItem]:
"""当前轮:流程全部节点;已审带信息,未审为 pending。"""
if not row.wf_id or not (row.approval_round or 0):
return []
nodes = _workflow_nodes(db, row.wf_id, row.tenant_no)
if not nodes:
return []
current_round = row.approval_round or 0
round_approvals = [a for a in approval_rows if (a.approval_round or 0) == current_round]
by_node_id: dict[str, PlVehicleRechargeApproval] = {}
for a in round_approvals:
if not a.node_id:
continue
existing = by_node_id.get(a.node_id)
if existing is None or (a.approval_time or datetime.min) >= (
existing.approval_time or datetime.min
):
by_node_id[a.node_id] = a
result: list[VehicleRechargeApprovalFlowItem] = []
for node in nodes:
approval = by_node_id.get(node.id)
if approval:
result.append(
_approval_flow_item_from_row(approval, node_order=node.node_order or 0)
)
else:
result.append(
VehicleRechargeApprovalFlowItem(
id="0",
applyId=row.id,
nodeName=node.node_name,
nodeOrder=node.node_order or 0,
approvalRound=current_round,
approvalStatus="pending",
approvalOpinion=None,
approverName=node.approver_name or "",
approvalTime=None,
)
)
return result
def _build_approval_history(
db: Session,
row: PlVehicleRechargeApply,
approval_rows: list[PlVehicleRechargeApproval],
) -> list[VehicleRechargeApprovalFlowItem]:
"""全部历史审批动作(含打回前各轮),按时间正序。"""
node_order_map: dict[str, int] = {}
if row.wf_id:
for node in _workflow_nodes(db, row.wf_id, row.tenant_no):
node_order_map[node.id] = node.node_order or 0
result: list[VehicleRechargeApprovalFlowItem] = []
for idx, a in enumerate(approval_rows):
order = node_order_map.get(a.node_id or "", 0) or (idx + 1)
result.append(_approval_flow_item_from_row(a, node_order=order))
return result
def _get_linked_record(
@@ -219,6 +317,8 @@ def apply_to_read(
include_detail: bool = False,
) -> VehicleRechargeApplyRead:
approvals: list[VehicleRechargeApprovalRead] = []
approval_flow: list[VehicleRechargeApprovalFlowItem] = []
approval_history: list[VehicleRechargeApprovalFlowItem] = []
record_read: Optional[VehicleRechargeRecordRead] = None
current_node_name: Optional[str] = None
current_node_action_type: Optional[str] = None
@@ -235,7 +335,10 @@ def apply_to_read(
current_node_name = node.node_name
current_node_action_type = (node.action_type or "approve").strip() or "approve"
if include_detail:
approvals = _list_approvals(db, row.tenant_no, row.id)
approval_rows = _list_approval_rows(db, row.tenant_no, row.id)
approvals = [approval_to_read(a) for a in approval_rows]
approval_flow = _build_approval_flow(db, row, approval_rows)
approval_history = _build_approval_history(db, row, approval_rows)
record_row = _get_linked_record(db, row.tenant_no, row.id)
if record_row:
record_read = record_to_read(record_row, db, apply_no=row.apply_no)
@@ -256,6 +359,8 @@ def apply_to_read(
currentNodeActionType=current_node_action_type,
approvalRound=row.approval_round or 0,
approvals=approvals,
approvalFlow=approval_flow,
approvalHistory=approval_history,
record=record_read,
remark=row.remark,
createTime=row.create_time,
@@ -343,7 +448,7 @@ def _resolve_apply_for_record(
if apply.status == "in_approval":
raise HTTPException(
status_code=400,
detail="审批中的申请请在「充值审批」中完成发放登记",
detail="审批中的申请请在「ETC/加油充值审批」中完成发放登记",
)
if apply.status != "pending":
raise HTTPException(status_code=400, detail="仅待发放申请可关联")
@@ -389,6 +494,7 @@ def list_applies(
max_page_size: Optional[int] = None,
sort_field: Optional[str] = None,
sort_order: Optional[str] = None,
mine_vehicles: bool = False,
) -> tuple[list[VehicleRechargeApplyRead], int]:
query = db.query(PlVehicleRechargeApply).filter(
PlVehicleRechargeApply.tenant_no == tenant_no,
@@ -424,7 +530,13 @@ def list_applies(
if ids:
query = query.filter(PlVehicleRechargeApply.id.in_(ids))
query = apply_pl_scope(query, PlVehicleRechargeApply, user, db, tenant_no)
if mine_vehicles:
plates = license_plates_for_user_personnel(db, tenant_no, user.id)
if not plates:
return [], 0
query = query.filter(PlVehicleRechargeApply.license_plate.in_(plates))
else:
query = apply_pl_scope(query, PlVehicleRechargeApply, user, db, tenant_no)
total = query.count()
page = max(1, page)
cap = int(max_page_size) if max_page_size else 500
@@ -439,9 +551,16 @@ def list_applies(
def get_apply(
db: Session, tenant_no: str, user: DbUserContext, apply_id: str
db: Session,
tenant_no: str,
user: DbUserContext,
apply_id: str,
*,
mine_vehicles: bool = False,
) -> VehicleRechargeApplyRead:
row = _get_apply_row(db, tenant_no, apply_id, user)
row = _get_apply_row(
db, tenant_no, apply_id, user, mine_vehicles=mine_vehicles
)
return apply_to_read(row, db, include_detail=True)
@@ -658,6 +777,7 @@ def board_item_to_read(
amount=record_row.amount if record_row else None,
recordCreateByName=_resolve_user_name(db, record_row.create_by) if record_row else None,
recordRemark=record_row.remark if record_row else None,
recordAttachmentUrls=record_row.attachment_urls if record_row else None,
)
@@ -96,3 +96,91 @@ def test_license_plates_empty_when_no_personnel():
db = _make_session()
db.commit()
assert license_plates_for_user_personnel(db, "t1", "u-nobody") == set()
def _seed_apply(db, *, apply_id, plate, applicant_id, status="draft"):
db.add(
PlVehicleRechargeApply(
id=apply_id,
apply_no=f"SXSQ{apply_id}",
license_plate=plate,
recharge_type="etc",
apply_date=date.today(),
applicant_id=applicant_id,
status=status,
tenant_no="t1",
create_by=applicant_id,
create_dept_id="d1",
create_time=datetime.now(),
update_time=datetime.now(),
del_flag=0,
)
)
def test_list_applies_mine_vehicles_includes_others_on_my_plate(monkeypatch):
from app.services import pl_vehicle_recharge_service as svc
db = _make_session()
_seed_person_vehicle(db)
_seed_apply(db, apply_id="a1", plate="鄂A11111", applicant_id="u-office")
_seed_apply(db, apply_id="a2", plate="鄂A99999", applicant_id="u-office")
db.commit()
monkeypatch.setattr(svc, "apply_pl_scope", lambda query, *_a, **_k: query)
monkeypatch.setattr(svc, "assert_pl_row_in_scope", lambda *_a, **_k: None)
monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
monkeypatch.setattr(
svc,
"apply_to_read",
lambda row, db, **kw: SimpleNamespace(
id=row.id, licensePlate=row.license_plate, applicantId=row.applicant_id
),
)
items, total = svc.list_applies(
db, "t1", _user(), mine_vehicles=True, page=1, page_size=20
)
assert total == 1
assert items[0].id == "a1"
def test_get_apply_mine_vehicles_allows_others_on_my_plate(monkeypatch):
from app.services import pl_vehicle_recharge_service as svc
db = _make_session()
_seed_person_vehicle(db)
_seed_apply(db, apply_id="a1", plate="鄂A11111", applicant_id="u-office")
db.commit()
monkeypatch.setattr(
svc,
"assert_pl_row_in_scope",
lambda *_a, **_k: (_ for _ in ()).throw(
AssertionError("should not use data scope when mine_vehicles")
),
)
monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
monkeypatch.setattr(
svc,
"apply_to_read",
lambda row, db, **kw: SimpleNamespace(id=row.id),
)
result = svc.get_apply(db, "t1", _user(), "a1", mine_vehicles=True)
assert result.id == "a1" or getattr(result, "id", None) == "a1"
def test_get_apply_mine_vehicles_rejects_foreign_plate(monkeypatch):
from fastapi import HTTPException
from app.services import pl_vehicle_recharge_service as svc
db = _make_session()
_seed_person_vehicle(db)
_seed_apply(db, apply_id="a2", plate="鄂A99999", applicant_id="u-office")
db.commit()
monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "X")
with pytest.raises(HTTPException) as exc:
svc.get_apply(db, "t1", _user(), "a2", mine_vehicles=True)
assert exc.value.status_code == 403
assert "无权" in exc.value.detail