Files
zkxw/backend/app/services/oa_finance_remark.py
T
zlwu 0e64b413ce feat(oa): add permission-controlled shared finance remarks to done center
Share one finance remark per approval instance across Web and mobile. Require the explicit permission and the current tenant user's own done task for every read and write; keep general instance responses free of financial remarks.

Add idempotent MySQL migration without automatic role grants, cross-end editors, isolated backend and frontend tests, and rollout requirements. Refs #7; platform validation and target database migration remain pending.
2026-10-30 20:42:05 +08:00

101 lines
3.8 KiB
Python

"""已办中心实例级财务备注;权限与本人已办范围同时满足才可访问。"""
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List
from sqlalchemy.orm import Session
from app.models.models import OaInstance, OaTask, SysUser
from app.services.web_auth_roles import iter_user_menu_perm_codes
PERMISSION = "oa:done:finance-remark"
DONE_STATUSES = ("approved", "rejected", "returned", "transferred")
class OaFinanceRemarkError(ValueError):
def __init__(self, status_code: int, message: str):
super().__init__(message)
self.status_code = status_code
@dataclass(frozen=True)
class FinanceRemarkAccess:
user_id: str
tenant_no: str
can_manage: bool
def resolve_access(db: Session, user: dict) -> FinanceRemarkAccess:
# 不使用缺省租户、客户端租户或角色名称作为敏感字段的授权依据。
uid = str(user.get("user_id") or "").strip()
identity = db.query(SysUser.id, SysUser.tenant_no).filter(
SysUser.id == uid, SysUser.del_flag == 0, SysUser.status == "1"
).first()
if not identity or not str(identity.tenant_no or "").strip():
raise OaFinanceRemarkError(403, "用户不可用")
tenant = str(identity.tenant_no).strip()
granted = iter_user_menu_perm_codes(db, uid, tenant_no=tenant)
return FinanceRemarkAccess(uid, tenant, PERMISSION in granted)
def _accessible_instances(db: Session, access: FinanceRemarkAccess):
own_done = db.query(OaTask.id).filter(
OaTask.instance_id == OaInstance.id,
OaTask.tenant_no == access.tenant_no,
OaTask.assignee_user_id == access.user_id,
OaTask.task_type == "approval",
OaTask.status.in_(DONE_STATUSES),
OaTask.del_flag == 0,
).exists()
return db.query(OaInstance).filter(
OaInstance.tenant_no == access.tenant_no,
OaInstance.del_flag == 0,
own_done,
)
def _require_instance(db: Session, access: FinanceRemarkAccess, instance_id: str) -> OaInstance:
if not access.can_manage:
raise OaFinanceRemarkError(403, "无权查看或修改财务备注")
instance = _accessible_instances(db, access).filter(OaInstance.id == instance_id).first()
if instance is None:
raise OaFinanceRemarkError(404, "审批单不在本人已办范围内")
return instance
def _remark_data(instance: OaInstance) -> Dict[str, str]:
return {"instanceId": instance.id, "financeRemark": instance.finance_remark or ""}
def get_finance_remark(db: Session, access: FinanceRemarkAccess, instance_id: str) -> Dict[str, str]:
return _remark_data(_require_instance(db, access, instance_id))
def update_finance_remark(
db: Session, access: FinanceRemarkAccess, instance_id: str, finance_remark: str
) -> Dict[str, str]:
instance = _require_instance(db, access, instance_id)
if not isinstance(finance_remark, str) or len(finance_remark) > 512:
raise OaFinanceRemarkError(422, "财务备注最多 512 字")
instance.finance_remark = finance_remark
instance.update_by = access.user_id
instance.update_time = datetime.now()
db.flush()
return _remark_data(instance)
def add_list_remarks(db: Session, access: FinanceRemarkAccess, rows: List[Dict[str, Any]]) -> None:
# 即使调用方未来调整序列化,也先移除敏感字段,再按权限补充。
for row in rows:
row.pop("financeRemark", None)
if not access.can_manage:
return
ids = {row.get("instanceId") for row in rows if row.get("instanceId")}
if not ids:
return
instances = _accessible_instances(db, access).filter(OaInstance.id.in_(ids)).all()
remarks = {inst.id: inst.finance_remark or "" for inst in instances}
for row in rows:
if row.get("instanceId") in remarks:
row["financeRemark"] = remarks[row["instanceId"]]