Compare commits
7 Commits
3ab47ab2fb
...
de0a5c8db2
| Author | SHA1 | Date | |
|---|---|---|---|
| de0a5c8db2 | |||
| bc866e7670 | |||
| ea28ee1c86 | |||
| aeea3fc25c | |||
| e1e74e5983 | |||
| be6575344a | |||
| 525acae423 |
@ -130,8 +130,29 @@ def create_app():
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Permission 模块导入失败 (请检查 app/api/v1/permission.py 是否存在): {e}")
|
||||
|
||||
# 2.8 注册审计日志模块 (Audit)
|
||||
# -----------------------------------------------------
|
||||
# 2.8 注册库位管理模块 (Warehouse)
|
||||
try:
|
||||
from app.api.v1.audit import audit_bp
|
||||
# 标准: /api/v1/audit/logs
|
||||
app.register_blueprint(audit_bp, url_prefix='/api/v1/audit')
|
||||
# 兼容: /api/audit/logs
|
||||
app.register_blueprint(audit_bp, url_prefix='/api/audit', name='audit_legacy')
|
||||
print("✅ Audit 模块注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Audit 模块导入失败: {e}")
|
||||
|
||||
# 2.9 初始化审计日志菜单和权限(防重复)
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.services.permission_service import PermissionService
|
||||
with app.app_context():
|
||||
PermissionService.init_audit_menu()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 审计日志菜单初始化跳过: {e}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.10 注册库位管理模块 (Warehouse)
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.api.v1.warehouse import warehouse_bp
|
||||
|
||||
122
inventory-backend/app/api/v1/audit.py
Normal file
122
inventory-backend/app/api/v1/audit.py
Normal file
@ -0,0 +1,122 @@
|
||||
# inventory-backend/app/api/v1/audit.py
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required, get_jwt
|
||||
from app.models.audit import AuditLog
|
||||
from app.extensions import db
|
||||
from sqlalchemy import or_
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
audit_bp = Blueprint('audit', __name__)
|
||||
|
||||
|
||||
@audit_bp.route('/logs', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_audit_logs():
|
||||
"""获取审计日志列表(分页)"""
|
||||
try:
|
||||
# 分页参数
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('pageSize', 50, type=int)
|
||||
|
||||
# 筛选参数
|
||||
username = request.args.get('username', '').strip()
|
||||
module = request.args.get('module', '').strip()
|
||||
action = request.args.get('action', '').strip()
|
||||
target_id = request.args.get('target_id', '').strip()
|
||||
start_date = request.args.get('start_date', '').strip()
|
||||
end_date = request.args.get('end_date', '').strip()
|
||||
|
||||
# 构建查询
|
||||
query = AuditLog.query
|
||||
|
||||
if username:
|
||||
query = query.filter(AuditLog.username.like(f'%{username}%'))
|
||||
if module:
|
||||
query = query.filter(AuditLog.module == module)
|
||||
if action:
|
||||
query = query.filter(AuditLog.action == action)
|
||||
if target_id:
|
||||
query = query.filter(AuditLog.target_id == target_id)
|
||||
if start_date:
|
||||
try:
|
||||
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
query = query.filter(AuditLog.created_at >= start_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
if end_date:
|
||||
try:
|
||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
||||
# 包含当天结束时间
|
||||
from datetime import timedelta
|
||||
end_dt = end_dt + timedelta(days=1)
|
||||
query = query.filter(AuditLog.created_at < end_dt)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 排序
|
||||
query = query.order_by(AuditLog.created_at.desc())
|
||||
|
||||
# 分页
|
||||
pagination = query.paginate(page=page, per_page=page_size, error_out=False)
|
||||
logs = pagination.items
|
||||
|
||||
# 序列化
|
||||
data = [log.to_dict() for log in logs]
|
||||
|
||||
# 获取可用的模块和操作类型(用于下拉选项)
|
||||
modules = db.session.query(AuditLog.module).distinct().all()
|
||||
modules = [m[0] for m in modules if m[0]]
|
||||
|
||||
actions = db.session.query(AuditLog.action).distinct().all()
|
||||
actions = [a[0] for a in actions if a[0]]
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
'data': {
|
||||
'list': data,
|
||||
'total': pagination.total,
|
||||
'page': page,
|
||||
'pageSize': page_size,
|
||||
'modules': modules,
|
||||
'actions': actions
|
||||
}
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取审计日志失败: {str(e)}")
|
||||
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||
|
||||
|
||||
@audit_bp.route('/logs/<int:log_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_audit_log_detail(log_id):
|
||||
"""获取单条审计日志详情"""
|
||||
try:
|
||||
log = AuditLog.query.get(log_id)
|
||||
if not log:
|
||||
return jsonify({'code': 404, 'msg': '日志不存在'}), 404
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
'data': log.to_dict()
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取审计日志详情失败: {str(e)}")
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
|
||||
@audit_bp.route('/modules', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_modules():
|
||||
"""获取所有模块列表(用于筛选)"""
|
||||
try:
|
||||
modules = db.session.query(AuditLog.module).distinct().all()
|
||||
modules = [m[0] for m in modules if m[0]]
|
||||
return jsonify({'code': 200, 'data': modules}), 200
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"获取模块列表失败: {str(e)}")
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
@ -2,7 +2,7 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required, get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
from app.utils.decorators import permission_required
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@ -107,6 +107,11 @@ def refresh():
|
||||
@auth_bp.route('/user/create', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user:operation')
|
||||
@audit_log(
|
||||
module='用户管理',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('username') if request.get_json() else None
|
||||
)
|
||||
def create_user():
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -151,6 +156,11 @@ def create_user():
|
||||
@auth_bp.route('/user/<int:user_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user:operation')
|
||||
@audit_log(
|
||||
module='用户管理',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('user_id')
|
||||
)
|
||||
def update_user(user_id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -207,6 +217,11 @@ def get_users():
|
||||
@auth_bp.route('/user/<int:user_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user:operation')
|
||||
@audit_log(
|
||||
module='用户管理',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('user_id')
|
||||
)
|
||||
def delete_user(user_id):
|
||||
try:
|
||||
claims = get_jwt()
|
||||
|
||||
@ -4,7 +4,7 @@ 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
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
bom_bp = Blueprint('bom', __name__)
|
||||
@ -109,6 +109,11 @@ def get_bom_detail(bom_no):
|
||||
@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:
|
||||
@ -191,6 +196,11 @@ def get_bom_with_stock_by_no(bom_no):
|
||||
@bom_bp.route('/<path:bom_no>', 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 编号删除
|
||||
@ -243,6 +253,11 @@ def get_bom(parent_id):
|
||||
@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()
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from flask import Blueprint, request, jsonify, send_file, g
|
||||
from app.services.inbound.base_service import MaterialBaseService
|
||||
from app.utils.decorators import login_required, permission_required
|
||||
from app.utils.decorators import login_required, permission_required, audit_log
|
||||
import traceback
|
||||
import datetime
|
||||
import json
|
||||
@ -197,6 +197,11 @@ def export_data():
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/', methods=['POST'])
|
||||
@permission_required('material_list:operation')
|
||||
@audit_log(
|
||||
module='基础信息管理',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('name') if request.get_json() else None
|
||||
)
|
||||
def create():
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -253,6 +258,12 @@ def create():
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('material_list:operation')
|
||||
@audit_log(
|
||||
module='基础信息管理',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('id'),
|
||||
get_target_name_fn=lambda: request.get_json().get('name') if request.get_json() else None
|
||||
)
|
||||
def update(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -302,6 +313,11 @@ def update(id):
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('material_list:operation')
|
||||
@audit_log(
|
||||
module='基础信息管理',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('id')
|
||||
)
|
||||
def delete(id):
|
||||
try:
|
||||
MaterialBaseService.delete_material(id)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.buy_service import BuyInboundService
|
||||
from app.utils.decorators import permission_required
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
import traceback
|
||||
|
||||
inbound_buy_bp = Blueprint('stock_buy', __name__)
|
||||
@ -155,6 +155,11 @@ def get_list():
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/submit', methods=['POST'])
|
||||
@permission_required('inbound_buy:operation')
|
||||
@audit_log(
|
||||
module='采购入库',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -224,6 +229,12 @@ def submit():
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('inbound_buy:operation')
|
||||
@audit_log(
|
||||
module='采购入库',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('id'),
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def update_buy(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -283,6 +294,11 @@ def update_buy(id):
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('inbound_buy:operation')
|
||||
@audit_log(
|
||||
module='采购入库',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('id')
|
||||
)
|
||||
def delete_buy(id):
|
||||
try:
|
||||
BuyInboundService.delete_inbound(id)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# inventory-backend/app/api/v1/inbound/product.py
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.product_service import ProductInboundService
|
||||
from app.utils.decorators import permission_required
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
import traceback
|
||||
|
||||
# === 这一行非常关键,绝对不能丢!===
|
||||
@ -123,6 +123,11 @@ def get_list():
|
||||
|
||||
@inbound_product_bp.route('/submit', methods=['POST'])
|
||||
@permission_required('inbound_product:operation')
|
||||
@audit_log(
|
||||
module='成品入库',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -141,6 +146,12 @@ def submit():
|
||||
|
||||
@inbound_product_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('inbound_product:operation')
|
||||
@audit_log(
|
||||
module='成品入库',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('id'),
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def update(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -158,6 +169,11 @@ def update(id):
|
||||
|
||||
@inbound_product_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('inbound_product:operation')
|
||||
@audit_log(
|
||||
module='成品入库',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('id')
|
||||
)
|
||||
def delete(id):
|
||||
try:
|
||||
ProductInboundService.delete_inbound(id)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# inventory-backend/app/api/v1/inbound/semi.py
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.semi_service import SemiInboundService
|
||||
from app.utils.decorators import permission_required
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
import traceback
|
||||
|
||||
# === 这一行非常关键,绝对不能丢!===
|
||||
@ -118,6 +118,11 @@ def get_list():
|
||||
|
||||
@inbound_semi_bp.route('/submit', methods=['POST'])
|
||||
@permission_required('inbound_semi:operation')
|
||||
@audit_log(
|
||||
module='半成品入库',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -136,6 +141,12 @@ def submit():
|
||||
|
||||
@inbound_semi_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('inbound_semi:operation')
|
||||
@audit_log(
|
||||
module='半成品入库',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('id'),
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def update_semi(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -153,6 +164,11 @@ def update_semi(id):
|
||||
|
||||
@inbound_semi_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('inbound_semi:operation')
|
||||
@audit_log(
|
||||
module='半成品入库',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('id')
|
||||
)
|
||||
def delete_semi(id):
|
||||
try:
|
||||
SemiInboundService.delete_inbound(id)
|
||||
|
||||
@ -3,7 +3,7 @@ from flask import request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
from . import inbound_bp
|
||||
from app.services.inbound.service_service import ServiceService
|
||||
from app.utils.decorators import role_required, permission_required
|
||||
from app.utils.decorators import role_required, permission_required, audit_log
|
||||
import traceback
|
||||
|
||||
|
||||
@ -112,6 +112,11 @@ def get_service_list():
|
||||
|
||||
@inbound_bp.route('/service', methods=['POST'])
|
||||
@permission_required('inbound_service:operation')
|
||||
@audit_log(
|
||||
module='服务权益',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def create_service():
|
||||
"""创建服务权益"""
|
||||
data = request.get_json()
|
||||
@ -188,6 +193,12 @@ def get_service(service_id):
|
||||
|
||||
@inbound_bp.route('/service/<int:service_id>', methods=['PUT'])
|
||||
@permission_required('inbound_service:operation')
|
||||
@audit_log(
|
||||
module='服务权益',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('service_id'),
|
||||
get_target_name_fn=lambda: request.get_json().get('material_name') if request.get_json() else None
|
||||
)
|
||||
def update_service(service_id):
|
||||
"""更新服务权益"""
|
||||
data = request.get_json()
|
||||
@ -247,6 +258,11 @@ def update_service(service_id):
|
||||
|
||||
@inbound_bp.route('/service/<int:service_id>', methods=['DELETE'])
|
||||
@permission_required('inbound_service:operation')
|
||||
@audit_log(
|
||||
module='服务权益',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('service_id')
|
||||
)
|
||||
def delete_service(service_id):
|
||||
"""删除服务权益"""
|
||||
try:
|
||||
|
||||
@ -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
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
from app.services.auth_service import AuthService
|
||||
import traceback
|
||||
|
||||
@ -107,6 +107,11 @@ def scan_barcode():
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@audit_log(
|
||||
module='出库管理',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('order_no') if request.get_json() else None
|
||||
)
|
||||
def create_outbound():
|
||||
# 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一
|
||||
claims = get_jwt()
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
from app.services.permission_service import PermissionService
|
||||
from app.utils.decorators import audit_log
|
||||
|
||||
permission_bp = Blueprint('permission', __name__)
|
||||
|
||||
@ -34,6 +35,11 @@ def get_role_perms(role_code):
|
||||
|
||||
@permission_bp.route('/assign', methods=['POST'])
|
||||
@jwt_required()
|
||||
@audit_log(
|
||||
module='权限管理',
|
||||
action='分配',
|
||||
get_target_name_fn=lambda: request.get_json().get('role_code') if request.get_json() else None
|
||||
)
|
||||
def assign_perms():
|
||||
"""保存权限分配"""
|
||||
try:
|
||||
|
||||
@ -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
|
||||
from app.utils.decorators import permission_required, audit_log
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.trans_service import TransService
|
||||
import traceback
|
||||
@ -59,6 +59,11 @@ def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records')
|
||||
@trans_bp.route('/borrow', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('op_borrow:operation')
|
||||
@audit_log(
|
||||
module='借库管理',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('borrow_no') if request.get_json() else None
|
||||
)
|
||||
def create_borrow():
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
@ -108,6 +113,11 @@ def scan_borrowed_item():
|
||||
@trans_bp.route('/return', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('op_return:operation')
|
||||
@audit_log(
|
||||
module='借库管理',
|
||||
action='归还',
|
||||
get_target_name_fn=lambda: request.get_json().get('borrow_no') if request.get_json() else None
|
||||
)
|
||||
def submit_return():
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
# inventory-backend/app/api/v1/warehouse.py
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import jwt_required
|
||||
from app.extensions import db
|
||||
from app.models.system import SysWarehouseLocation
|
||||
from app.utils.decorators import audit_log
|
||||
|
||||
warehouse_bp = Blueprint('warehouse', __name__, url_prefix='/api/v1/warehouse')
|
||||
|
||||
@ -49,6 +51,12 @@ def get_tree():
|
||||
|
||||
|
||||
@warehouse_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@audit_log(
|
||||
module='库位管理',
|
||||
action='新增',
|
||||
get_target_name_fn=lambda: request.get_json().get('name') if request.get_json() else None
|
||||
)
|
||||
def create_location():
|
||||
"""
|
||||
创建库位
|
||||
@ -100,6 +108,13 @@ def create_location():
|
||||
|
||||
|
||||
@warehouse_bp.route('/<int:location_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@audit_log(
|
||||
module='库位管理',
|
||||
action='修改',
|
||||
get_target_id_fn=lambda: request.view_args.get('location_id'),
|
||||
get_target_name_fn=lambda: request.get_json().get('name') if request.get_json() else None
|
||||
)
|
||||
def update_location(location_id):
|
||||
"""
|
||||
更新库位
|
||||
@ -144,6 +159,12 @@ def update_location(location_id):
|
||||
|
||||
|
||||
@warehouse_bp.route('/<int:location_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@audit_log(
|
||||
module='库位管理',
|
||||
action='删除',
|
||||
get_target_id_fn=lambda: request.view_args.get('location_id')
|
||||
)
|
||||
def delete_location(location_id):
|
||||
"""
|
||||
删除库位(级联删除子库位)
|
||||
|
||||
47
inventory-backend/app/models/audit.py
Normal file
47
inventory-backend/app/models/audit.py
Normal file
@ -0,0 +1,47 @@
|
||||
# inventory-backend/app/models/audit.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AuditLog(db.Model):
|
||||
"""
|
||||
操作审计日志表
|
||||
记录所有关键业务操作
|
||||
"""
|
||||
__tablename__ = 'audit_logs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, nullable=True, index=True) # 操作人ID(sys_user.id)
|
||||
username = db.Column(db.String(100), nullable=False, index=True) # 操作人账号
|
||||
display_name = db.Column(db.String(100)) # 操作人显示名称
|
||||
action = db.Column(db.String(50), nullable=False, index=True) # 操作类型: create/update/delete/export 等
|
||||
module = db.Column(db.String(50), nullable=False, index=True) # 业务模块: inbound_buy/inbound_semi/bom/user 等
|
||||
target_id = db.Column(db.String(100), index=True) # 被操作的数据ID
|
||||
target_name = db.Column(db.String(200)) # 被操作数据的显示名称
|
||||
details = db.Column(db.JSON) # 详细变更内容 {old: {}, new: {}}
|
||||
ip_address = db.Column(db.String(50)) # 操作IP
|
||||
user_agent = db.Column(db.String(500)) # 浏览器UA
|
||||
method = db.Column(db.String(10)) # HTTP方法
|
||||
url = db.Column(db.String(500)) # 请求URL
|
||||
status_code = db.Column(db.Integer) # 响应状态码
|
||||
error_message = db.Column(db.Text) # 错误信息(如有)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now, index=True) # 操作时间
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
'username': self.username,
|
||||
'display_name': self.display_name,
|
||||
'action': self.action,
|
||||
'module': self.module,
|
||||
'target_id': self.target_id,
|
||||
'target_name': self.target_name,
|
||||
'details': self.details,
|
||||
'ip_address': self.ip_address,
|
||||
'method': self.method,
|
||||
'url': self.url,
|
||||
'status_code': self.status_code,
|
||||
'error_message': self.error_message,
|
||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None
|
||||
}
|
||||
@ -147,3 +147,57 @@ class PermissionService:
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
@staticmethod
|
||||
def init_audit_menu():
|
||||
"""
|
||||
初始化审计日志菜单和超级管理员权限
|
||||
防重复:只插入不存在的记录
|
||||
"""
|
||||
try:
|
||||
# 1. 检查并创建审计日志菜单
|
||||
menu_code = 'system_audit'
|
||||
existing_menu = SysMenu.query.filter_by(code=menu_code).first()
|
||||
|
||||
if not existing_menu:
|
||||
new_menu = SysMenu(
|
||||
parent_id=0,
|
||||
name='审计日志',
|
||||
code=menu_code,
|
||||
path='/system/audit',
|
||||
sort_order=110,
|
||||
is_visible=True
|
||||
)
|
||||
db.session.add(new_menu)
|
||||
db.session.flush() # 获取新插入的 ID
|
||||
print(f"✅ 审计日志菜单已创建 (code: {menu_code})")
|
||||
else:
|
||||
print(f"ℹ️ 审计日志菜单已存在 (code: {menu_code})")
|
||||
|
||||
# 2. 为超级管理员赋予审计日志菜单权限
|
||||
role_code = 'SUPER_ADMIN'
|
||||
existing_perm = SysRolePermission.query.filter_by(
|
||||
role_code=role_code,
|
||||
target_code=menu_code
|
||||
).first()
|
||||
|
||||
if not existing_perm:
|
||||
new_perm = SysRolePermission(
|
||||
role_code=role_code,
|
||||
target_code=menu_code,
|
||||
type='menu'
|
||||
)
|
||||
db.session.add(new_perm)
|
||||
print(f"✅ 超级管理员已赋予审计日志权限")
|
||||
else:
|
||||
print(f"ℹ️ 超级管理员已拥有审计日志权限")
|
||||
|
||||
# 3. 提交
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"❌ 初始化审计日志菜单失败: {str(e)}")
|
||||
raise e
|
||||
|
||||
@ -275,6 +275,11 @@ class LabelPrintService:
|
||||
ip = config['ip']
|
||||
port = config['port']
|
||||
|
||||
# 获取打印份数,默认为 1
|
||||
copies = data.get('copies', 1)
|
||||
if copies < 1:
|
||||
copies = 1
|
||||
|
||||
try:
|
||||
# 1. 获取 RGB 图像
|
||||
img_rgb = LabelPrintService._create_image_object(data)
|
||||
@ -301,7 +306,7 @@ class LabelPrintService:
|
||||
|
||||
# 位图指令
|
||||
bitmap_cmd = f"BITMAP 0,0,{width_bytes},{height_dots},0,".encode('gbk')
|
||||
footer = b"\r\nPRINT 1,1\r\n"
|
||||
footer = f"\r\nPRINT 1,{copies}\r\n".encode('gbk')
|
||||
|
||||
# 5. 发送 socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
# app/utils/decorators.py
|
||||
from functools import wraps
|
||||
from flask_jwt_extended import get_jwt, verify_jwt_in_request
|
||||
from flask import jsonify, g
|
||||
from flask_jwt_extended import get_jwt, verify_jwt_in_request, get_jwt_identity
|
||||
from flask import jsonify, g, request
|
||||
import logging
|
||||
import json
|
||||
|
||||
|
||||
def role_required(*roles):
|
||||
@ -87,3 +88,104 @@ def permission_required(permission_code):
|
||||
return fn(*args, **kwargs)
|
||||
return decorator
|
||||
return wrapper
|
||||
|
||||
|
||||
def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target_name_fn=None, get_details_fn=None):
|
||||
"""
|
||||
审计日志装饰器
|
||||
用法: @audit_log(module='inbound_buy', action='create')
|
||||
@audit_log(module='bom', action='update', get_target_id_fn=lambda: ..., get_details_fn=lambda req, resp: ...)
|
||||
"""
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorator(*args, **kwargs):
|
||||
# 获取请求上下文
|
||||
claims = get_jwt()
|
||||
user_id = get_jwt_identity()
|
||||
username = claims.get('username', '')
|
||||
display_name = claims.get('display_name', '')
|
||||
|
||||
# 获取IP
|
||||
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
||||
if ip_address and ',' in ip_address:
|
||||
ip_address = ip_address.split(',')[0].strip()
|
||||
|
||||
# 获取请求信息
|
||||
http_method = request.method
|
||||
url = request.url
|
||||
user_agent = request.headers.get('User-Agent', '')[:500]
|
||||
|
||||
# 解析 action(支持动态)
|
||||
final_action = action
|
||||
if callable(action):
|
||||
final_action = action()
|
||||
|
||||
# 执行原函数
|
||||
response = fn(*args, **kwargs)
|
||||
|
||||
# 只记录成功的请求(响应状态码 200/201)
|
||||
status_code = 200
|
||||
if hasattr(response, 'status_code'):
|
||||
status_code = response.status_code
|
||||
|
||||
if status_code in [200, 201]:
|
||||
try:
|
||||
from app.models.audit import AuditLog
|
||||
from app.extensions import db
|
||||
from flask import current_app
|
||||
|
||||
# 获取 target_id
|
||||
target_id = None
|
||||
if get_target_id_fn:
|
||||
try:
|
||||
target_id = get_target_id_fn()
|
||||
except Exception:
|
||||
pass
|
||||
if not target_id and hasattr(response, 'json'):
|
||||
resp_data = response.get_json()
|
||||
if resp_data and isinstance(resp_data, dict):
|
||||
target_id = resp_data.get('id')
|
||||
|
||||
# 获取 target_name
|
||||
target_name = None
|
||||
if get_target_name_fn:
|
||||
try:
|
||||
target_name = get_target_name_fn()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 获取 details
|
||||
details = None
|
||||
if get_details_fn:
|
||||
try:
|
||||
details = get_details_fn(request, response)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 保存日志
|
||||
log_entry = AuditLog(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
action=final_action or http_method.lower(),
|
||||
module=module,
|
||||
target_id=str(target_id) if target_id else None,
|
||||
target_name=target_name,
|
||||
details=details,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
method=http_method,
|
||||
url=url,
|
||||
status_code=status_code
|
||||
)
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"审计日志记录失败: {str(e)}")
|
||||
db.session.rollback()
|
||||
|
||||
return response
|
||||
|
||||
return decorator
|
||||
return wrapper
|
||||
|
||||
26
inventory-web/src/api/audit.ts
Normal file
26
inventory-web/src/api/audit.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取审计日志列表
|
||||
export function getAuditLogs(params: any) {
|
||||
return request({
|
||||
url: '/audit/logs',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取审计日志详情
|
||||
export function getAuditLogDetail(logId: number) {
|
||||
return request({
|
||||
url: `/audit/logs/${logId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取可选模块列表
|
||||
export function getAuditModules() {
|
||||
return request({
|
||||
url: '/audit/modules',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
45
inventory-web/src/directives/permission.ts
Normal file
45
inventory-web/src/directives/permission.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { Directive, DirectiveBinding } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
/**
|
||||
* v-permission 指令
|
||||
* 用法: v-permission="'inbound_buy:delete'" 或 v-permission="['inbound_buy:delete', 'inbound_buy:edit']"
|
||||
* 支持数组形式(满足任一权限即显示)和字符串形式
|
||||
*/
|
||||
const permissionDirective: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const userStore = useUserStore()
|
||||
const value = binding.value
|
||||
|
||||
// 没有绑定值,不做任何处理
|
||||
if (value === undefined || value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析权限码数组
|
||||
let permissionCodes: string[] = []
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// 字符串形式: "inbound_buy:delete"
|
||||
permissionCodes = [value]
|
||||
} else if (Array.isArray(value)) {
|
||||
// 数组形式: ['inbound_buy:delete', 'inbound_buy:edit']
|
||||
permissionCodes = value
|
||||
}
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if (userStore.role === 'SUPER_ADMIN') {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有任意一个权限码
|
||||
const hasAuth = permissionCodes.some((code) => userStore.hasPermission(code))
|
||||
|
||||
if (!hasAuth) {
|
||||
// 没有权限,从 DOM 中移除该元素
|
||||
el.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default permissionDirective
|
||||
@ -17,6 +17,9 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
// 4. 引入全局样式 (通常建议加上,如果没有可忽略)
|
||||
import './style.css'
|
||||
|
||||
// 5. 引入全局自定义指令
|
||||
import permissionDirective from './directives/permission'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// =========================================================
|
||||
@ -41,4 +44,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
// 5. 注册全局自定义指令
|
||||
app.directive('permission', permissionDirective)
|
||||
|
||||
app.mount('#app')
|
||||
@ -209,6 +209,17 @@ const routes: Array<RouteRecordRaw> = [
|
||||
icon: 'Lock',
|
||||
roles: ['SUPER_ADMIN']
|
||||
}
|
||||
},
|
||||
// [新增] 审计日志页面,只有超级管理员可进
|
||||
{
|
||||
path: 'audit',
|
||||
name: 'AuditLog',
|
||||
component: () => import('@/views/system/AuditLog.vue'),
|
||||
meta: {
|
||||
title: '审计日志',
|
||||
icon: 'Document',
|
||||
roles: ['SUPER_ADMIN']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -13,7 +13,21 @@ const service = axios.create({
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 2. 无感刷新 Token 核心逻辑
|
||||
// 2. 防重复提交 - Pending 请求池
|
||||
// ============================================================
|
||||
const pendingRequests = new Map<string, AbortController>()
|
||||
|
||||
// 生成唯一请求 Key:方法 + URL + 序列化参数
|
||||
const generateRequestKey = (config: InternalAxiosRequestConfig): string => {
|
||||
const method = (config.method || 'get').toLowerCase()
|
||||
const url = config.url || ''
|
||||
const params = config.params ? JSON.stringify(config.params) : ''
|
||||
const data = config.data ? JSON.stringify(config.data) : ''
|
||||
return `${method}:${url}:${params}:${data}`
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. 无感刷新 Token 核心逻辑
|
||||
// ============================================================
|
||||
|
||||
// 标记是否正在刷新 Token
|
||||
@ -54,17 +68,37 @@ const refreshToken = async (refreshTokenValue: string): Promise<string> => {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3. 请求拦截器
|
||||
// 4. 请求拦截器(添加 Token + 防重复提交)
|
||||
// ============================================================
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么
|
||||
// 1. 添加 Token
|
||||
const token = localStorage.getItem('access_token') || localStorage.getItem('token')
|
||||
|
||||
if (token && config.headers) {
|
||||
// Flask-JWT-Extended 默认需要 'Bearer <token>' 格式
|
||||
config.headers['Authorization'] = 'Bearer ' + token
|
||||
}
|
||||
|
||||
// 2. 防重复提交检查
|
||||
const requestKey = generateRequestKey(config)
|
||||
|
||||
// 排除一些不需要防重复的请求(如查询类 GET 请求可以根据需求调整)
|
||||
const ignoreMethods = ['get', 'head']
|
||||
if (!ignoreMethods.includes((config.method || 'get').toLowerCase())) {
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
// 取消之前的请求
|
||||
const controller = pendingRequests.get(requestKey)
|
||||
controller?.abort('正在处理中,请勿重复操作')
|
||||
pendingRequests.delete(requestKey)
|
||||
console.warn(`[防重复] 取消重复请求: ${requestKey}`)
|
||||
}
|
||||
|
||||
// 创建新的 AbortController 并存储
|
||||
const controller = new AbortController()
|
||||
config.signal = controller.signal
|
||||
pendingRequests.set(requestKey, controller)
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
@ -73,10 +107,16 @@ service.interceptors.request.use(
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 4. 响应拦截器(核心:无感刷新)
|
||||
// 5. 响应拦截器(核心:无感刷新 + 清理 pending)
|
||||
// ============================================================
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
// 清理 pending 请求池
|
||||
const requestKey = generateRequestKey(response.config)
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
pendingRequests.delete(requestKey)
|
||||
}
|
||||
|
||||
// Axios 默认包了一层 data,所以这里取 response.data
|
||||
const res = response.data
|
||||
|
||||
@ -94,6 +134,14 @@ service.interceptors.response.use(
|
||||
async (error: AxiosError) => {
|
||||
console.log('err: ' + error) // for debug
|
||||
|
||||
// 清理 pending 请求池(无论成功还是失败都要清理)
|
||||
if (error.config) {
|
||||
const requestKey = generateRequestKey(error.config)
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
pendingRequests.delete(requestKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是 axios 错误,直接抛出
|
||||
if (!error.response) {
|
||||
return Promise.reject(error)
|
||||
|
||||
@ -219,7 +219,7 @@
|
||||
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="default">删除</el-button>
|
||||
<el-button link type="danger" size="default" v-permission="'inbound_buy:delete'">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
276
inventory-web/src/views/system/AuditLog.vue
Normal file
276
inventory-web/src/views/system/AuditLog.vue
Normal file
@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span style="font-weight: bold;">操作审计日志</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<el-form :inline="true" :model="queryParams" class="search-form">
|
||||
<el-form-item label="操作人">
|
||||
<el-input v-model="queryParams.username" placeholder="请输入操作人账号" clearable @keyup.enter="handleQuery" style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模块">
|
||||
<el-select v-model="queryParams.module" placeholder="请选择模块" clearable style="width: 150px">
|
||||
<el-option v-for="item in moduleOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="queryParams.action" placeholder="请选择操作类型" clearable style="width: 120px">
|
||||
<el-option v-for="item in actionOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :loading="tableLoading">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table v-loading="tableLoading" :data="tableData" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="username" label="操作人" width="120" />
|
||||
<el-table-column prop="display_name" label="姓名" width="100" />
|
||||
<el-table-column prop="module" label="模块" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.module }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="action" label="操作" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getActionType(scope.row.action)">{{ scope.row.action }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="target_name" label="操作对象" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="ip_address" label="IP地址" width="130" />
|
||||
<el-table-column prop="created_at" label="操作时间" width="170" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.details" link type="primary" size="small" @click="handleViewDetails(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<span v-else class="text-gray">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[20, 50, 100, 200]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
style="margin-top: 20px"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="操作详情" width="700px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ currentLog.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">{{ currentLog.username }} ({{ currentLog.display_name }})</el-descriptions-item>
|
||||
<el-descriptions-item label="模块">{{ currentLog.module }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作类型">
|
||||
<el-tag :type="getActionType(currentLog.action)">{{ currentLog.action }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作对象" :span="2">{{ currentLog.target_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">{{ currentLog.ip_address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方式">{{ currentLog.method }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间" :span="2">{{ currentLog.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求URL" :span="2">
|
||||
<el-text size="small">{{ currentLog.url }}</el-text>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="currentLog.details" class="details-box">
|
||||
<div class="details-title">变更内容 (JSON)</div>
|
||||
<pre class="json-content">{{ formatDetails(currentLog.details) }}</pre>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getAuditLogs, getAuditModules } from '@/api/audit'
|
||||
|
||||
// 表格数据
|
||||
const tableLoading = ref(false)
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
username: '',
|
||||
module: '',
|
||||
action: '',
|
||||
target_id: '',
|
||||
start_date: '',
|
||||
end_date: ''
|
||||
})
|
||||
|
||||
// 日期范围
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
|
||||
// 选项数据
|
||||
const moduleOptions = ref<string[]>([])
|
||||
const actionOptions = ref<string[]>(['create', 'update', 'delete', 'export', 'import'])
|
||||
|
||||
// 详情弹窗
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentLog = ref<any>({})
|
||||
|
||||
// 获取操作类型对应的标签样式
|
||||
const getActionType = (action: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'create': 'success',
|
||||
'add': 'success',
|
||||
'update': 'warning',
|
||||
'edit': 'warning',
|
||||
'delete': 'danger',
|
||||
'export': 'info',
|
||||
'import': 'info'
|
||||
}
|
||||
return typeMap[action?.toLowerCase()] || 'info'
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const getList = async () => {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
// 处理日期范围
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.start_date = dateRange.value[0]
|
||||
queryParams.end_date = dateRange.value[1]
|
||||
} else {
|
||||
queryParams.start_date = ''
|
||||
queryParams.end_date = ''
|
||||
}
|
||||
|
||||
const res = await getAuditLogs(queryParams)
|
||||
if (res.code === 200) {
|
||||
tableData.value = res.data.list
|
||||
total.value = res.data.total
|
||||
// 更新选项
|
||||
if (res.data.modules) {
|
||||
moduleOptions.value = res.data.modules
|
||||
}
|
||||
if (res.data.actions) {
|
||||
actionOptions.value = res.data.actions
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取审计日志失败:', error)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleQuery = () => {
|
||||
queryParams.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.page = 1
|
||||
queryParams.username = ''
|
||||
queryParams.module = ''
|
||||
queryParams.action = ''
|
||||
queryParams.target_id = ''
|
||||
queryParams.start_date = ''
|
||||
queryParams.end_date = ''
|
||||
dateRange.value = null
|
||||
getList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetails = (row: any) => {
|
||||
currentLog.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 格式化详情 JSON
|
||||
const formatDetails = (details: any) => {
|
||||
if (!details) return ''
|
||||
if (typeof details === 'string') {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(details), null, 2)
|
||||
} catch {
|
||||
return details
|
||||
}
|
||||
}
|
||||
return JSON.stringify(details, null, 2)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getList()
|
||||
// 获取可选模块
|
||||
getAuditModules().then(res => {
|
||||
if (res.code === 200 && res.data) {
|
||||
moduleOptions.value = res.data
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.details-box {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.details-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user