refactor(repos): 归属留痕共享数据访问,人员在职校验收口单点

- ownership_log_repository:客户/业务两侧逐行同构的 list/count/add
  收口(owner_type 与引用列参数化),双键倒序与被删人员兜底口径
  单点维护;两侧 repository 变薄包装,service 层零改动
- PERSONNEL_STATUS_ACTIVE 单点导出于 models/personnel(对照
  BUSINESS_STATUS_* 先例);业务/客户两侧的「存在且在职」校验收口
  于 personnel_service.ensure_active——此前两份常量+校验仅靠注释
  维持同步,在职判定调整时会两入口口径分叉;校验放 service 层遵守
  「repositories 不抛业务异常」红线

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-15 09:49:53 +08:00
1 parent 409e26614f
commit 60f6af6494
7 files changed
+119 -85

No files matched your search

+4 -10
View File
@@ -49,7 +49,7 @@ from app.models.business import (
BUSINESS_STATUS_DONE,
)
from app.models.customer import OWNER_TYPE_BUSINESS
from app.repositories import business_repository, customer_repository, personnel_repository
from app.repositories import business_repository, customer_repository
from app.schemas.business import (
BusinessCreateIn,
BusinessDetailOut,
@@ -66,7 +66,7 @@ from app.schemas.business import (
GrossProfitRowOut,
OwnershipChangeLogOut,
)
from app.services import audit_service, param_service
from app.services import audit_service, param_service, personnel_service
from app.services.audit_service import AuditRecord, RequestMeta
from app.services.formula import converted_profit, profit_rate
from app.services.no_generator import next_no
@@ -77,9 +77,6 @@ logger = logging.getLogger(__name__)
# 重试是为极端场景(锁等待超时后重入)兜底 —— 无限重试反而掩盖死锁类缺陷
_NO_RETRY = 1
# personnel.status 的在职值(与 customer_service 同口径,注释见彼处)
_PERSONNEL_STATUS_ACTIVE = 1
def _to_out(row: tuple) -> BusinessOut:
"""(business, company_name, owner_name, assistant_name) → 响应模型。
@@ -214,11 +211,8 @@ def _generate_no(session: Session) -> str:
def _ensure_personnel_active(session: Session, personnel_id: int, label: str) -> Personnel:
"""归属人员须存在且在职(人员侧复用校验文案口径)。"""
person = personnel_repository.get_by_id(session, personnel_id)
if person is None or person.status != _PERSONNEL_STATUS_ACTIVE:
raise BadRequestError(f"{label}人员不存在或已离职")
return person
"""归属人员须存在且在职(共享校验收口于 personnel_service.ensure_active)。"""
return personnel_service.ensure_active(session, personnel_id, label=label)
def _ensure_customer(session: Session, customer_id: int) -> Customer:
+3 -16
View File
@@ -40,15 +40,11 @@ from app.schemas.customer import (
CustomerUpdateIn,
OwnershipChangeLogOut,
)
from app.services import audit_service
from app.services import audit_service, personnel_service
from app.services.audit_service import AuditRecord, RequestMeta
logger = logging.getLogger(__name__)
# personnel.status 的在职值(列注释口径:1在职 0离职)。离职人员不可再被
# 客户归属引用;人员侧抽出共享常量后此处替换
_PERSONNEL_STATUS_ACTIVE = 1
def _to_out(customer: Customer, owner_name: str | None) -> CustomerOut:
"""ORM 对象转响应模型。禁止把 ORM 对象直接当 response_model。"""
@@ -95,17 +91,8 @@ def _ensure_customer_type(customer_type: str) -> None:
def _ensure_owner_personnel(session: Session, personnel_id: int) -> Personnel:
"""归属人员必须存在且在职,返回人员行。
「不存在」与「已离职」合并为同一文案:人员 ID 是否有效不构成需要
向调用方区分的信息(同登录防枚举的思路)。
:raises BadRequestError: 归属人员不存在或已离职
"""
person = personnel_repository.get_by_id(session, personnel_id)
if person is None or person.status != _PERSONNEL_STATUS_ACTIVE:
raise BadRequestError("归属人员不存在或已离职")
return person
"""归属人员必须存在且在职(共享校验收口于 personnel_service.ensure_active)。"""
return personnel_service.ensure_active(session, personnel_id, label="归属")
def _is_duplicate_entry(exc: IntegrityError) -> bool:
+17
View File
@@ -30,6 +30,7 @@ from app.core.constants import (
from app.core.exceptions import BadRequestError, NotFoundError
from app.core.response import PageResult
from app.models import BusinessRole, Personnel, PersonnelPlan, PersonnelRegion, User
from app.models.personnel import PERSONNEL_STATUS_ACTIVE
from app.repositories import business_role_repository, personnel_repository
from app.schemas.personnel import (
PersonnelCreateIn,
@@ -51,6 +52,22 @@ logger = logging.getLogger(__name__)
_DELETE_BLOCKED = "存在团队成员、客户归属或业务归属引用,请改为离职而非删除"
def ensure_active(session: Session, personnel_id: int, *, label: str) -> Personnel:
"""归属人员必须存在且在职,返回人员行(业务/客户两侧归属校验共用)。
「不存在」与「已离职」合并为同一文案:人员 ID 是否有效不构成需要向
调用方区分的信息(同登录防枚举的思路);label 落文案(负责/协助/归属)。
收口单点:在职状态值取 models.personnel.PERSONNEL_STATUS_ACTIVE,
此前 business/customer 两 service 各持常量与校验,仅靠注释维持同步。
:raises BadRequestError: {label}人员不存在或已离职
"""
person = personnel_repository.get_by_id(session, personnel_id)
if person is None or person.status != PERSONNEL_STATUS_ACTIVE:
raise BadRequestError(f"{label}人员不存在或已离职")
return person
def list_personnel(session: Session, query: PersonnelQuery) -> PageResult[PersonnelOut]:
"""分页查询人员(含离职)。"""
rows = personnel_repository.list_personnel(