feat: 全模块公司隔离 + crossDomain权限码动态跨域控制

- get_current_company_filter: 新增_has_cross_domain_permission, 权限码替代硬编码
- 补全11个Service的get_current_company_filter调用(semi/product/service/outbound/bom/trans/scrap/summary)
- base/search修复: search_material此前无隔离, 已补全
- get_current_company_filter兜底: JWT缺company_name时返回__NO_COMPANY__防止放行
- permission.py: _get_operator_company补全返回值, 修复权限页保存逻辑
- 新增crossDomain迁移脚本, element_type=element挂system_mgmt下
This commit is contained in:
yueli
2026-07-15 11:11:40 +08:00
parent 56edb92d80
commit e1417d740a
14 changed files with 186 additions and 14 deletions

View File

@ -4,6 +4,7 @@ from app.models.base import MaterialBase
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
from app.utils.decorators import get_current_company_filter
from sqlalchemy import func, distinct, or_, case
from collections import defaultdict
import uuid
@ -126,6 +127,11 @@ class BomService:
if active_only:
query_base = query_base.filter(BomTable.is_enabled == True)
# 【行级数据隔离】基于 JWT 多租户公司过滤
company_limit = get_current_company_filter()
if company_limit is not None:
query_base = query_base.filter(MaterialBase.company_name == company_limit)
if keyword:
kw = f'%{keyword}%'
# 关联子件表以支持子件搜索

View File

@ -48,6 +48,12 @@ class MaterialBaseService:
)
)
# 【行级数据隔离】基于 JWT 多租户公司过滤
from app.utils.decorators import get_current_company_filter
company_limit = get_current_company_filter()
if company_limit is not None:
query = query.filter(MaterialBase.company_name == company_limit)
# [修改1] 增加返回数量限制
# 原为 limit(20),现改为 1000,确保前端能获取所有(或足够多)的数据
query = query.limit(1000)

View File

