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:
@ -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}%'
|
||||
# 关联子件表以支持子件搜索
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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 的跨组织隔离
|
||||
# ============================================================
|
||||
|
||||
@ -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 的跨组织隔离
|
||||
# ============================================================
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 确保跨表查询不重复
|
||||
|
||||
@ -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':
|
||||
# 单号下至少一条未还
|
||||
|
||||
Reference in New Issue
Block a user