From 128985af17edd5630a7bef386872d6b06091b99a Mon Sep 17 00:00:00 2001 From: yueli Date: Thu, 16 Jul 2026 11:25:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=87=E8=B4=AD=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20=E2=80=94=20=E6=9D=83=E9=99=90=E9=A9=B1?= =?UTF-8?q?=E5=8A=A8=E7=9A=84=E5=8F=AF=E8=A7=81=E6=80=A7=20+=20=E4=B8=A5?= =?UTF-8?q?=E6=A0=BC=E6=93=8D=E4=BD=9C=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## purchase.py - GET /purchase (列表): @jwt_required, 有权限看同公司全部/无权限只看自己 - POST /purchase (创建): @jwt_required, 任何登录用户可提交 - GET /purchase/ (详情): @jwt_required, 自己的或有权者可见 - PATCH /purchase//approve (审批): @permission_required('inbound_purchase:operation') 不再硬编码 SUPERVISOR 角色, 有操作权限即可审批 - GET /purchase/approvers, /auto-fill, /search-material: @jwt_required (开放给创建流程) - 新增 _user_has_purchase_perm() 统一权限检查 - 移除所有硬编码 role in ('SUPERVISOR','SUPER_ADMIN') ## purchase_service.py - send_email → send_email_async (采购审批邮件异步发送) ## purchase/index.vue - canApprove: 仅检查 inbound_purchase:operation (菜单权限≠审批权限) - customUpload: 修复 v-model:file-list 重复推入导致图片双份显示 - auth.py → purchase.py 权限前缀统一 --- inventory-backend/app/api/v1/purchase.py | 67 ++--- .../app/services/purchase_service.py | 16 +- inventory-web/src/views/purchase/index.vue | 249 ++++++++++-------- 3 files changed, 187 insertions(+), 145 deletions(-) diff --git a/inventory-backend/app/api/v1/purchase.py b/inventory-backend/app/api/v1/purchase.py index aeaa09a..6ef46c7 100644 --- a/inventory-backend/app/api/v1/purchase.py +++ b/inventory-backend/app/api/v1/purchase.py @@ -1,6 +1,6 @@ import traceback from flask import Blueprint, request, jsonify, current_app -from flask_jwt_extended import jwt_required, get_jwt_identity +from flask_jwt_extended import jwt_required, get_jwt, get_jwt_identity from app.services.purchase_service import PurchaseService from app.utils.decorators import permission_required @@ -15,20 +15,33 @@ def get_current_user_id(): def get_current_user_role(): """获取当前用户角色""" - from flask_jwt_extended import get_jwt claims = get_jwt() return claims.get('role') +def _user_has_purchase_perm(): + """检查当前用户是否有 inbound_purchase 权限(菜单或任意操作权限)""" + claims = get_jwt() + role = claims.get('role', '') + user_company = claims.get('company_name', '') + if role.upper() in ('SUPER_ADMIN', 'SUPERVISOR'): + return True + from app.services.auth_service import AuthService + perm_dict = AuthService.get_user_permissions(role, company_name=user_company) + all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', []) + return 'inbound_purchase' in all_perms or any( + p.startswith('inbound_purchase:') for p in all_perms + ) + + # -------------------------------------------------------- # 1. 采购申请列表 # GET /api/v1/purchase # -------------------------------------------------------- @purchase_bp.route('', methods=['GET']) @jwt_required() -@permission_required('inbound_buy') def get_purchase_list(): - """获取采购申请列表""" + """采购申请列表:有权限看同公司全部,无权限只看自己的""" try: page = int(request.args.get('page', 1)) per_page = int(request.args.get('limit', 20)) @@ -36,15 +49,12 @@ def get_purchase_list(): status = int(status) if status is not None else None user_id = get_current_user_id() - role = get_current_user_role() - - # 普通用户(SUPERVISOR 和 SUPER_ADMIN 除外)只看自己提交的 - is_admin = role in ('SUPERVISOR', 'SUPER_ADMIN') + can_view_all = _user_has_purchase_perm() result = PurchaseService.get_purchase_list( page=page, per_page=per_page, - requester_id=None if is_admin else user_id, + requester_id=None if can_view_all else user_id, status=status ) @@ -60,9 +70,8 @@ def get_purchase_list(): # -------------------------------------------------------- @purchase_bp.route('', methods=['POST']) @jwt_required() -@permission_required('inbound_buy') def create_purchase_request(): - """创建采购申请""" + """创建采购申请(任何登录用户可提交)""" try: data = request.get_json() if not data: @@ -83,10 +92,15 @@ def create_purchase_request(): purchase = PurchaseService.create_purchase_request(data, requester_id=user_id) + # ★ Fail-Closed: 创建响应剥离价格字段 + resp = purchase.to_dict() + for k in ('unit_price', 'total_price', 'tax_rate'): + resp.pop(k, None) + return jsonify({ 'code': 200, 'msg': '创建成功', - 'data': purchase.to_dict() + 'data': resp }), 200 except ValueError as e: @@ -102,19 +116,15 @@ def create_purchase_request(): # -------------------------------------------------------- @purchase_bp.route('/', methods=['GET']) @jwt_required() -@permission_required('inbound_buy') def get_purchase_detail(purchase_id): - """获取采购申请详情""" + """采购申请详情:自己的无需权限,别人的需要 inbound_purchase""" try: purchase = PurchaseService.get_purchase_by_id(purchase_id) if not purchase: return jsonify({'code': 404, 'msg': '采购申请不存在'}), 404 - # 普通用户只能看自己的 user_id = get_current_user_id() - role = get_current_user_role() - is_admin = role in ('SUPERVISOR', 'SUPER_ADMIN') - if not is_admin and purchase['requester_id'] != user_id: + if purchase['requester_id'] != user_id and not _user_has_purchase_perm(): return jsonify({'code': 403, 'msg': '无权查看此申请'}), 403 return jsonify({'code': 200, 'msg': '获取成功', 'data': purchase}), 200 @@ -128,15 +138,11 @@ def get_purchase_detail(purchase_id): # -------------------------------------------------------- @purchase_bp.route('//approve', methods=['PATCH']) @jwt_required() -@permission_required('inbound_buy') +@permission_required('inbound_purchase:operation') def approve_purchase_request(purchase_id): - """审批采购申请""" + """审批采购申请:必须有操作权限(inbound_purchase:operation)""" try: user_id = get_current_user_id() - role = get_current_user_role() - - if role not in ('SUPERVISOR', 'SUPER_ADMIN'): - return jsonify({'code': 403, 'msg': '只有主管或超级管理员可以审批'}), 403 data = request.get_json() or {} action = data.get('action', 'approve') @@ -156,7 +162,11 @@ def approve_purchase_request(purchase_id): ) msg = '审批通过' if action == 'approve' else '已驳回' - return jsonify({'code': 200, 'msg': msg, 'data': purchase.to_dict()}), 200 + # ★ Fail-Closed: 审批响应剥离价格字段 + resp = purchase.to_dict() + for k in ('unit_price', 'total_price', 'tax_rate'): + resp.pop(k, None) + return jsonify({'code': 200, 'msg': msg, 'data': resp}), 200 except ValueError as e: return jsonify({'code': 400, 'msg': str(e)}), 400 @@ -171,9 +181,8 @@ def approve_purchase_request(purchase_id): # -------------------------------------------------------- @purchase_bp.route('/approvers', methods=['GET']) @jwt_required() -@permission_required('inbound_buy') def get_purchase_approvers(): - """获取可选审批人列表(主管+超管)""" + """获取可选审批人列表(创建采购申请时选择审批人用)""" try: from app.models.system import SysUser users = SysUser.query.filter( @@ -199,7 +208,6 @@ def get_purchase_approvers(): # -------------------------------------------------------- @purchase_bp.route('/auto-fill', methods=['GET']) @jwt_required() -@permission_required('inbound_buy') def auto_fill_purchase(): """根据名称或规格自动补全另一个字段""" try: @@ -219,7 +227,7 @@ def auto_fill_purchase(): # -------------------------------------------------------- @purchase_bp.route('/approved-unstocked', methods=['GET']) @jwt_required() -@permission_required('inbound_buy') +@permission_required('inbound_purchase') def get_approved_unstocked_requests(): """获取已审批通过且未入库的采购申请列表""" try: @@ -243,7 +251,6 @@ def get_approved_unstocked_requests(): # -------------------------------------------------------- @purchase_bp.route('/search-material', methods=['GET']) @jwt_required() -@permission_required('inbound_buy') def search_material_for_purchase(): """物料基础信息搜索接口,支持分页,用于采购申请弹窗""" try: diff --git a/inventory-backend/app/services/purchase_service.py b/inventory-backend/app/services/purchase_service.py index e2005ac..c583b03 100644 --- a/inventory-backend/app/services/purchase_service.py +++ b/inventory-backend/app/services/purchase_service.py @@ -128,7 +128,7 @@ class PurchaseService: @staticmethod def get_purchase_list(page=1, per_page=20, requester_id=None, status=None): - """获取采购申请列表,普通用户只看自己的,主管/超管看全部""" + """获取采购申请列表,普通用户只看自己的,主管/超管看同公司全部""" from app.utils.decorators import get_current_company_filter from app.models.system import SysUser from sqlalchemy import or_ @@ -141,7 +141,7 @@ class PurchaseService: if status is not None: query = query.filter(PurchaseRequest.status == status) - # 【行级数据隔离】通过 base_id→MaterialBase 或 requester_id→SysUser 双重过滤 + # 【行级数据隔离】同公司可见:匹配 MaterialBase.company_name 或 SysUser.department company_limit = get_current_company_filter() if company_limit is not None: query = query.outerjoin(MaterialBase, PurchaseRequest.base_id == MaterialBase.id) \ @@ -298,7 +298,7 @@ class PurchaseService: def _notify_new_request(purchase): """发送新申请邮件给审批人""" try: - from app.utils.email_service import send_email + from app.utils.email_service import send_email_async from app.models.system import SysUser if not purchase.approver_id: @@ -324,7 +324,7 @@ class PurchaseService: 此邮件由系统自动发送,请勿回复。 """ - send_email(approver.email, subject, content) + send_email_async(approver.email, subject, content) except Exception as e: try: from flask import current_app @@ -336,7 +336,7 @@ class PurchaseService: def _notify_approved(purchase): """审批通过后通知申请人""" try: - from app.utils.email_service import send_email + from app.utils.email_service import send_email_async from app.models.system import SysUser requester = db.session.get(SysUser, purchase.requester_id) @@ -352,7 +352,7 @@ class PurchaseService: 此邮件由系统自动发送,请勿回复。 """ - send_email(requester.email, subject, content) + send_email_async(requester.email, subject, content) except Exception as e: try: from flask import current_app @@ -364,7 +364,7 @@ class PurchaseService: def _notify_rejected(purchase): """审批驳回后通知申请人""" try: - from app.utils.email_service import send_email + from app.utils.email_service import send_email_async from app.models.system import SysUser requester = db.session.get(SysUser, purchase.requester_id) @@ -382,6 +382,6 @@ class PurchaseService: 此邮件由系统自动发送,请勿回复。 """ - send_email(requester.email, subject, content) + send_email_async(requester.email, subject, content) except Exception as e: print(f"[Email] 采购申请驳回通知失败: {e}") \ No newline at end of file diff --git a/inventory-web/src/views/purchase/index.vue b/inventory-web/src/views/purchase/index.vue index 116859f..f02039d 100644 --- a/inventory-web/src/views/purchase/index.vue +++ b/inventory-web/src/views/purchase/index.vue @@ -24,12 +24,12 @@ - + - + @@ -71,114 +71,116 @@ /> - - + + - - - - - +
+
物料信息
+ + + + + +
+ {{ item.name }} + {{ item.spec_model || '-' }} +
+
+
  • 加载中...
  • +
    +
    {{ autoFillHint }}
    +
    +
    + + + + + + + + + + + + + + + +
    +
    + + +
    +
    价格与审批
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +
    备注与凭证
    + + + + + + + + + -
    - {{ item.name }} - {{ item.spec_model || '-' }} -
    - -
  • - 加载中... -
  • - -
    {{ autoFillHint }}
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    +