按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
102 lines
5.4 KiB
Python
102 lines
5.4 KiB
Python
"""业务表单参照只返回必要字段,不借下拉权限开放整个资料接口。"""
|
|
from sqlalchemy import select
|
|
|
|
from app.core.exceptions import BadRequestError, ForbiddenError
|
|
from app.models import (
|
|
Business,
|
|
BusinessRole,
|
|
CalcTemplateDefinition,
|
|
Customer,
|
|
CustomerRatingGrade,
|
|
Personnel,
|
|
Product,
|
|
Shipment,
|
|
)
|
|
from app.services.authorization_service import calculate
|
|
|
|
OPTIONS = {
|
|
"managers": (Personnel, ("id", "personnel_name", "business_role_id", "status"), (
|
|
"base:personnel:view", "base:personnel:create", "base:personnel:update",
|
|
)),
|
|
"rating_grades": (CustomerRatingGrade, ("id", "grade_name", "min_score", "max_score", "sort_order", "remark"), (
|
|
"base:customers:view", "base:customers:rating", "business:ratings:view",
|
|
)),
|
|
"personnel": (Personnel, ("id", "personnel_name", "business_role_id", "status"), (
|
|
"base:personnel:view", "base:customers:view", "base:customers:create", "base:customers:update",
|
|
"base:customers:ownership", "business:businesses:view", "business:businesses:create",
|
|
"business:businesses:update", "business:businesses:ownership", "payroll:prepare", "adjustments:create",
|
|
"payroll:view", "reports:personal:view", "reports:department:view", "reports:business:view", "reports:payments:view",
|
|
)),
|
|
"customers": (Customer, ("id", "company_name", "customer_type"), (
|
|
"base:customers:view", "business:businesses:view", "business:businesses:create", "business:businesses:update",
|
|
"reports:business:view",
|
|
)),
|
|
"products": (Product, ("id", "product_name", "spec_model", "unit"), (
|
|
"base:products:view", "business:businesses:create", "business:businesses:update",
|
|
"business:shipments:create", "business:shipments:update",
|
|
)),
|
|
"businesses": (Business, ("id", "business_no", "status"), (
|
|
"business:businesses:view", "business:shipments:view", "business:shipments:create",
|
|
"business:shipments:update", "business:receipts:view", "business:receipts:create", "business:receipts:update",
|
|
)),
|
|
"shipments": (Shipment, ("id", "shipment_no", "business_id", "ship_date"), (
|
|
"business:shipments:view", "business:receipts:view", "business:receipts:create", "business:receipts:update",
|
|
)),
|
|
"business_roles": (BusinessRole, ("id", "role_code", "role_name", "status"), (
|
|
"base:personnel:view", "base:personnel:create", "base:personnel:update", "system:business_roles:view",
|
|
"system:params:view", "reports:personal:view", "reports:department:view", "reports:business:view", "reports:payments:view",
|
|
)),
|
|
}
|
|
|
|
|
|
def payroll_options(session, actor, business_id):
|
|
"""核算资料所需的人员及发货明细,不开放人员薪酬或联系方式。"""
|
|
from app.core.exceptions import NotFoundError
|
|
from app.models import ShipmentItem
|
|
from app.services.authorization_service import require_permission
|
|
|
|
require_permission(session, actor, "payroll:view")
|
|
if session.get(Business, business_id) is None:
|
|
raise NotFoundError("业务不存在")
|
|
heads = list(session.scalars(select(Shipment).where(Shipment.business_id == business_id).order_by(Shipment.id).limit(201)))
|
|
if len(heads) > 200:
|
|
raise BadRequestError("单笔业务超过 200 张发货单,请分批核对资料")
|
|
people = [dict(row) for row in session.execute(select(Personnel.id, Personnel.personnel_name).order_by(Personnel.id).limit(201)).mappings()]
|
|
if len(people) > 200:
|
|
raise BadRequestError("核算人员超过单次范围,请按实际归属分批处理")
|
|
lines = session.execute(select(ShipmentItem.id, ShipmentItem.shipment_id, ShipmentItem.product_name, ShipmentItem.amount).where(ShipmentItem.shipment_id.in_([h.id for h in heads]))).mappings()
|
|
by_head = {head.id: head for head in heads}
|
|
return {"personnel": people, "shipments": [
|
|
{"id": row["id"], "shipment_no": by_head[row["shipment_id"]].shipment_no,
|
|
"ship_date": str(by_head[row["shipment_id"]].ship_date), "product_name": row["product_name"], "amount": str(row["amount"])}
|
|
for row in lines
|
|
]}
|
|
|
|
|
|
def options(session, actor, resource, *, business_id=None, search=None):
|
|
definition = OPTIONS.get(resource)
|
|
if definition is None:
|
|
raise BadRequestError("不支持的参照类型")
|
|
model, fields, callers = definition
|
|
if not set(calculate(session, actor)["permissions"]) & set(callers):
|
|
raise ForbiddenError("没有使用此参照数据的业务权限")
|
|
statement = select(*(getattr(model, f) for f in fields))
|
|
if resource == "managers":
|
|
statement = (
|
|
statement.join(BusinessRole, BusinessRole.id == Personnel.business_role_id)
|
|
.join(CalcTemplateDefinition, CalcTemplateDefinition.template_code == BusinessRole.calc_template)
|
|
.where(
|
|
Personnel.status == 1, BusinessRole.status == 1,
|
|
CalcTemplateDefinition.formula_type == "manager_share",
|
|
)
|
|
)
|
|
if business_id is not None and model == Shipment:
|
|
statement = statement.where(Shipment.business_id == business_id)
|
|
if search:
|
|
name = getattr(model, fields[1])
|
|
escaped = search.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
statement = statement.where(name.like(f"%{escaped}%", escape="\\"))
|
|
order = (model.sort_order, model.id) if model == CustomerRatingGrade else (model.id,)
|
|
rows = session.execute(statement.order_by(*order).limit(200)).mappings()
|
|
return [dict(row) for row in rows]
|