@ -15,7 +15,7 @@ from openpyxl.utils import get_column_letter
class InboundSummaryService:
@staticmethod
def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None, source_type=None):
def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None, source_type=None, company=None):
"""
聚合查询:
1. 联合 StockBuy, StockSemi, StockProduct 三张表
@ -126,6 +126,10 @@ class InboundSummaryService:
if source_type:
query = query.filter(cte.c.source_type == source_type)
# 公司过滤(跨域选择器传入)
if company:
query = query.filter(MaterialBase.company_name == company)
# =========================================================
# 5. 获取总数
# =========================================================
@ -139,6 +143,8 @@ class InboundSummaryService:
count_query = count_query.filter(cte.c.inbound_date.between(start_date, end_date))
if source_type:
count_query = count_query.filter(cte.c.source_type == source_type)
if company:
count_query = count_query.filter(MaterialBase.company_name == company)
total = count_query.scalar() or 0
@ -218,7 +224,7 @@ class InboundSummaryService:
raise e
@staticmethod
def export_excel(keyword=None, start_date=None, end_date=None, source_type=None):
def export_excel(keyword=None, start_date=None, end_date=None, source_type=None, company=None):
"""
导出入库记录 Excel
"""
@ -304,6 +310,9 @@ class InboundSummaryService:
if source_type:
query = query.filter(cte.c.source_type == source_type)
if company:
query = query.filter(MaterialBase.company_name == company)
# 排序
query = query.order_by(desc(cte.c.inbound_date), asc(cte.c.sku))

View File

@ -1,6 +1,7 @@
# app/services/inbound/product_service.py
from app.extensions import db
from app.models.base import MaterialBase
from app.utils.decorators import get_current_company_filter
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.outbound import TransOutbound
@ -363,6 +364,13 @@ class ProductInboundService:
if material_type and material_type.strip():
query = query.filter(MaterialBase.material_type == material_type.strip())
# ============================================================
# 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================
company_limit = get_current_company_filter()
if company_limit is not None:
query = query.filter(MaterialBase.company_name == company_limit)
# ============================================================
# 【全局特权】基于 JWT 与 global:cross_company_op 的跨组织隔离
# ============================================================

View File

@ -1,6 +1,7 @@
# app/services/inbound/semi_service.py
from app.extensions import db
from app.models.base import MaterialBase
from app.utils.decorators import get_current_company_filter
from app.models.inbound.buy import StockBuy
from app.models.inbound.product import StockProduct
from app.models.outbound import TransOutbound
@ -453,6 +454,13 @@ class SemiInboundService:
if material_type and material_type.strip():
query = query.filter(MaterialBase.material_type == material_type.strip())
# ============================================================
# 【行级数据隔离】基于 JWT 多租户公司过滤
# ============================================================
company_limit = get_current_company_filter()
if company_limit is not None:
query = query.filter(MaterialBase.company_name == company_limit)
# ============================================================
# 【全局特权】基于 JWT 与 global:cross_company_op 的跨组织隔离
# ============================================================

View File

@ -158,9 +158,13 @@ class ServiceService:
try:
query = StockService.query.filter_by(is_deleted=False)
# 始终 join MaterialBase(base_id 为 NOT NULL,inner join 不会改变结果行数)
# 用于公司过滤和关键词搜索
query = query.join(StockService.base)
# 关键词联表搜索
if keyword:
query = query.join(StockService.base).filter(
query = query.filter(
db.or_(
StockService.sku.ilike(f'%{keyword}%'),
MaterialBase.name.ilike(f'%{keyword}%'),
@ -168,6 +172,13 @@ class ServiceService:
)
)
# 【行级数据隔离】基于 JWT 多租户公司过滤
from app.utils.decorators import get_current_company_filter
company_limit = get_current_company_filter()
if company_limit is not None:
query = query.filter(MaterialBase.company_name == company_limit)
# 日期过滤
if start_date:
try:

View File

@ -274,11 +274,12 @@ class OutboundService:
raise e
@staticmethod
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None):
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None):
"""
查询出库记录(按出库单号分组),包含详细物品信息
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
search_type: all, no, name, sku, material_name, spec_model
company: 可选的公司过滤参数
"""
# 日期补全:解决零点截断问题
if end_date and len(str(end_date).strip()) == 10:
@ -436,6 +437,44 @@ class OutboundService:
else:
keyword_conditions = None
# 【行级数据隔离】基于 JWT 多租户公司过滤
# 通过三个库存表路径,找到匹配公司的出库单号(排除 trans_repair,因其无 MaterialBase 关联)
from app.utils.decorators import get_current_company_filter
company_limit = get_current_company_filter()
if company_limit is not None:
buy_comp = db.session.query(TransOutbound.outbound_no).join(
StockBuy, and_(
TransOutbound.stock_id == StockBuy.id,
TransOutbound.source_table == 'stock_buy'
)
).join(MaterialBase, StockBuy.base_id == MaterialBase.id).filter(
MaterialBase.company_name == company_limit
).subquery()
semi_comp = db.session.query(TransOutbound.outbound_no).join(
StockSemi, and_(
TransOutbound.stock_id == StockSemi.id,
TransOutbound.source_table == 'stock_semi'
)
).join(MaterialBase, StockSemi.base_id == MaterialBase.id).filter(
MaterialBase.company_name == company_limit
).subquery()
prod_comp = db.session.query(TransOutbound.outbound_no).join(
StockProduct, and_(
TransOutbound.stock_id == StockProduct.id,
TransOutbound.source_table == 'stock_product'
)
).join(MaterialBase, StockProduct.base_id == MaterialBase.id).filter(
MaterialBase.company_name == company_limit
).subquery()
comp_all = db.session.query(buy_comp.c.outbound_no).union(
db.session.query(semi_comp.c.outbound_no),
db.session.query(prod_comp.c.outbound_no)
).subquery()
stmt = db.session.query(
TransOutbound.outbound_no,
func.max(TransOutbound.outbound_time).label('max_time')
@ -447,6 +486,10 @@ class OutboundService:
if start_date and end_date:
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
# 【行级数据隔离】应用公司过滤到主查询
if company_limit is not None:
stmt = stmt.filter(TransOutbound.outbound_no.in_(comp_all))
stmt = stmt.order_by(desc('max_time'))
# 使用 distinct 确保跨表查询不重复

View File

@ -6,6 +6,7 @@ from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
from app.models.base import MaterialBase
from app.utils.decorators import get_current_company_filter
from sqlalchemy import desc, func, nullslast, asc, or_, and_, case
from sqlalchemy.orm import joinedload
@ -523,6 +524,35 @@ class TransService:
.subquery()
)
# ====================================================================
# 【行级数据隔离】基于 JWT 多租户公司过滤
# 通过 stock 表关联到 MaterialBase,确保只返回本公司借还记录
# ====================================================================
company_borrow_nos_subq = None
company_limit = get_current_company_filter()
if company_limit is not None:
buy_nos = db.session.query(TransBorrow.borrow_no).join(
StockBuy, and_(TransBorrow.stock_id == StockBuy.id,
TransBorrow.source_table == 'stock_buy')
).join(MaterialBase, StockBuy.base_id == MaterialBase.id).filter(
MaterialBase.company_name == company_limit
)
semi_nos = db.session.query(TransBorrow.borrow_no).join(
StockSemi, and_(TransBorrow.stock_id == StockSemi.id,
TransBorrow.source_table == 'stock_semi')
).join(MaterialBase, StockSemi.base_id == MaterialBase.id).filter(
MaterialBase.company_name == company_limit
)
product_nos = db.session.query(TransBorrow.borrow_no).join(
StockProduct, and_(TransBorrow.stock_id == StockProduct.id,
TransBorrow.source_table == 'stock_product')
).join(MaterialBase, StockProduct.base_id == MaterialBase.id).filter(
MaterialBase.company_name == company_limit
)
company_borrow_nos_subq = buy_nos.union(
semi_nos, product_nos
).subquery()
# ====================================================================
# 步骤 2:纯净列查询分页(SELECT 只有 order_subq.c.borrow_no 一列)
# ====================================================================
@ -534,6 +564,12 @@ class TransService:
order_subq.c.borrow_no.in_(keyword_borrow_nos_subq)
)
# 公司隔离过滤
if company_borrow_nos_subq is not None:
borrow_no_q = borrow_no_q.filter(
order_subq.c.borrow_no.in_(company_borrow_nos_subq)
)
# 状态过滤(按"单号聚合"判定)
if status == 'borrowed':
# 单号下至少一条未还