from flask import Blueprint, request, jsonify, current_app from sqlalchemy import or_ from app.services.bom_service import BomService from app.models.base import MaterialBase from app.models.bom import BomTable from app.extensions import db from flask_jwt_extended import jwt_required, get_jwt from app.utils.decorators import permission_required, audit_log from app.services.auth_service import AuthService bom_bp = Blueprint('bom', __name__) # ============================================================================== # 辅助函数:获取当前用户的完整权限列表(基于角色查询) # ============================================================================== def get_current_user_permissions(): """ 返回当前用户拥有的所有权限码列表(包括菜单和元素) 此函数根据角色查询数据库得到权限。 """ claims = get_jwt() user_role = claims.get('role') if not user_role: return [] # 超级管理员返回所有字段权限 (忽略大小写) if user_role.upper() == 'SUPER_ADMIN': return ['bom_manage:*'] perm_dict = AuthService.get_user_permissions(user_role) # 合并菜单和元素权限 perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) return perms def filter_item_by_permissions(item_dict, user_permissions): """ 根据用户权限过滤 item 字典,无权限的字段值置为 None """ # 字段名到权限码的映射(与前端 permissionMap 保持一致) field_to_perm = { 'bom_no': 'bom_manage:bom_no', 'parent_name': 'bom_manage:parent_name', 'parent_spec': 'bom_manage:parent_spec', 'version': 'bom_manage:version', 'is_enabled': 'bom_manage:status', 'child_count': 'bom_manage:child_count', } # 如果用户是超级管理员且有 'bom_manage:*',则不过滤 if 'bom_manage:*' in user_permissions: return item_dict for field, perm_code in field_to_perm.items(): if field in item_dict and perm_code not in user_permissions: item_dict[field] = None return item_dict # ==================== 新版 BOM 接口(基于 bom_no) ==================== @bom_bp.route('/list', methods=['GET']) @jwt_required() @permission_required('bom_manage') def get_bom_list(): """获取所有 BOM 配方列表,支持 keyword 搜索和 active_only 过滤""" try: keyword = request.args.get('keyword', '').strip() # 将字符串 'true' 转为布尔值 active_only = request.args.get('active_only', 'false').lower() == 'true' data = BomService.get_bom_list(keyword=keyword, active_only=active_only) # 字段级脱敏 user_permissions = get_current_user_permissions() if isinstance(data, list): data = [filter_item_by_permissions(item, user_permissions) for item in data] return jsonify({ 'code': 200, 'msg': 'success', 'data': data }) except Exception as e: current_app.logger.error(f'获取BOM列表失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 @bom_bp.route('/detail/', methods=['GET']) @jwt_required() @permission_required('bom_manage') def get_bom_detail(bom_no): """ 根据 BOM 编号获取配方详情 Query参数: ?version=V1.0 (如果不传则取最新) """ try: version = request.args.get('version') data = BomService.get_bom_detail(bom_no, version=version) if not data: return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404 # 字段级脱敏 user_permissions = get_current_user_permissions() data = filter_item_by_permissions(data, user_permissions) return jsonify({ 'code': 200, 'msg': 'success', 'data': data }) except Exception as e: current_app.logger.error(f'获取BOM详情失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 @bom_bp.route('/save', methods=['POST']) @jwt_required() @permission_required('bom_manage:operation') @audit_log( module='BOM管理', action='新增', get_target_name_fn=lambda: request.get_json().get('bom_no') if request.get_json() else None ) def save_bom(): """保存或更新 BOM 配方(支持自定义 bom_no 和 多版本)""" try: req_data = request.get_json() # 数据清洗:移除用户没有权限的字段 user_permissions = get_current_user_permissions() # 超级管理员不过滤 if 'bom_manage:*' not in user_permissions: # 字段名到权限码的映射 field_to_perm = { 'parent_id': 'bom_manage:parent_id', 'version': 'bom_manage:version', 'is_enabled': 'bom_manage:status', 'bom_no': 'bom_manage:bom_no', } # 清洗顶级字段 for field in list(req_data.keys()): perm_code = field_to_perm.get(field) if perm_code and perm_code not in user_permissions: req_data.pop(field, None) # 清洗 children 中的字段 if 'children' in req_data and isinstance(req_data['children'], list): for child in req_data['children']: # 子件字段映射 child_field_to_perm = { 'child_id': 'bom_manage:child_id', 'dosage': 'bom_manage:dosage', 'remark': 'bom_manage:remark', } for field in list(child.keys()): perm_code = child_field_to_perm.get(field) if perm_code and perm_code not in user_permissions: child.pop(field, None) # 必需字段校验 if 'parent_id' not in req_data or 'children' not in req_data: return jsonify({'code': 400, 'msg': '缺少 parent_id 或 children 字段'}), 400 # 校验 bom_no 不能为空 if 'bom_no' in req_data and not req_data['bom_no']: return jsonify({'code': 400, 'msg': 'BOM编号不能为空'}), 400 bom_no = BomService.save_bom(req_data) return jsonify({ 'code': 200, 'msg': '保存成功', 'data': {'bom_no': bom_no} }) except ValueError as e: return jsonify({'code': 400, 'msg': str(e)}), 400 except Exception as e: current_app.logger.error(f'保存BOM失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 @bom_bp.route('/stock/', methods=['GET']) @jwt_required() @permission_required('bom_manage') def get_bom_with_stock_by_no(bom_no): """根据 BOM 编号获取配方详情及库存信息""" try: data = BomService.get_bom_with_stock_by_bom_no(bom_no) if not data: return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404 # 字段级脱敏 user_permissions = get_current_user_permissions() data = filter_item_by_permissions(data, user_permissions) return jsonify({ 'code': 200, 'msg': 'success', 'data': data }) except Exception as e: current_app.logger.error(f'获取BOM库存信息失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 # ==================== 删除BOM接口 ==================== @bom_bp.route('/', methods=['DELETE']) @jwt_required() @permission_required('bom_manage:operation') @audit_log( module='BOM管理', action='删除', get_target_id_fn=lambda: request.view_args.get('bom_no') ) def delete_bom(bom_no): """ 根据 BOM 编号删除 Query参数: ?version=V1.0 (如果不传,删除该编号下所有版本) """ try: version = request.args.get('version') query = BomTable.query.filter_by(bom_no=bom_no) if version: query = query.filter_by(version=version) exist = query.first() if not exist: return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404 # 删除 query.delete() db.session.commit() return jsonify({ 'code': 200, 'msg': '删除成功', 'bom_no': bom_no }) except Exception as e: current_app.logger.error(f'删除BOM失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 # ==================== 兼容旧接口 ==================== @bom_bp.route('/', methods=['GET']) @jwt_required() @permission_required('bom_manage') def get_bom(parent_id): try: data = BomService.get_bom_with_stock(parent_id) # 字段级脱敏 user_permissions = get_current_user_permissions() data = filter_item_by_permissions(data, user_permissions) return jsonify({ 'code': 200, 'msg': 'success', 'data': data }) except Exception as e: current_app.logger.error(f'获取BOM失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 @bom_bp.route('', methods=['POST']) @jwt_required() @permission_required('bom_manage:operation') @audit_log( module='BOM管理', action='新增', get_target_name_fn=lambda: request.get_json().get('bom_no') if request.get_json() else None ) def save_bom_legacy(): try: req_data = request.get_json() # 数据清洗:移除用户没有权限的字段 user_permissions = get_current_user_permissions() # 超级管理员不过滤 if 'bom_manage:*' not in user_permissions: # 字段名到权限码的映射 field_to_perm = { 'parent_id': 'bom_manage:parent_id', 'version': 'bom_manage:version', 'is_enabled': 'bom_manage:status', 'bom_no': 'bom_manage:bom_no', } # 清洗顶级字段 for field in list(req_data.keys()): perm_code = field_to_perm.get(field) if perm_code and perm_code not in user_permissions: req_data.pop(field, None) # 清洗 children 中的字段 if 'children' in req_data and isinstance(req_data['children'], list): for child in req_data['children']: # 子件字段映射 child_field_to_perm = { 'child_id': 'bom_manage:child_id', 'dosage': 'bom_manage:dosage', 'remark': 'bom_manage:remark', } for field in list(child.keys()): perm_code = child_field_to_perm.get(field) if perm_code and perm_code not in user_permissions: child.pop(field, None) parent_id = req_data.get('parent_id') child_list = req_data.get('children', []) if not parent_id or not isinstance(child_list, list): return jsonify({'code': 400, 'msg': '参数错误'}), 400 BomService.create_or_update_bom(parent_id, child_list) return jsonify({ 'code': 200, 'msg': '保存成功' }) except ValueError as e: return jsonify({'code': 400, 'msg': str(e)}), 400 except Exception as e: current_app.logger.error(f'保存BOM失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 @bom_bp.route('/base/list', methods=['GET']) @jwt_required() @permission_required('bom_manage') def get_material_base_list(): """获取基础物料列表,支持分页和关键字搜索,用于前端下拉框""" try: # 获取分页和搜索参数 page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 20)) keyword = request.args.get('keyword', '').strip() # 构建查询条件 query = MaterialBase.query.filter_by(is_enabled=True) # 添加关键字模糊搜索 if keyword: query = query.filter( or_( MaterialBase.name.ilike(f"%{keyword}%"), MaterialBase.spec_model.ilike(f"%{keyword}%") ) ) # 执行分页查询 pagination = query.order_by(MaterialBase.id.desc()).paginate( page=page, per_page=limit, error_out=False ) # 构建返回数据 data = { 'list': [item.to_dict() for item in pagination.items], 'total': pagination.total } return jsonify({ 'code': 200, 'msg': 'success', 'data': data }) except Exception as e: current_app.logger.error(f'获取基础物料列表失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 @bom_bp.route('/parents', methods=['GET']) @jwt_required() @permission_required('bom_manage') def get_bom_parents(): """获取所有已定义BOM的父件物料列表(兼容旧版)""" try: subq = db.session.query(BomTable.parent_id).distinct().subquery() parents = MaterialBase.query.join(subq, MaterialBase.id == subq.c.parent_id).all() data = [item.to_dict() for item in parents] # 字段级脱敏 (如果需要) user_permissions = get_current_user_permissions() data = [filter_item_by_permissions(item, user_permissions) for item in data] return jsonify({ 'code': 200, 'msg': 'success', 'data': data }) except Exception as e: current_app.logger.error(f'获取BOM父件列表失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500