feat: Redis幂等锁 + 关键端点防重复提交

## decorators.py
- 新增 @prevent_double_submit(lock_timeout=5) 装饰器
  Redis key: idem:{user_id}:{path}:{MD5(body)}
  锁存在→409, 不存在→setex→执行业务→delete
  Redis不可用时fail-open降级放行

## purchase.py
- POST /purchase (创建): +@prevent_double_submit
- PATCH /purchase/<id>/approve (审批): +@prevent_double_submit

## outbound.py
- POST /outbound (出库): +@prevent_double_submit

## transactions.py
- POST /borrow/dispatch (借库扣减): +@prevent_double_submit
This commit is contained in:
yueli
2026-07-16 13:08:37 +08:00
parent 8c10bb0903
commit 347f20497c
4 changed files with 68 additions and 3 deletions

View File

@ -1,7 +1,7 @@
from flask import Blueprint, request, jsonify
from app.services.outbound_service import OutboundService
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, prevent_double_submit
from app.services.auth_service import AuthService
import traceback
@ -110,6 +110,7 @@ def scan_barcode():
# --------------------------------------------------------
@outbound_bp.route('', methods=['POST'])
@jwt_required()
@prevent_double_submit(lock_timeout=5)
@audit_log(
module='出库管理',
action='新增',

View File

@ -2,7 +2,7 @@ import traceback
from flask import Blueprint, request, jsonify, current_app
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
from app.utils.decorators import permission_required, prevent_double_submit
purchase_bp = Blueprint('purchase', __name__, url_prefix='/api/v1/purchase')
@ -70,6 +70,7 @@ def get_purchase_list():
# --------------------------------------------------------
@purchase_bp.route('', methods=['POST'])
@jwt_required()
@prevent_double_submit(lock_timeout=5)
def create_purchase_request():
"""创建采购申请(任何登录用户可提交)"""
try:
@ -138,6 +139,7 @@ def get_purchase_detail(purchase_id):
# --------------------------------------------------------
@purchase_bp.route('/<int:purchase_id>/approve', methods=['PATCH'])
@jwt_required()
@prevent_double_submit(lock_timeout=5)
@permission_required('inbound_purchase:operation')
def approve_purchase_request(purchase_id):
"""审批采购申请必须有操作权限inbound_purchase:operation"""

View File

@ -1,6 +1,6 @@
from flask import Blueprint, jsonify, request # .material -> .base refactor checked
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, prevent_double_submit
from app.services.auth_service import AuthService
from app.services.trans_service import TransService
from app.services.borrow_service import BorrowApprovalService
@ -308,6 +308,7 @@ def get_borrow_stock_list():
# --- 执行借库扣减(审批通过后调用)---
@trans_bp.route('/borrow/dispatch', methods=['POST'])
@jwt_required()
@prevent_double_submit(lock_timeout=5)
@permission_required('op_borrow:operation')
def dispatch_borrow():
"""