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:
@ -14,6 +14,7 @@ def get_list():
|
|||||||
start_date = request.args.get('start_date')
|
start_date = request.args.get('start_date')
|
||||||
end_date = request.args.get('end_date')
|
end_date = request.args.get('end_date')
|
||||||
source_type = request.args.get('source_type') # 可选:筛选 specific table
|
source_type = request.args.get('source_type') # 可选:筛选 specific table
|
||||||
|
company = request.args.get('company', '')
|
||||||
|
|
||||||
result = InboundSummaryService.get_list(
|
result = InboundSummaryService.get_list(
|
||||||
page=page,
|
page=page,
|
||||||
@ -21,7 +22,8 @@ def get_list():
|
|||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
source_type=source_type
|
source_type=source_type,
|
||||||
|
company=company
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -47,13 +49,15 @@ def export_data():
|
|||||||
start_date = request.args.get('start_date')
|
start_date = request.args.get('start_date')
|
||||||
end_date = request.args.get('end_date')
|
end_date = request.args.get('end_date')
|
||||||
source_type = request.args.get('source_type')
|
source_type = request.args.get('source_type')
|
||||||
|
company = request.args.get('company', '')
|
||||||
|
|
||||||
# 调用导出服务
|
# 调用导出服务
|
||||||
file_stream = InboundSummaryService.export_excel(
|
file_stream = InboundSummaryService.export_excel(
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
source_type=source_type
|
source_type=source_type,
|
||||||
|
company=company
|
||||||
)
|
)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
@ -179,10 +179,10 @@ def get_outbound_list():
|
|||||||
limit = int(request.args.get('limit', 10))
|
limit = int(request.args.get('limit', 10))
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
search_type = request.args.get('search_type', 'all')
|
search_type = request.args.get('search_type', 'all')
|
||||||
# 如果前端传了日期范围,可以解析处理,这里暂略
|
company = request.args.get('company', '')
|
||||||
|
|
||||||
# ★ [修改] 调用分组查询服务,支持搜索类型
|
# ★ [修改] 调用分组查询服务,支持搜索类型
|
||||||
result = OutboundService.get_grouped_list(page, limit, keyword, search_type=search_type)
|
result = OutboundService.get_grouped_list(page, limit, keyword, search_type=search_type, company=company)
|
||||||
|
|
||||||
# 字段级脱敏
|
# 字段级脱敏
|
||||||
user_permissions = get_current_user_permissions()
|
user_permissions = get_current_user_permissions()
|
||||||
|
|||||||
@ -8,11 +8,12 @@ permission_bp = Blueprint('permission', __name__)
|
|||||||
|
|
||||||
|
|
||||||
def _get_operator_company():
|
def _get_operator_company():
|
||||||
"""从 JWT 获取当前操作者公司(None=超管)"""
|
"""从 JWT 获取当前操作者公司(None=超管,具体值=该角色所属公司)"""
|
||||||
claims = get_jwt()
|
claims = get_jwt()
|
||||||
role = claims.get('role', '')
|
role = claims.get('role', '')
|
||||||
if role and role.upper() == 'SUPER_ADMIN':
|
if role and role.upper() == 'SUPER_ADMIN':
|
||||||
return None # 超管不限制公司
|
return None # 超管不限制公司
|
||||||
|
return claims.get('company_name', '')
|
||||||
|
|
||||||
|
|
||||||
def _has_system_permission(role_code):
|
def _has_system_permission(role_code):
|
||||||
@ -24,7 +25,6 @@ def _has_system_permission(role_code):
|
|||||||
return 'system_permission' in all_perms
|
return 'system_permission' in all_perms
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
return claims.get('company_name', '')
|
|
||||||
|
|
||||||
|
|
||||||
@permission_bp.route('/tree', methods=['GET'])
|
@permission_bp.route('/tree', methods=['GET'])
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# inventory-backend/app/api/v1/scrap.py
|
# inventory-backend/app/api/v1/scrap.py
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
||||||
from app.utils.decorators import permission_required, audit_log
|
from app.utils.decorators import permission_required, audit_log, get_current_company_filter
|
||||||
from app.services.auth_service import AuthService
|
from app.services.auth_service import AuthService
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.transaction import TransScrap, TransRepair
|
from app.models.transaction import TransScrap, TransRepair
|
||||||
@ -322,6 +322,37 @@ class ScrapService:
|
|||||||
if end_date:
|
if end_date:
|
||||||
query = query.filter(TransScrap.operation_time <= end_date + ' 23:59:59')
|
query = query.filter(TransScrap.operation_time <= end_date + ' 23:59:59')
|
||||||
|
|
||||||
|
# 【行级数据隔离】基于 JWT 多租户公司过滤
|
||||||
|
# 通过 stock 表或 trans_repair 关联到 MaterialBase
|
||||||
|
company_limit = get_current_company_filter()
|
||||||
|
if company_limit is not None:
|
||||||
|
buy_subq = db.session.query(TransScrap.id).join(
|
||||||
|
StockBuy, db.and_(TransScrap.stock_id == StockBuy.id,
|
||||||
|
TransScrap.source_table == 'stock_buy')
|
||||||
|
).join(MaterialBase, StockBuy.base_id == MaterialBase.id).filter(
|
||||||
|
MaterialBase.company_name == company_limit
|
||||||
|
)
|
||||||
|
semi_subq = db.session.query(TransScrap.id).join(
|
||||||
|
StockSemi, db.and_(TransScrap.stock_id == StockSemi.id,
|
||||||
|
TransScrap.source_table == 'stock_semi')
|
||||||
|
).join(MaterialBase, StockSemi.base_id == MaterialBase.id).filter(
|
||||||
|
MaterialBase.company_name == company_limit
|
||||||
|
)
|
||||||
|
product_subq = db.session.query(TransScrap.id).join(
|
||||||
|
StockProduct, db.and_(TransScrap.stock_id == StockProduct.id,
|
||||||
|
TransScrap.source_table == 'stock_product')
|
||||||
|
).join(MaterialBase, StockProduct.base_id == MaterialBase.id).filter(
|
||||||
|
MaterialBase.company_name == company_limit
|
||||||
|
)
|
||||||
|
repair_subq = db.session.query(TransScrap.id).join(
|
||||||
|
TransRepair, db.and_(TransScrap.stock_id == TransRepair.id,
|
||||||
|
TransScrap.source_table == 'trans_repair')
|
||||||
|
).join(MaterialBase, TransRepair.base_id == MaterialBase.id).filter(
|
||||||
|
MaterialBase.company_name == company_limit
|
||||||
|
)
|
||||||
|
all_matches = buy_subq.union(semi_subq, product_subq, repair_subq).subquery()
|
||||||
|
query = query.filter(TransScrap.id.in_(all_matches))
|
||||||
|
|
||||||
# 按时间倒序
|
# 按时间倒序
|
||||||
query = query.order_by(TransScrap.operation_time.desc())
|
query = query.order_by(TransScrap.operation_time.desc())
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from app.models.base import MaterialBase
|
|||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
|
from app.utils.decorators import get_current_company_filter
|
||||||
from sqlalchemy import func, distinct, or_, case
|
from sqlalchemy import func, distinct, or_, case
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import uuid
|
import uuid
|
||||||
@ -126,6 +127,11 @@ class BomService:
|
|||||||
if active_only:
|
if active_only:
|
||||||
query_base = query_base.filter(BomTable.is_enabled == True)
|
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:
|
if keyword:
|
||||||
kw = f'%{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] 增加返回数量限制
|
# [修改1] 增加返回数量限制
|
||||||
# 原为 limit(20),现改为 1000,确保前端能获取所有(或足够多)的数据
|
# 原为 limit(20),现改为 1000,确保前端能获取所有(或足够多)的数据
|
||||||
query = query.limit(1000)
|
query = query.limit(1000)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from openpyxl.utils import get_column_letter
|
|||||||
class InboundSummaryService:
|
class InboundSummaryService:
|
||||||
|
|
||||||
@staticmethod
|
@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 三张表
|
1. 联合 StockBuy, StockSemi, StockProduct 三张表
|
||||||
@ -126,6 +126,10 @@ class InboundSummaryService:
|
|||||||
if source_type:
|
if source_type:
|
||||||
query = query.filter(cte.c.source_type == source_type)
|
query = query.filter(cte.c.source_type == source_type)
|
||||||
|
|
||||||
|
# 公司过滤(跨域选择器传入)
|
||||||
|
if company:
|
||||||
|
query = query.filter(MaterialBase.company_name == company)
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 5. 获取总数
|
# 5. 获取总数
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@ -139,6 +143,8 @@ class InboundSummaryService:
|
|||||||
count_query = count_query.filter(cte.c.inbound_date.between(start_date, end_date))
|
count_query = count_query.filter(cte.c.inbound_date.between(start_date, end_date))
|
||||||
if source_type:
|
if source_type:
|
||||||
count_query = count_query.filter(cte.c.source_type == 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
|
total = count_query.scalar() or 0
|
||||||
|
|
||||||
@ -218,7 +224,7 @@ class InboundSummaryService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@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
|
导出入库记录 Excel
|
||||||
"""
|
"""
|
||||||
@ -304,6 +310,9 @@ class InboundSummaryService:
|
|||||||
if source_type:
|
if source_type:
|
||||||
query = query.filter(cte.c.source_type == 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))
|
query = query.order_by(desc(cte.c.inbound_date), asc(cte.c.sku))
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# app/services/inbound/product_service.py
|
# app/services/inbound/product_service.py
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.base import MaterialBase
|
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.buy import StockBuy
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
@ -363,6 +364,13 @@ class ProductInboundService:
|
|||||||
if material_type and material_type.strip():
|
if material_type and material_type.strip():
|
||||||
query = query.filter(MaterialBase.material_type == 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 的跨组织隔离
|
# 【全局特权】基于 JWT 与 global:cross_company_op 的跨组织隔离
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# app/services/inbound/semi_service.py
|
# app/services/inbound/semi_service.py
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.base import MaterialBase
|
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.buy import StockBuy
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
@ -453,6 +454,13 @@ class SemiInboundService:
|
|||||||
if material_type and material_type.strip():
|
if material_type and material_type.strip():
|
||||||
query = query.filter(MaterialBase.material_type == 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 的跨组织隔离
|
# 【全局特权】基于 JWT 与 global:cross_company_op 的跨组织隔离
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@ -158,9 +158,13 @@ class ServiceService:
|
|||||||
try:
|
try:
|
||||||
query = StockService.query.filter_by(is_deleted=False)
|
query = StockService.query.filter_by(is_deleted=False)
|
||||||
|
|
||||||
|
# 始终 join MaterialBase(base_id 为 NOT NULL,inner join 不会改变结果行数)
|
||||||
|
# 用于公司过滤和关键词搜索
|
||||||
|
query = query.join(StockService.base)
|
||||||
|
|
||||||
# 关键词联表搜索
|
# 关键词联表搜索
|
||||||
if keyword:
|
if keyword:
|
||||||
query = query.join(StockService.base).filter(
|
query = query.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
StockService.sku.ilike(f'%{keyword}%'),
|
StockService.sku.ilike(f'%{keyword}%'),
|
||||||
MaterialBase.name.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:
|
if start_date:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -274,11 +274,12 @@ class OutboundService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@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、物料名称、规格型号
|
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
||||||
search_type: all, no, name, sku, material_name, spec_model
|
search_type: all, no, name, sku, material_name, spec_model
|
||||||
|
company: 可选的公司过滤参数
|
||||||
"""
|
"""
|
||||||
# 日期补全:解决零点截断问题
|
# 日期补全:解决零点截断问题
|
||||||
if end_date and len(str(end_date).strip()) == 10:
|
if end_date and len(str(end_date).strip()) == 10:
|
||||||
@ -436,6 +437,44 @@ class OutboundService:
|
|||||||
else:
|
else:
|
||||||
keyword_conditions = None
|
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(
|
stmt = db.session.query(
|
||||||
TransOutbound.outbound_no,
|
TransOutbound.outbound_no,
|
||||||
func.max(TransOutbound.outbound_time).label('max_time')
|
func.max(TransOutbound.outbound_time).label('max_time')
|
||||||
@ -447,6 +486,10 @@ class OutboundService:
|
|||||||
if start_date and end_date:
|
if start_date and end_date:
|
||||||
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, 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'))
|
stmt = stmt.order_by(desc('max_time'))
|
||||||
|
|
||||||
# 使用 distinct 确保跨表查询不重复
|
# 使用 distinct 确保跨表查询不重复
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from app.models.inbound.buy import StockBuy
|
|||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
from app.models.base import MaterialBase
|
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 import desc, func, nullslast, asc, or_, and_, case
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
@ -523,6 +524,35 @@ class TransService:
|
|||||||
.subquery()
|
.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 一列)
|
# 步骤 2:纯净列查询分页(SELECT 只有 order_subq.c.borrow_no 一列)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
@ -534,6 +564,12 @@ class TransService:
|
|||||||
order_subq.c.borrow_no.in_(keyword_borrow_nos_subq)
|
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':
|
if status == 'borrowed':
|
||||||
# 单号下至少一条未还
|
# 单号下至少一条未还
|
||||||
|
|||||||
@ -227,7 +227,10 @@ def get_current_company_filter():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 普通用户 → 强制隔离到本公司
|
# 普通用户 → 强制隔离到本公司
|
||||||
return user_company if user_company else None
|
# 如果 JWT 中没有 company_name,返回哨兵值确保不会匹配任何数据
|
||||||
|
if user_company:
|
||||||
|
return user_company
|
||||||
|
return '__NO_COMPANY__'
|
||||||
|
|
||||||
|
|
||||||
def audit_log(module: str = None, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None):
|
def audit_log(module: str = None, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None):
|
||||||
|
|||||||
@ -14,7 +14,7 @@ BEGIN;
|
|||||||
|
|
||||||
-- 1. 向 sys_element 插入跨域权限码(挂 system 菜单下)
|
-- 1. 向 sys_element 插入跨域权限码(挂 system 菜单下)
|
||||||
INSERT INTO sys_element (menu_code, name, code, element_type)
|
INSERT INTO sys_element (menu_code, name, code, element_type)
|
||||||
SELECT 'system_mgmt', '全局跨域访问', 'crossDomain', 'button'
|
SELECT 'system_mgmt', '全局跨域访问', 'crossDomain', 'element'
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM sys_element WHERE code = 'crossDomain'
|
SELECT 1 FROM sys_element WHERE code = 'crossDomain'
|
||||||
);
|
);
|
||||||
@ -29,7 +29,7 @@ WHERE NOT EXISTS (
|
|||||||
SELECT 1 FROM sys_menu WHERE code = 'system_cross_domain'
|
SELECT 1 FROM sys_menu WHERE code = 'system_cross_domain'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 3. 默认分配给 SUPER_ADMIN 角色
|
-- 3. 默认分配给 SUPER_ADMIN 和 SUPERVISOR 角色
|
||||||
INSERT INTO sys_role_permission (role_code, target_code, type)
|
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||||
SELECT 'SUPER_ADMIN', 'crossDomain', 'element'
|
SELECT 'SUPER_ADMIN', 'crossDomain', 'element'
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
@ -37,6 +37,13 @@ WHERE NOT EXISTS (
|
|||||||
WHERE role_code = 'SUPER_ADMIN' AND target_code = 'crossDomain'
|
WHERE role_code = 'SUPER_ADMIN' AND target_code = 'crossDomain'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT INTO sys_role_permission (role_code, target_code, type)
|
||||||
|
SELECT 'SUPERVISOR', 'crossDomain', 'element'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_role_permission
|
||||||
|
WHERE role_code = 'SUPERVISOR' AND target_code = 'crossDomain'
|
||||||
|
);
|
||||||
|
|
||||||
-- 4. 验证
|
-- 4. 验证
|
||||||
SELECT code, name, menu_code, element_type
|
SELECT code, name, menu_code, element_type
|
||||||
FROM sys_element WHERE code = 'crossDomain';
|
FROM sys_element WHERE code = 'crossDomain';
|
||||||
|
|||||||
Reference in New Issue
Block a user