dy-dashboard/services/field_perms.py
2026-10-29 10:36:31 +08:00

721 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""字段级权限:字段清单单一来源 + 权限解析 + 后端强制过滤。
设计目标(对应「字段级权限」优化):
1. 字段清单单一来源:FIELD_REGISTRY 定义每个页面的字段(key=中文显示名,与前端列定义、
现有 user_permissions 存储保持一致),sensitive 标记敏感字段。
2. 权限存储复用现有表结构,无需迁移:
- 角色级字段权限:role_operation_permissions,operation = 'field:<字段>:view' / 'field:<字段>:edit'
- 用户级字段权限:user_permissions,operation 同上
3. 敏感字段默认隐藏:敏感字段未显式授权时,默认拒绝查看/编辑(安全优先)。
4. 后端强制过滤:filter_rows / filter_row 按有效字段权限剔除无权查看的字段。
权限解析优先级:管理员全权限 → 用户级明确值 → 角色级明确值 → 敏感字段默认拒绝 / 非敏感默认继承页面权限。
"""
from services.db import query
from services.permission_model import ensure_position_permissions, user_position_code
# ─── 字段清单单一来源 ───
# key 统一使用中文显示名(与前端列定义一致),sensitive=True 表示敏感字段(默认隐藏,需显式授权)。
FIELD_REGISTRY = {
# 计划预测(与 PLAN_COLUMNS 对齐)
'planning': [
{'key': '产品类型', 'sensitive': False},
{'key': '产品种类', 'sensitive': False},
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '批注', 'sensitive': False},
{'key': '新旧品', 'sensitive': False},
{'key': '下单清单', 'sensitive': False},
{'key': '产品分级', 'sensitive': False},
{'key': '在途进度', 'sensitive': False},
{'key': '昨日销量', 'sensitive': False},
{'key': '云仓昨日销量', 'sensitive': False},
{'key': '外仓昨日销量', 'sensitive': False},
{'key': '销售趋势', 'sensitive': False},
{'key': '总库存', 'sensitive': False},
{'key': '库存', 'sensitive': False},
{'key': '总在途库存', 'sensitive': False},
{'key': '云仓库存', 'sensitive': False},
{'key': '外仓库存', 'sensitive': False},
{'key': '加工厂库存', 'sensitive': False},
{'key': '云仓可分配库存', 'sensitive': False},
{'key': '外仓可分配库存', 'sensitive': False},
{'key': '是否调拨', 'sensitive': False},
{'key': '生产在途', 'sensitive': False},
{'key': '采购在途', 'sensitive': False},
{'key': '日销量预测', 'sensitive': False},
{'key': '云仓日销量预测', 'sensitive': False},
{'key': '外仓日销量预测', 'sensitive': False},
{'key': '建议下单数量', 'sensitive': False},
{'key': '连续建议天数', 'sensitive': False},
{'key': '实际下单数量', 'sensitive': False},
{'key': '正式下单数量', 'sensitive': False},
{'key': '库存健康指数', 'sensitive': False},
{'key': '风险把控', 'sensitive': False},
{'key': '供应商名称', 'sensitive': False},
{'key': '采购跟单', 'sensitive': False},
{'key': '采购计划', 'sensitive': False},
{'key': '开发专员', 'sensitive': False},
{'key': '归属平台', 'sensitive': False},
{'key': '运营专员', 'sensitive': False},
{'key': '成本价', 'sensitive': True},
{'key': '3天平均销量', 'sensitive': False},
{'key': '云仓3天平均销量', 'sensitive': False},
{'key': '外仓3天平均销量', 'sensitive': False},
{'key': '7天平均销量', 'sensitive': False},
{'key': '云仓7天平均销量', 'sensitive': False},
{'key': '外仓7天平均销量', 'sensitive': False},
{'key': '14天平均销量', 'sensitive': False},
{'key': '云仓14天平均销量', 'sensitive': False},
{'key': '外仓14天平均销量', 'sensitive': False},
{'key': '库存可支撑天数', 'sensitive': False},
{'key': '现货采购在途', 'sensitive': False},
{'key': '加工厂库存支撑', 'sensitive': False},
{'key': '定制品在途', 'sensitive': False},
{'key': '总支撑天数', 'sensitive': False},
{'key': '云仓支撑天数', 'sensitive': False},
{'key': '外仓支撑天数', 'sensitive': False},
{'key': '建议回货数量', 'sensitive': False},
{'key': '产品规格', 'sensitive': False},
{'key': '30天平均销量', 'sensitive': False},
{'key': '60天平均销量', 'sensitive': False},
{'key': '生产货期', 'sensitive': False},
{'key': '物流时间', 'sensitive': False},
{'key': '最低起订量', 'sensitive': False},
{'key': '创建时间', 'sensitive': False},
{'key': '是否新品', 'sensitive': False},
{'key': '一级类目', 'sensitive': False},
{'key': '二级类目', 'sensitive': False},
{'key': '三级类目', 'sensitive': False},
],
# 数据概览
'data_summary': [
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '店铺', 'sensitive': False},
{'key': '总数量', 'sensitive': False},
{'key': '总成本', 'sensitive': True},
{'key': '日期', 'sensitive': False},
{'key': '数据来源', 'sensitive': False},
],
# 数据明细
'data_detail': [
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '店铺', 'sensitive': False},
{'key': '一级类目', 'sensitive': False},
{'key': '二级类目', 'sensitive': False},
{'key': '三级类目', 'sensitive': False},
{'key': '货品数量', 'sensitive': False},
{'key': '货品成本', 'sensitive': True},
{'key': '日期', 'sensitive': False},
{'key': '数据来源', 'sensitive': False},
],
# 库存-成品
'inventory_finished': [
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '可用库存', 'sensitive': False},
{'key': '生产在途', 'sensitive': False},
{'key': '采购在途', 'sensitive': False},
{'key': '供应商', 'sensitive': False},
{'key': '库存日期', 'sensitive': False},
{'key': '采购跟单', 'sensitive': False},
{'key': '采购计划', 'sensitive': False},
],
# 库存-包材
'inventory_packaging': [
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '可用库存', 'sensitive': False},
{'key': '生产在途', 'sensitive': False},
{'key': '采购在途', 'sensitive': False},
{'key': '供应商', 'sensitive': False},
{'key': '库存日期', 'sensitive': False},
],
# 采购待下单
'purchase_pending': [
{'key': '产品类型', 'sensitive': False},
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '确认下单', 'sensitive': False},
{'key': '产品分级', 'sensitive': False},
{'key': '供应商名称', 'sensitive': False},
{'key': '建议下单数量', 'sensitive': False},
{'key': '正式下单数量', 'sensitive': False},
{'key': '下单备注', 'sensitive': False},
{'key': '货期', 'sensitive': False},
{'key': '物流', 'sensitive': False},
{'key': '总支撑天数', 'sensitive': False},
{'key': '平台', 'sensitive': False},
{'key': '运营', 'sensitive': False},
{'key': '审核价格', 'sensitive': True},
{'key': '下单人员', 'sensitive': False},
{'key': '下单时间', 'sensitive': False},
{'key': '采购跟单', 'sensitive': False},
{'key': '采购计划', 'sensitive': False},
{'key': '加入时间', 'sensitive': False},
],
# 采购已下单
'purchase_backup': [
{'key': '产品类型', 'sensitive': False},
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '下单时间', 'sensitive': False},
{'key': '下单人', 'sensitive': False},
{'key': '生产单号', 'sensitive': False},
{'key': '建议下单数量', 'sensitive': False},
{'key': '正式下单数量', 'sensitive': False},
{'key': '产品分级', 'sensitive': False},
{'key': '供应商名称', 'sensitive': False},
{'key': '下单备注', 'sensitive': False},
{'key': '货期', 'sensitive': False},
{'key': '物流', 'sensitive': False},
{'key': '总支撑天数', 'sensitive': False},
{'key': '采购跟单', 'sensitive': False},
{'key': '采购计划', 'sensitive': False},
{'key': '平台', 'sensitive': False},
{'key': '运营', 'sensitive': False},
{'key': '审核价格', 'sensitive': True},
],
# 国内订单
'order_domestic': [
{'key': '日期', 'sensitive': False},
{'key': '计划下单时间', 'sensitive': False},
{'key': '新旧品', 'sensitive': False},
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '货品类型', 'sensitive': False},
{'key': '下单备注', 'sensitive': False},
{'key': '单价', 'sensitive': True},
{'key': '审核价格', 'sensitive': True},
{'key': '数量', 'sensitive': False},
{'key': '金额', 'sensitive': True},
{'key': '供货商', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '金蝶采购单', 'sensitive': False},
{'key': '采购员', 'sensitive': False},
{'key': '下单人', 'sensitive': False},
{'key': '进度', 'sensitive': False},
{'key': '旺店通单号', 'sensitive': False},
{'key': '采购开发', 'sensitive': False},
{'key': '预计到货日期', 'sensitive': False},
{'key': '金蝶入库', 'sensitive': False},
],
# 海外订单
'order_overseas': [
{'key': '产品编码', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '货品类型', 'sensitive': False},
{'key': '日期', 'sensitive': False},
{'key': '计划下单时间', 'sensitive': False},
{'key': '单价', 'sensitive': True},
{'key': '审核价格', 'sensitive': True},
{'key': '数量', 'sensitive': False},
{'key': '金额', 'sensitive': True},
{'key': '供应商', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '新旧品', 'sensitive': False},
{'key': '下单备注', 'sensitive': False},
{'key': '采购员', 'sensitive': False},
{'key': '下单人', 'sensitive': False},
{'key': '进度', 'sensitive': False},
{'key': '旺店通单号', 'sensitive': False},
{'key': '采购开发', 'sensitive': False},
{'key': '预计到货日期', 'sensitive': False},
],
# 包材订单
'order_packaging': [
{'key': '日期', 'sensitive': False},
{'key': '计划下单时间', 'sensitive': False},
{'key': '产品编码', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '货品类型', 'sensitive': False},
{'key': '单价', 'sensitive': True},
{'key': '审核价格', 'sensitive': True},
{'key': '数量', 'sensitive': False},
{'key': '金额', 'sensitive': True},
{'key': '供应商', 'sensitive': False},
{'key': '成品供应商', 'sensitive': False},
{'key': '仓库', 'sensitive': False},
{'key': '新旧品', 'sensitive': False},
{'key': '采购员', 'sensitive': False},
{'key': '下单人', 'sensitive': False},
{'key': '进度', 'sensitive': False},
{'key': '下单备注', 'sensitive': False},
{'key': '采购开发', 'sensitive': False},
{'key': '旺店通单号', 'sensitive': False},
],
# 成品订单
'order_finished': [
{'key': '品类', 'sensitive': False},
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '货品类型', 'sensitive': False},
{'key': '供应商', 'sensitive': False},
{'key': '可用库存', 'sensitive': False},
{'key': '千羽仓库存', 'sensitive': False},
{'key': '昨日销量', 'sensitive': False},
{'key': '3天均销', 'sensitive': False},
{'key': '7天均销', 'sensitive': False},
{'key': '15天均销', 'sensitive': False},
{'key': '支撑天数', 'sensitive': False},
{'key': '采购总数量', 'sensitive': False},
{'key': '生产在途', 'sensitive': False},
{'key': '采购在途', 'sensitive': False},
{'key': '产品进度', 'sensitive': False},
{'key': '加工厂库存', 'sensitive': False},
{'key': '跟单小组', 'sensitive': False},
{'key': '采购计划', 'sensitive': False},
{'key': '备注', 'sensitive': False},
],
# 产品基础信息
'products': [
{'key': '货品编号', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '货品类型', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '供应商编码', 'sensitive': False},
{'key': '供应商名称', 'sensitive': False},
{'key': '成本价', 'sensitive': True},
{'key': '货期', 'sensitive': False},
{'key': '物流时间', 'sensitive': False},
{'key': '最低起订量', 'sensitive': False},
{'key': '采购跟单', 'sensitive': False},
{'key': '采购计划', 'sensitive': False},
{'key': '平台', 'sensitive': False},
{'key': '运营', 'sensitive': False},
{'key': '开发专员', 'sensitive': False},
{'key': '创建时间', 'sensitive': False},
{'key': '导入日期', 'sensitive': False},
],
# 新品管理
'product_new': [
{'key': '产品编码', 'sensitive': False},
{'key': '产品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '条形码', 'sensitive': False},
{'key': '容量', 'sensitive': False},
{'key': '成品备注', 'sensitive': False},
{'key': '产品总成本', 'sensitive': True},
{'key': '产品总货款', 'sensitive': True},
{'key': '是否含运', 'sensitive': False},
{'key': '产品开发', 'sensitive': False},
{'key': '包材开发', 'sensitive': False},
{'key': '设计师', 'sensitive': False},
{'key': '负责运营', 'sensitive': False},
],
# 新品包材管理(独立于新品成品管理;字段来自 new_product_bom)
'product_packaging': [
{'key': '成品编码', 'sensitive': False},
{'key': '成品名称', 'sensitive': False},
{'key': '物料编码', 'sensitive': False},
{'key': '物料名称', 'sensitive': False},
{'key': '用量', 'sensitive': False},
{'key': '供应商编码', 'sensitive': False},
{'key': '供应商', 'sensitive': False},
{'key': '货期/天', 'sensitive': False},
{'key': '税点', 'sensitive': True},
{'key': '是否含税', 'sensitive': False},
{'key': '单价/元', 'sensitive': True},
{'key': '数量', 'sensitive': False},
{'key': '货款/元', 'sensitive': True},
{'key': '结款方式', 'sensitive': False},
{'key': '是否可下单', 'sensitive': False},
{'key': '下单备注', 'sensitive': False},
{'key': '通用物料编码', 'sensitive': False},
{'key': '通用物料名称', 'sensitive': False},
{'key': '下单日期', 'sensitive': False},
],
# BOM
'bom': [
{'key': '成品编号', 'sensitive': False},
{'key': '成品名称', 'sensitive': False},
{'key': '物料编号', 'sensitive': False},
{'key': '物料名称', 'sensitive': False},
{'key': '用量', 'sensitive': False},
{'key': '供应商', 'sensitive': False},
{'key': '成本价', 'sensitive': True},
{'key': '创建时间', 'sensitive': False},
],
# 仓库
'warehouse': [
{'key': '仓库编号', 'sensitive': False},
{'key': '仓库名称', 'sensitive': False},
{'key': '仓库类型', 'sensitive': False},
{'key': '地址', 'sensitive': False},
{'key': '联系人', 'sensitive': False},
{'key': '联系电话', 'sensitive': False},
{'key': '创建时间', 'sensitive': False},
],
# 供应商
'suppliers': [
{'key': '供应商编码', 'sensitive': False},
{'key': '供应商名称', 'sensitive': False},
{'key': '联系人', 'sensitive': False},
{'key': '联系电话', 'sensitive': False},
{'key': '地址', 'sensitive': False},
{'key': '合作状态', 'sensitive': False},
{'key': '创建时间', 'sensitive': False},
],
# 现货销售
'spot_sales': [
{'key': '商家编码', 'sensitive': False},
{'key': '货品名称', 'sensitive': False},
{'key': '品牌', 'sensitive': False},
{'key': '首批入库时间', 'sensitive': False},
{'key': '首批成本价', 'sensitive': True},
],
# AI 分析:金额类数据源单独控制(敏感,默认不开放)
'ai_analysis': [
{'key': '库存成本汇总', 'sensitive': True},
{'key': '商品库存金额', 'sensitive': True},
{'key': '品牌汇总', 'sensitive': True},
],
}
# 运行页面ID → 标准权限页面ID 回退映射
_FIELD_REGISTRY_ALIASES = {
'purchase': 'purchase_pending',
'purchase_ready': 'purchase_backup',
'orders_domestic': 'order_domestic',
'orders_overseas': 'order_overseas',
'orders_packaging': 'order_packaging',
'orders_finished': 'order_finished',
}
# 标准页面ID → 存储/历史别名集合(与 routes/auth.py 的 PERMISSION_PAGE_ALIASES 保持一致)
_PAGE_ALIASES = {
'purchase_pending': ['purchase_pending', 'purchase'],
'purchase_backup': ['purchase_backup', 'purchase_ready', 'purchase'],
'order_domestic': ['order_domestic', 'orders_domestic', 'orders'],
'order_overseas': ['order_overseas', 'orders_overseas', 'orders'],
'order_packaging': ['order_packaging', 'orders_packaging', 'orders'],
'order_finished': ['order_finished', 'orders_finished', 'orders'],
'data_summary': ['data_summary', 'data'],
'data_detail': ['data_detail', 'data'],
'inventory_finished': ['inventory_finished', 'inventory'],
'inventory_packaging': ['inventory_packaging', 'inventory'],
'product_new': ['product_new'],
'product_packaging': ['product_packaging'],
'spot_sales': ['spot_sales', 'spot'],
}
# 标准页面ID → 前端运行页面ID(注入前端时使用)
_RUNTIME_PAGE = {
'purchase_pending': 'purchase',
'purchase_backup': 'purchase_ready',
'order_domestic': 'orders_domestic',
'order_overseas': 'orders_overseas',
'order_packaging': 'orders_packaging',
'order_finished': 'orders_finished',
}
# 敏感字段的响应 key 映射(后端过滤时按实际返回字段名剔除)。
# 未列出的字段默认按 key(中文显示名)匹配响应字段。
FIELD_RESP_KEYS = {
# 采购待下单/已下单接口使用英文键名,统一映射到权限字段中文名。
('purchase_pending', '产品类型'): ['product_type'],
('purchase_pending', '货品编号'): ['product_code'],
('purchase_pending', '货品名称'): ['product_name'],
('purchase_pending', '产品分级'): ['product_grade'],
('purchase_pending', '供应商名称'): ['supplier_name'],
('purchase_pending', '建议下单数量'): ['suggested_qty'],
('purchase_pending', '正式下单数量'): ['formal_order_qty'],
('purchase_pending', '下单备注'): ['remark'],
('purchase_pending', '货期'): ['lead_time'],
('purchase_pending', '物流'): ['logistics_time'],
('purchase_pending', '总支撑天数'): ['total_support_days'],
('purchase_pending', '平台'): ['platform'],
('purchase_pending', '运营'): ['operator'],
('purchase_pending', '审核价格'): ['audit_price'],
('purchase_pending', '下单人员'): ['created_by'],
('purchase_pending', '下单时间'): ['created_at'],
('purchase_pending', '采购跟单'): ['procurement_follower'],
('purchase_pending', '采购计划'): ['procurement_planner'],
('purchase_pending', '加入时间'): ['created_at'],
('purchase_backup', '产品类型'): ['product_type'],
('purchase_backup', '货品编号'): ['product_code'],
('purchase_backup', '货品名称'): ['product_name'],
('purchase_backup', '供应商名称'): ['supplier_name'],
('purchase_backup', '建议下单数量'): ['suggested_qty'],
('purchase_backup', '正式下单数量'): ['formal_order_qty'],
('purchase_backup', '下单备注'): ['remark'],
('purchase_backup', '货期'): ['lead_time'],
('purchase_backup', '物流'): ['logistics_time'],
('purchase_backup', '总支撑天数'): ['total_support_days'],
('purchase_backup', '平台'): ['platform'],
('purchase_backup', '运营'): ['operator'],
('purchase_backup', '审核价格'): ['audit_price'],
('purchase_backup', '下单人员'): ['created_by'],
('purchase_backup', '下单时间'): ['ordered_at'],
('purchase_backup', '采购跟单'): ['procurement_follower'],
('purchase_backup', '采购计划'): ['procurement_planner'],
('purchase_backup', '生产单号'): ['production_order_no'],
# 包材订单接口字段与权限字段名称不同。
('order_packaging', '产品编码'): ['编码'],
('order_packaging', '货品名称'): ['名称'],
('order_packaging', '金额'): ['合计'],
('order_packaging', '供应商'): ['供应商'],
('order_packaging', '旺店通单号'): ['旺店通编号'],
('product_packaging', '成品编码'): ['finished_product_code'],
('product_packaging', '成品名称'): ['finished_product_name'],
('product_packaging', '物料编码'): ['material_code'],
('product_packaging', '物料名称'): ['material_name'],
('product_packaging', '供应商编码'): ['supplier_code'],
('product_packaging', '供应商'): ['supplier'],
('product_packaging', '货期/天'): ['lead_time'],
('product_packaging', '税点'): ['tax_rate'],
('product_packaging', '是否含税'): ['tax_included'],
('product_packaging', '单价/元'): ['unit_price'],
('product_packaging', '数量'): ['quantity'],
('product_packaging', '货款/元'): ['amount'],
('product_packaging', '结款方式'): ['payment_method'],
('product_packaging', '是否可下单'): ['can_order'],
('product_packaging', '下单备注'): ['remark'],
('product_packaging', '通用物料编码'): ['common_code'],
('product_packaging', '通用物料名称'): ['common_name'],
('product_packaging', '下单日期'): ['order_date'],
('planning', '成本价'): ['cost_price'],
('planning', '新旧品'): ['planning_new_old'],
('data_summary', '总成本'): ['total_cost'],
('products', '成本价'): ['cost_price'],
('product_new', '产品总成本'): ['total_cost'],
('product_new', '产品总货款'): ['total_payment'],
('product_new', '是否含运'): ['freight_included'],
('bom', '成本价'): ['cost_price'],
}
def _field_resp_keys(page, key):
"""返回某字段应剔除的响应 key 列表(含 key 本身)。"""
canonical = _canonical_page(page)
resp = FIELD_RESP_KEYS.get((canonical, key), [])
keys = [key] + [r for r in resp if r != key]
return keys
def _canonical_page(page):
return _FIELD_REGISTRY_ALIASES.get(page, page)
def _page_aliases(page):
canonical = _canonical_page(page)
return _PAGE_ALIASES.get(canonical, [canonical])
def _runtime_page(page):
canonical = _canonical_page(page)
return _RUNTIME_PAGE.get(canonical, canonical)
def get_field_list(page):
"""返回页面字段清单(list of {key, sensitive}),未注册的页面返回空列表。"""
return FIELD_REGISTRY.get(_canonical_page(page), [])
def is_admin_role(role):
return role in ('admin', 'super_admin')
def _load_field_config(user_id, role, page):
"""读取角色、岗位、用户三级字段权限配置。"""
aliases = _page_aliases(page)
placeholders = ','.join(['%s'] * len(aliases))
role_perm, position_perm, user_perm = {}, {}, {}
explicit = set()
role_rows = query(
f"SELECT operation, permission FROM role_operation_permissions "
f"WHERE role=%s AND page IN ({placeholders}) AND operation LIKE 'field:%%'",
tuple(['viewer' if role == '查看者' else role] + aliases)
)
for row in role_rows:
parts = row['operation'].split(':', 2)
if len(parts) == 3:
role_perm.setdefault(parts[1], {})[parts[2]] = int(row['permission'])
explicit.add(parts[1])
ensure_position_permissions()
position = user_position_code(user_id)
if position:
position_rows = query(
f"SELECT operation, permission FROM position_operation_permissions "
f"WHERE position_code=%s AND page IN ({placeholders}) AND operation LIKE 'field:%%'",
tuple([position] + aliases)
)
for row in position_rows:
parts = row['operation'].split(':', 2)
if len(parts) == 3:
position_perm.setdefault(parts[1], {})[parts[2]] = int(row['permission'])
explicit.add(parts[1])
user_rows = query(
f"SELECT operation, permission FROM user_permissions "
f"WHERE user_id=%s AND page IN ({placeholders}) AND operation LIKE 'field:%%'",
tuple([user_id] + aliases)
)
for row in user_rows:
parts = row['operation'].split(':', 2)
if len(parts) == 3:
user_perm.setdefault(parts[1], {})[parts[2]] = int(row['permission'])
explicit.add(parts[1])
return role_perm, position_perm, user_perm, explicit
def _resolve_field_value(user_perm, position_perm, role_perm, field_key, operation, default_allow):
"""字段权限优先级:用户例外 > 岗位权限 > 角色权限 > 页面默认。"""
user_value = user_perm.get(field_key, {}).get(operation)
if user_value is not None:
return bool(user_value)
position_value = position_perm.get(field_key, {}).get(operation)
if position_value is not None:
return bool(position_value)
role_value = role_perm.get(field_key, {}).get(operation)
if role_value is not None:
return bool(role_value)
return bool(default_allow)
def field_perms_for_page(user_id, role, page, page_view=True, page_edit=False):
"""返回合并角色、岗位、用户后的字段权限。"""
fields = get_field_list(page)
if not fields:
return {}
if is_admin_role(role):
return {field['key']: {'view': True, 'edit': True} for field in fields}
role_perm, position_perm, user_perm, _ = _load_field_config(user_id, role, page)
result = {}
for field in fields:
key = field['key']
result[key] = {
'view': _resolve_field_value(user_perm, position_perm, role_perm, key, 'view', page_view),
'edit': _resolve_field_value(user_perm, position_perm, role_perm, key, 'edit', page_edit),
}
return result
def role_field_perms_for_page(role, page):
"""仅角色级字段权限,用于角色权限矩阵。"""
fields = get_field_list(page)
if not fields:
return {}
if is_admin_role(role):
return {field['key']: {'view': True, 'edit': True} for field in fields}
aliases = _page_aliases(page)
placeholders = ','.join(['%s'] * len(aliases))
role_perm = {}
rows = query(
f"SELECT operation, permission FROM role_operation_permissions "
f"WHERE role=%s AND page IN ({placeholders}) AND operation LIKE 'field:%%'",
tuple(['viewer' if role == '查看者' else role] + aliases)
)
for row in rows:
parts = row['operation'].split(':', 2)
if len(parts) == 3:
role_perm.setdefault(parts[1], {})[parts[2]] = int(row['permission'])
return {
field['key']: {
'view': _resolve_field_value({}, {}, role_perm, field['key'], 'view', True),
'edit': _resolve_field_value({}, {}, role_perm, field['key'], 'edit', False),
}
for field in fields
}
def inherited_field_perms_for_page(user_id, role, page):
"""角色+岗位的字段继承值,不包含用户个人覆盖。"""
fields = get_field_list(page)
if not fields:
return {}
if is_admin_role(role):
return {field['key']: {'view': True, 'edit': True} for field in fields}
role_perm, position_perm, _, _ = _load_field_config(user_id, role, page)
return {
field['key']: {
'view': _resolve_field_value({}, position_perm, role_perm, field['key'], 'view', True),
'edit': _resolve_field_value({}, position_perm, role_perm, field['key'], 'edit', False),
}
for field in fields
}
def filter_row(page, row, user_id, role, page_view=True, page_edit=False):
"""对单个 dict 按字段 view 权限剔除无权查看的字段(原地修改并返回)。"""
if row is None or is_admin_role(role):
return row
perms = field_perms_for_page(user_id, role, page, page_view, page_edit)
if not perms:
return row
for key in list(row.keys()):
# 响应字段可能用中文显示名,也可能用英文 DB 列名;统一按映射剔除
matched = None
for fkey, fperm in perms.items():
if key in _field_resp_keys(page, fkey):
matched = (fkey, fperm)
break
if matched and not matched[1]['view']:
del row[key]
return row
def filter_rows(page, rows, user_id, role, page_view=True, page_edit=False):
"""对 dict 列表按字段 view 权限剔除字段(原地修改并返回)。"""
if not rows or is_admin_role(role):
return rows
perms = field_perms_for_page(user_id, role, page, page_view, page_edit)
if not perms:
return rows
# 预先构建 响应key -> 是否可查看 的映射,避免逐行重复计算
resp_visibility = {}
for fkey, fperm in perms.items():
for rkey in _field_resp_keys(page, fkey):
resp_visibility[rkey] = fperm['view']
for row in rows:
if not isinstance(row, dict):
continue
for key in list(row.keys()):
if key in resp_visibility and not resp_visibility[key]:
del row[key]
return rows
def effective_field_overrides(user_id, role, page_view_by_page=None, page_edit_by_page=None):
"""返回需要注入前端的字段级权限显式值(用于敏感字段默认隐藏 + 显式配置)。
仅返回「敏感字段」和「有显式配置」的字段;其余字段由前端按页面权限默认继承。
返回结构:{runtime_page: {field_key: {'view': 0/1, 'edit': 0/1}}}。
"""
if is_admin_role(role):
return {}
page_view_by_page = page_view_by_page or {}
page_edit_by_page = page_edit_by_page or {}
overrides = {}
for page, fields in FIELD_REGISTRY.items():
role_perm, position_perm, user_perm, explicit = _load_field_config(user_id, role, page)
pv = page_view_by_page.get(page, True)
pe = page_edit_by_page.get(page, False)
page_overrides = {}
for f in fields:
key = f['key']
sensitive = bool(f.get('sensitive'))
if key not in explicit:
continue
page_overrides[key] = {
'view': 1 if _resolve_field_value(user_perm, position_perm, role_perm, key, 'view', pv) else 0,
'edit': 1 if _resolve_field_value(user_perm, position_perm, role_perm, key, 'edit', pe) else 0,
}
if page_overrides:
overrides[_runtime_page(page)] = page_overrides
return overrides