新手进阶Python:办公看板集成审批流+精细化权限+日志审计

大家好!我是CSDN的Python新手博主~ 上一篇我们完成了看板的多数据源整合与ECharts交互式可视化,提升了数据探查能力,但很多企业用户反馈三个核心痛点:① 缺乏审批流程,权限申请、敏感数据导出等操作无管控,易造成数据泄露;② 权限管控颗粒度粗,仅按"管理员/部门负责人/普通员工"分级,无法实现"某员工仅看本部门某类数据"的精细化需求;③ 无操作日志追溯,数据修改、导出、权限变更等行为无法审计,不符合企业合规要求。今天就带来超落地的新手实战项目------办公看板集成智能审批流+精细化权限管控+操作日志审计!

本次基于之前的"多源数据可视化看板"代码,新增4大核心功能:① 企业微信审批流集成(支持权限申请、数据导出、数据修改三类审批,自动推送审批通知);② 精细化权限管控(按"数据维度+功能维度"双重授权,支持自定义权限规则);③ 全链路操作日志审计(记录用户登录、数据查看、导出、审批等所有行为,支持日志查询与导出);④ 合规校验(审批流程未通过时禁止敏感操作,日志留存满足合规追溯要求)。全程基于现有技术栈(Flask+MySQL+企业微信API),新增审批模型、权限规则引擎、日志装饰器,代码注释详细,新手只需配置审批模板与权限规则,跟着步骤复制操作就能成功,让办公看板完全适配企业合规办公场景~

一、本次学习目标

  1. 掌握企业微信审批流API的调用方法,实现审批模板创建、审批单发起、审批结果回调全流程;

  2. 理解精细化权限管控逻辑,实现"数据维度(部门/日期/字段)+功能维度(查看/导出/修改)"双重授权;

  3. 学会用Python装饰器封装日志记录功能,实现全链路操作日志的自动采集、存储与查询;

  4. 掌握合规校验逻辑,确保敏感操作必须经过审批,操作行为可追溯、可审计;

  5. 实现审批、权限、日志与现有功能(AI分析、多源数据、可视化)的无缝衔接,不影响原有业务流程。

二、前期准备

  1. 安装核心依赖库

安装核心依赖(sqlalchemy用于审批/日志数据存储,python-jose用于权限令牌)

pip3 install sqlalchemy python-jose python-multipart -i https://pypi.tuna.tsinghua.edu.cn/simple

确保已有依赖正常(Flask、requests、pandas等)

pip3 install --upgrade flask flask-login gunicorn requests pandas pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple

说明:企业微信审批流无需额外安装依赖,通过requests调用API即可;权限管控基于Flask-Login扩展,新增规则引擎逻辑;日志记录用Python装饰器实现,轻量化无侵入。

  1. 企业微信审批模板配置
  • 登录企业微信后台,进入「应用管理」→「审批」→「创建模板」;

  • 创建3个模板(对应三类审批):

    1. 权限申请模板:字段含"申请人、申请部门、申请权限类型(数据查看/导出)、具体权限范围(部门/字段)、申请理由";

    2. 数据导出模板:字段含"申请人、导出数据日期范围、导出字段、导出用途、接收邮箱";

    3. 数据修改模板:字段含"申请人、修改数据日期、修改内容(原数据/新数据)、修改理由"。

  • 记录每个模板的「模板ID(template_id)」,配置审批流程(如普通员工申请→部门负责人审批→管理员终审);

  • 设置审批回调地址(服务器公网IP/域名+接口路径,如"http://47.108.xxx.xxx/wecom/approval/callback"),用于接收审批结果。

  1. 数据库表设计与创建

-- 连接MySQL数据库(替换为你的数据库信息)

mysql -u office_user -p -h 47.108.xxx.xxx office_data

-- 创建审批单表(approval)

CREATE TABLE approval (

id INT AUTO_INCREMENT PRIMARY KEY,

approval_no VARCHAR(50) NOT NULL COMMENT '审批单号',

template_id VARCHAR(50) NOT NULL COMMENT '企业微信审批模板ID',

applicant VARCHAR(50) NOT NULL COMMENT '申请人姓名',

applicant_wecom_id VARCHAR(50) NOT NULL COMMENT '申请人企业微信ID',

approval_type ENUM('permission', 'export', 'modify') NOT NULL COMMENT '审批类型:权限申请/导出/修改',

content JSON NOT NULL COMMENT '审批内容(JSON格式)',

status ENUM('pending', 'approved', 'rejected', 'canceled') DEFAULT 'pending' COMMENT '审批状态',

approve_time DATETIME NULL COMMENT '审批通过时间',

reject_reason VARCHAR(200) NULL COMMENT '驳回理由',

create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

UNIQUE KEY uk_approval_no (approval_no)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批单表';

-- 创建权限规则表(permission_rule)

CREATE TABLE permission_rule (

id INT AUTO_INCREMENT PRIMARY KEY,

username VARCHAR(50) NOT NULL COMMENT '用户名',

data_dimension JSON NOT NULL COMMENT '数据维度权限(如{"dept":["销售一部"],"fields":["sales","name"]})',

func_dimension JSON NOT NULL COMMENT '功能维度权限(如{"view":true,"export":true,"modify":false})',

effective_time DATETIME NOT NULL COMMENT '生效时间',

expire_time DATETIME NULL COMMENT '过期时间(NULL表示永久有效)',

approval_id INT NULL COMMENT '关联审批单ID',

create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

UNIQUE KEY uk_username (username),

KEY idx_approval_id (approval_id)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限规则表';

-- 创建操作日志表(operation_log)

CREATE TABLE operation_log (

id INT AUTO_INCREMENT PRIMARY KEY,

username VARCHAR(50) NOT NULL COMMENT '操作人姓名',

user_role VARCHAR(20) NOT NULL COMMENT '操作人角色',

operation_type ENUM('login', 'view', 'export', 'modify', 'approve', 'permission') NOT NULL COMMENT '操作类型',

operation_content JSON NOT NULL COMMENT '操作内容(JSON格式)',

operation_result ENUM('success', 'fail') NOT NULL COMMENT '操作结果',

ip_address VARCHAR(50) NOT NULL COMMENT '操作IP地址',

user_agent TEXT NULL COMMENT '用户终端信息',

operation_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',

KEY idx_username (username),

KEY idx_operation_time (operation_time),

KEY idx_operation_type (operation_type)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

三、实战:审批流+权限管控+日志审计集成

  1. 第一步:封装企业微信审批流API,实现审批全流程

-- coding: utf-8 --

approval.py 企业微信审批流脚本

import requests

import json

import uuid

from datetime import datetime

from flask import Blueprint, request, jsonify

from dotenv import load_dotenv

import os

from models import db, Approval # 后续创建ORM模型

from wecom_login import get_wechat_access_token # 复用之前的access_token获取函数

加载环境变量

load_dotenv()

approval_bp = Blueprint("approval", name)

====================== 审批配置(新手修改这里) ======================

CORP_ID = os.getenv("CORP_ID")

APP_SECRET = os.getenv("WECOM_APP_SECRET") # 复用企业微信应用Secret

审批模板ID(对应权限申请、导出、修改三类模板)

TEMPLATE_IDS = {

"permission": "xxxxxxxxxxxxxxxxxxxx", # 权限申请模板ID

"export": "xxxxxxxxxxxxxxxxxxxx", # 数据导出模板ID

"modify": "xxxxxxxxxxxxxxxxxxxx" # 数据修改模板ID

}

审批回调token(企业微信后台配置,用于验证回调合法性)

APPROVAL_CALLBACK_TOKEN = os.getenv("APPROVAL_CALLBACK_TOKEN")

APPROVAL_ENCODING_AES_KEY = os.getenv("APPROVAL_ENCODING_AES_KEY")

企业微信审批API地址

APPROVAL_CREATE_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent"

APPROVAL_GET_DETAIL_URL = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovalinfo"

@approval_bp.route("/approval/create", methods=["POST"])

def create_approval():

"""发起审批单(支持权限申请、导出、修改三类)"""

data = request.get_json()

校验必填参数

required_params = ["username", "wecom_userid", "approval_type", "content"]

for param in required_params:

if param not in data:

return jsonify({"success": False, "error": f"缺少必填参数:{param}"}), 400

复制代码
username = data["username"]
wecom_userid = data["wecom_userid"]
approval_type = data["approval_type"]
content = data["content"]

# 校验审批类型与模板ID
if approval_type not in TEMPLATE_IDS:
    return jsonify({"success": False, "error": "审批类型错误(仅支持permission/export/modify)"}), 400
template_id = TEMPLATE_IDS[approval_type]

# 生成唯一审批单号
approval_no = f"APV-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:8].upper()}"

# 构建企业微信审批单参数
access_token = get_wechat_access_token()
if not access_token:
    return jsonify({"success": False, "error": "获取企业微信access_token失败"}), 500

approval_params = {
    "creator_userid": wecom_userid,
    "template_id": template_id,
    "use_template_approver": 1,  # 使用模板配置的审批人
    "approvers": [],  # 若不使用模板审批人,可手动指定
    "apply_data": {
        "contents": [
            # 按模板字段构建内容(对应企业微信审批模板字段)
            {"title": "申请人", "value": username},
            {"title": "申请时间", "value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
            # 动态添加审批内容(根据审批类型)
            *[{"title": k, "value": str(v)} for k, v in content.items()]
        ]
    },
    "summary_list": [
        {"summary_info": f"{get_approval_type_name(approval_type)}", "details": [f"{k}:{v}" for k, v in content.items()][:3]}
    ]
}

# 调用企业微信API发起审批
try:
    response = requests.post(
        f"{APPROVAL_CREATE_URL}?access_token={access_token}",
        json=approval_params
    )
    result = response.json()
    if result.get("errcode") != 0:
        return jsonify({"success": False, "error": f"发起审批失败:{result.get('errmsg')}"}), 500
    
    # 存储审批单到数据库
    new_approval = Approval(
        approval_no=approval_no,
        template_id=template_id,
        applicant=username,
        applicant_wecom_id=wecom_userid,
        approval_type=approval_type,
        content=json.dumps(content),
        status="pending"
    )
    db.session.add(new_approval)
    db.session.commit()
    
    return jsonify({
        "success": True,
        "msg": "审批单发起成功",
        "data": {
            "approval_no": approval_no,
            "approval_url": result.get("url")  # 审批单链接(可发送给申请人)
        }
    })
except Exception as e:
    db.session.rollback()
    return jsonify({"success": False, "error": f"发起审批异常:{str(e)}"}), 500

@approval_bp.route("/wecom/approval/callback", methods=["POST"])

def approval_callback():

"""企业微信审批结果回调接口"""

验证回调合法性(省略AES解密逻辑,企业微信官方提供解密示例)

实际生产环境需按企业微信文档实现解密与签名验证

callback_data = request.get_json()

if callback_data.get("Event") != "approve":

return "success" # 忽略非审批事件

复制代码
# 解析审批结果
approval_info = callback_data.get("ApprovalInfo")
approval_no = approval_info.get("ThirdNo")  # 第三方审批单号(即我们生成的approval_no)
status = approval_info.get("SpStatus")  # 审批状态:1-通过,2-驳回,3-取消
approve_time = datetime.fromtimestamp(approval_info.get("SpTime"))
reject_reason = approval_info.get("ApproverComments", [{}])[0].get("CommentContent", "")

# 映射审批状态
status_map = {1: "approved", 2: "rejected", 3: "canceled"}
new_status = status_map.get(status, "pending")

# 更新数据库审批单状态
approval = Approval.query.filter_by(approval_no=approval_no).first()
if not approval:
    return "success"  # 未找到对应审批单,忽略

approval.status = new_status
approval.approve_time = approve_time if new_status == "approved" else None
approval.reject_reason = reject_reason if new_status == "rejected" else None
db.session.commit()

# 审批通过后执行对应操作(权限开通/导出授权/修改允许)
if new_status == "approved":
    if approval.approval_type == "permission":
        # 权限申请通过,创建权限规则
        create_permission_rule(approval)
    elif approval.approval_type == "export":
        # 数据导出通过,生成导出授权令牌
        create_export_token(approval)
    elif approval.approval_type == "modify":
        # 数据修改通过,允许修改操作
        create_modify_permit(approval)

return "success"  # 必须返回"success",否则企业微信会重复回调

@approval_bp.route("/approval/status", methods=["GET"])

def get_approval_status():

"""查询审批单状态"""

approval_no = request.args.get("approval_no")

if not approval_no:

return jsonify({"success": False, "error": "缺少审批单号"}), 400

复制代码
approval = Approval.query.filter_by(approval_no=approval_no).first()
if not approval:
    return jsonify({"success": False, "error": "未找到对应审批单"}), 404

return jsonify({
    "success": True,
    "data": {
        "approval_no": approval.approval_no,
        "approval_type": get_approval_type_name(approval.approval_type),
        "status": approval.status,
        "status_text": {"pending": "审批中", "approved": "已通过", "rejected": "已驳回", "canceled": "已取消"}[approval.status],
        "approve_time": approval.approve_time.strftime("%Y-%m-%d %H:%M:%S") if approval.approve_time else None,
        "reject_reason": approval.reject_reason
    }
})

====================== 辅助函数 ======================

def get_approval_type_name(approval_type):

"""获取审批类型中文名称"""

type_map = {"permission": "权限申请", "export": "数据导出申请", "modify": "数据修改申请"}

return type_map.get(approval_type, "未知审批类型")

def create_permission_rule(approval):

"""权限申请通过后,创建权限规则"""

from models import PermissionRule

content = json.loads(approval.content)

构建权限规则(数据维度+功能维度)

data_dimension = {

"dept": content.get("dept", []), # 可访问的部门

"fields": content.get("fields", []), # 可访问的字段

"date_range": content.get("date_range", []) # 可访问的日期范围

}

func_dimension = {

"view": content.get("permission_type") in ["view", "export"], # 查看权限

"export": content.get("permission_type") == "export", # 导出权限

"modify": False # 权限申请暂不开放修改权限

}

生效时间(审批通过时间),过期时间(若申请时指定则设置,否则永久)

effective_time = approval.approve_time

expire_time = datetime.strptime(content.get("expire_time"), "%Y-%m-%d") if content.get("expire_time") else None

复制代码
# 存储权限规则
new_rule = PermissionRule(
    username=approval.applicant,
    data_dimension=json.dumps(data_dimension),
    func_dimension=json.dumps(func_dimension),
    effective_time=effective_time,
    expire_time=expire_time,
    approval_id=approval.id
)
db.session.add(new_rule)
db.session.commit()

def create_export_token(approval):

"""数据导出通过后,生成导出授权令牌(有效期24小时)"""

from jose import jwt

from datetime import timedelta

content = json.loads(approval.content)

生成JWT令牌(包含导出权限信息)

export_token = jwt.encode(

{

"username": approval.applicant,

"date_range": content.get("date_range"),

"fields": content.get("fields"),

"exp": datetime.utcnow() + timedelta(hours=24) # 有效期24小时

},

os.getenv("JWT_SECRET_KEY"), # 自定义JWT密钥,在.env中配置

algorithm="HS256"

)

存储令牌到审批单扩展字段(或单独创建表)

approval.content = json.dumps({**content, "export_token": export_token})

db.session.commit()

def create_modify_permit(approval):

"""数据修改通过后,生成修改授权(有效期12小时)"""

content = json.loads(approval.content)

modify_token = str(uuid.uuid4())

存储修改授权令牌(有效期12小时)

approval.content = json.dumps({**content, "modify_token": modify_token, "modify_expire": (datetime.now() + timedelta(hours=12)).strftime("%Y-%m-%d %H:%M:%S")})

db.session.commit()

测试审批功能

if name == "main ":

需初始化Flask app与数据库连接,此处省略

pass

  1. 第二步:创建ORM模型,关联数据库表

-- coding: utf-8 --

models.py 新增ORM模型(补充到原有User模型之后)

from flask_sqlalchemy import SQLAlchemy

from datetime import datetime

import json

db = SQLAlchemy()

原有User模型保留(省略重复代码)

...

审批单模型(对应approval表)

class Approval(db.Model):
tablename = "approval"

id = db.Column(db.Integer, primary_key=True)

approval_no = db.Column(db.String(50), unique=True, nullable=False, comment="审批单号")

template_id = db.Column(db.String(50), nullable=False, comment="企业微信审批模板ID")

applicant = db.Column(db.String(50), nullable=False, comment="申请人姓名")

applicant_wecom_id = db.Column(db.String(50), nullable=False, comment="申请人企业微信ID")

approval_type = db.Column(db.Enum("permission", "export", "modify"), nullable=False, comment="审批类型")

content = db.Column(db.Text, nullable=False, comment="审批内容(JSON格式)")

status = db.Column(db.Enum("pending", "approved", "rejected", "canceled"), default="pending", comment="审批状态")

approve_time = db.Column(db.DateTime, nullable=True, comment="审批通过时间")

reject_reason = db.Column(db.String(200), nullable=True, comment="驳回理由")

create_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")

update_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")

复制代码
# 辅助方法:解析content为字典
def get_content(self):
    return json.loads(self.content) if self.content else {}

权限规则模型(对应permission_rule表)

class PermissionRule(db.Model):
tablename = "permission_rule"

id = db.Column(db.Integer, primary_key=True)

username = db.Column(db.String(50), unique=True, nullable=False, comment="用户名")

data_dimension = db.Column(db.Text, nullable=False, comment="数据维度权限(JSON格式)")

func_dimension = db.Column(db.Text, nullable=False, comment="功能维度权限(JSON格式)")

effective_time = db.Column(db.DateTime, nullable=False, comment="生效时间")

expire_time = db.Column(db.DateTime, nullable=True, comment="过期时间")

approval_id = db.Column(db.Integer, db.ForeignKey("approval.id"), nullable=True, comment="关联审批单ID")

create_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")

update_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")

复制代码
# 辅助方法:解析权限规则
def get_data_dimension(self):
    return json.loads(self.data_dimension) if self.data_dimension else {}

def get_func_dimension(self):
    return json.loads(self.func_dimension) if self.func_dimension else {}

# 校验权限是否有效
def is_valid(self):
    now = datetime.now()
    if now < self.effective_time:
        return False
    if self.expire_time and now > self.expire_time:
        return False
    return True

操作日志模型(对应operation_log表)

class OperationLog(db.Model):
tablename = "operation_log"

id = db.Column(db.Integer, primary_key=True)

username = db.Column(db.String(50), nullable=False, comment="操作人姓名")

user_role = db.Column(db.String(20), nullable=False, comment="操作人角色")

operation_type = db.Column(db.Enum("login", "view", "export", "modify", "approve", "permission"), nullable=False, comment="操作类型")

operation_content = db.Column(db.Text, nullable=False, comment="操作内容(JSON格式)")

operation_result = db.Column(db.Enum("success", "fail"), nullable=False, comment="操作结果")

ip_address = db.Column(db.String(50), nullable=False, comment="操作IP地址")

user_agent = db.Column(db.Text, nullable=True, comment="用户终端信息")

operation_time = db.Column(db.DateTime, default=datetime.now, comment="操作时间")

复制代码
# 辅助方法:解析操作内容
def get_operation_content(self):
    return json.loads(self.operation_content) if self.operation_content else {}
  1. 第三步:实现精细化权限管控,封装权限校验装饰器

-- coding: utf-8 --

permission.py 精细化权限管控脚本

from functools import wraps

from flask import request, jsonify, g

from flask_login import current_user

from models import PermissionRule

from datetime import datetime

import json

====================== 权限校验核心函数 ======================

def get_user_permission(username):

"""获取用户的有效权限规则"""

rule = PermissionRule.query.filter_by(username=username).first()

if not rule or not rule.is_valid():

无有效权限规则,返回默认权限(仅查看本人数据,无导出/修改权限)

return {

"data_dimension": {"dept": [], "fields": ["date", "name", "dept", "sales"], "date_range": []},

"func_dimension": {"view": True, "export": False, "modify": False}

}

return {

"data_dimension": rule.get_data_dimension(),

"func_dimension": rule.get_func_dimension()

}

def check_data_permission(data_df, username):

"""按数据维度权限过滤数据"""

permission = get_user_permission(username)

data_dim = permission["data_dimension"]

user_role = current_user.role

复制代码
# 1. 按部门过滤(管理员可查看所有部门,部门负责人可查看本部门,普通员工按权限)
if user_role == "leader":
    filtered_df = data_df
elif user_role == "dept":
    filtered_df = data_df[data_df["dept"] == current_user.dept]
else:
    dept_list = data_dim["dept"]
    if dept_list:
        filtered_df = data_df[data_df["dept"].isin(dept_list)]
    else:
        filtered_df = data_df[data_df["name"] == username]

# 2. 按字段过滤(仅保留授权字段)
field_list = data_dim["fields"]
if field_list and field_list != ["all"]:
    filtered_df = filtered_df[field_list]

# 3. 按日期范围过滤
date_range = data_dim["date_range"]
if date_range and len(date_range) == 2:
    start_date = datetime.strptime(date_range[0], "%Y-%m-%d")
    end_date = datetime.strptime(date_range[1], "%Y-%m-%d")
    filtered_df["date"] = datetime.strptime(filtered_df["date"], "%Y-%m-%d")
    filtered_df = filtered_df[(filtered_df["date"] >= start_date) & (filtered_df["date"] <= end_date)]
    filtered_df["date"] = filtered_df["date"].dt.strftime("%Y-%m-%d")

return filtered_df

def check_func_permission(func_type, username):

"""检查功能维度权限(view/export/modify)"""

permission = get_user_permission(username)

func_dim = permission["func_dimension"]

管理员默认拥有所有功能权限

if current_user.role == "leader":

return True

return func_dim.get(func_type, False)

====================== 权限校验装饰器 ======================

def permission_required(func_type):

"""功能权限校验装饰器(用于接口)"""

def decorator(f):

@wraps(f)

def wrapper(*args, **kwargs):

校验用户是否登录

if not current_user.is_authenticated:

return jsonify({"success": False, "error": "请先登录"}), 401

校验功能权限

if not check_func_permission(func_type, current_user.name):

return jsonify({"success": False, "error": f"无{get_func_name(func_type)}权限,请先发起审批"}), 403

权限通过,执行原函数

return f(*args, **kwargs)

return wrapper

return decorator

def data_permission_required(f):

"""数据权限校验装饰器(用于过滤返回数据)"""

@wraps(f)

def wrapper(*args, **kwargs):

if not current_user.is_authenticated:

return jsonify({"success": False, "error": "请先登录"}), 401

执行原函数,获取数据

result = f(*args, **kwargs)

if not result.get("success"):

return result

data_df = result.get("data")

if data_df is None:

return result

按权限过滤数据

filtered_df = check_data_permission(data_df, current_user.name)

return {**result, "data": filtered_df}

return wrapper

====================== 辅助函数 ======================

def get_func_name(func_type):

"""获取功能类型中文名称"""

func_map = {"view": "查看", "export": "导出", "modify": "修改"}

return func_map.get(func_type, "未知")

  1. 第四步:封装操作日志装饰器,实现全链路日志审计

-- coding: utf-8 --

logger.py 操作日志审计脚本

from functools import wraps

from flask import request, g

from flask_login import current_user

from models import OperationLog, db

import json

from datetime import datetime

====================== 日志记录装饰器 ======================

def log_operation(operation_type):

"""操作日志记录装饰器"""

def decorator(f):

@wraps(f)

def wrapper(*args, **kwargs):

初始化日志数据

log_data = {

"username": current_user.name if current_user.is_authenticated else "anonymous",

"user_role": current_user.role if current_user.is_authenticated else "anonymous",

"operation_type": operation_type,

"operation_content": {},

"operation_result": "fail",

"ip_address": get_client_ip(),

"user_agent": request.headers.get("User-Agent", "")

}

复制代码
        try:
            # 记录请求参数(操作内容)
            if request.method == "GET":
                log_data["operation_content"] = dict(request.args)
            elif request.method in ["POST", "PUT", "DELETE"]:
                log_data["operation_content"] = request.get_json() or dict(request.form)
            
            # 执行原函数
            result = f(*args, **kwargs)
            
            # 更新操作结果(成功)
            log_data["operation_result"] = "success"
            # 补充响应结果(脱敏处理,避免敏感数据)
            if isinstance(result, tuple) and len(result) == 2:
                resp_data, status_code = result
                if status_code == 200 and isinstance(resp_data, dict) and resp_data.get("success"):
                    log_data["operation_content"]["resp_msg"] = "操作成功"
            elif isinstance(result, dict) and result.get("success"):
                log_data["operation_content"]["resp_msg"] = "操作成功"
            
            return result
        except Exception as e:
            # 记录异常信息
            log_data["operation_content"]["error_msg"] = str(e)
            raise e
        finally:
            # 存储日志到数据库(匿名操作可选不记录)
            if log_data["username"] != "anonymous":
                save_operation_log(log_data)
    
    return wrapper
return decorator

====================== 日志存储与查询函数 ======================

def save_operation_log(log_data):

"""存储操作日志到数据库"""

try:

new_log = OperationLog(

username=log_data["username"],

user_role=log_data["user_role"],

operation_type=log_data["operation_type"],

operation_content=json.dumps(log_data["operation_content"], ensure_ascii=False),

operation_result=log_data["operation_result"],

ip_address=log_data["ip_address"],

user_agent=log_data["user_agent"],

operation_time=datetime.now()

)

db.session.add(new_log)

db.session.commit()

except Exception as e:

db.session.rollback()

print(f"日志存储失败:{str(e)}")

def query_operation_log(filters=None, page=1, page_size=20):

"""查询操作日志(支持多条件过滤、分页)"""

query = OperationLog.query

多条件过滤

if filters:

if "username" in filters:

query = query.filter(OperationLog.username == filters["username"])

if "operation_type" in filters:

query = query.filter(OperationLog.operation_type == filters["operation_type"])

if "operation_result" in filters:

query = query.filter(OperationLog.operation_result == filters["operation_result"])

if "start_time" in filters and "end_time" in filters:

start_time = datetime.strptime(filters["start_time"], "%Y-%m-%d %H:%M:%S")

end_time = datetime.strptime(filters["end_time"], "%Y-%m-%d %H:%M:%S")

query = query.filter(OperationLog.operation_time.between(start_time, end_time))

按操作时间倒序

query = query.order_by(OperationLog.operation_time.desc())

分页

total = query.count()

logs = query.limit(page_size).offset((page-1)*page_size).all()

格式化返回

log_list = []

for log in logs:

log_list.append({

"id": log.id,

"username": log.username,

"user_role": log.user_role,

"operation_type": get_operation_type_name(log.operation_type),

"operation_content": log.get_operation_content(),

"operation_result": "成功" if log.operation_result == "success" else "失败",

"ip_address": log.ip_address,

"user_agent": log.user_agent,

"operation_time": log.operation_time.strftime("%Y-%m-%d %H:%M:%S")

})

return {

"total": total,

"page": page,

"page_size": page_size,

"logs": log_list

}

====================== 辅助函数 ======================

def get_client_ip():

"""获取客户端IP地址"""

x_forwarded_for = request.headers.get("X-Forwarded-For")

if x_forwarded_for:

return x_forwarded_for.split(",")[0].strip()

return request.remote_addr

def get_operation_type_name(operation_type):

"""获取操作类型中文名称"""

type_map = {

"login": "登录", "view": "数据查看", "export": "数据导出",

"modify": "数据修改", "approve": "审批操作", "permission": "权限变更"

}

return type_map.get(operation_type, "未知操作")

  1. 第五步:整合到主应用,对接现有功能并测试

在app.py中新增/修改以下内容

from approval import approval_bp

from permission import permission_required, data_permission_required

from logger import log_operation, query_operation_log

from models import db, PermissionRule, OperationLog

from flask import jsonify, request

from datetime import datetime

注册审批流蓝图

app.register_blueprint(approval_bp)

====================== 原有接口添加权限与日志装饰器 ======================

1. 数据看板首页(添加数据权限校验和日志记录)

@app.route("/")

@login_required

@log_operation("view") # 记录数据查看操作

@data_permission_required # 按权限过滤数据

def dashboard():

selected_date = request.args.get("date", TODAY)

原有逻辑不变,获取融合数据后会自动被data_permission_required过滤

combined_df, _ = get_combined_data(date=selected_date)

summary, detail_data = get_summary_data(combined_df)

return render_template(

"dashboard.html",

summary=summary,

detail_data=detail_data,

combined_data=combined_df.to_dict("records"),

selected_date=selected_date

)

2. 数据导出接口(添加功能权限校验和日志记录)

@app.route("/export/data", methods=["POST"])

@login_required

@log_operation("export") # 记录数据导出操作

@permission_required("export") # 校验导出权限

def export_data():

原有逻辑不变,仅新增权限校验和日志记录

data = request.get_json()

export_format = data.get("format", "excel")

fields = data.get("fields", ["date", "name", "dept", "sales"])

selected_date = data.get("date", TODAY)

combined_df, _ = get_combined_data(date=selected_date)

按权限过滤数据

from permission import check_data_permission

filtered_df = check_data_permission(combined_df, current_user.name)

原有导出逻辑...

return response

3. 数据修改接口(新增,添加审批校验、权限校验、日志记录)

@app.route("/data/modify", methods=["POST"])

@login_required

@log_operation("modify") # 记录数据修改操作

@permission_required("modify") # 校验修改权限

def modify_data():

data = request.get_json()

校验修改授权令牌(审批通过后生成)

modify_token = data.get("modify_token")

from models import Approval

approval = Approval.query.filter(

Approval.applicant == current_user.name,

Approval.approval_type == "modify",

Approval.status == "approved",

Approval.content.like(f"%{modify_token}%")

).first()

if not approval:

return jsonify({"success": False, "error": "无有效修改授权,请先发起审批"}), 403

校验令牌有效期

content = approval.get_content()

expire_time = datetime.strptime(content["modify_expire"], "%Y-%m-%d %H:%M:%S")

if datetime.now() > expire_time:

return jsonify({"success": False, "error": "修改授权已过期,请重新发起审批"}), 403

原有数据修改逻辑...

return jsonify({"success": True, "msg": "数据修改成功"})

====================== 新增日志查询接口 ======================

@app.route("/log/query", methods=["POST"])

@login_required

@permission_required("view") # 仅管理员可查询所有日志,其他人仅查询自己的

def log_query():

if current_user.role != "leader":

普通用户仅能查询自己的日志

filters = {**request.get_json(), "username": current_user.name}

else:

filters = request.get_json() or {}

page = request.args.get("page", 1, type=int)

page_size = request.args.get("page_size", 20, type=int)

logs = query_operation_log(filters, page, page_size)

return jsonify({"success": True, "data": logs})

====================== 初始化数据库 ======================

确保新增表已创建(若用SQLAlchemy迁移工具,可执行迁移)

db.create_all() # 首次运行时执行,创建所有表

  1. 第六步:测试验证功能

重启Gunicorn和Nginx

kill -9 (ps aux \| grep gunicorn \| grep -v grep \| awk '{print 2}')

nohup gunicorn -w 4 -b 0.0.0.0:5000 app:app &

systemctl restart nginx

  • 审批流功能:普通员工发起权限申请/数据导出申请,部门负责人/管理员在企业微信收到审批通知,审批通过/驳回后,系统自动更新审批状态并执行对应操作(开通权限/生成授权令牌);

  • 精细化权限:用普通员工账号登录,验证仅能查看授权部门/字段/日期范围的数据,无导出权限时无法调用导出接口;

  • 操作日志:执行登录、查看数据、导出、审批等操作,查询日志接口,验证所有行为均被记录,日志信息完整(操作人、时间、内容、结果);

  • 合规校验:无审批通过的导出权限时,调用导出接口提示"无权限";修改数据时无有效授权令牌,提示"无修改授权";

  • 权限有效期:设置权限规则过期时间,过期后验证用户权限自动失效,无法访问授权数据。

五、新手避坑小贴士

  1. 审批回调验证失败:企业微信审批回调需实现AES解密与签名验证,严格按照官方文档编写解密逻辑,避免回调数据被篡改;回调地址必须公网可访问,且端口为80/443;

  2. 权限规则过滤异常:确保数据维度权限中的字段、部门名称与数据源一致(大小写、格式统一),日期范围格式为"YYYY-MM-DD",避免过滤逻辑报错;

  3. 日志记录重复/缺失:装饰器需放在Flask-Login的@login_required之后,确保能获取当前用户信息;异常捕获时需在finally中存储日志,避免操作失败无日志记录;

  4. JWT令牌失效:生成导出令牌时,确保JWT密钥一致,有效期设置合理(建议不超过24小时);令牌过期后需重新发起审批;

  5. 数据库性能问题:操作日志表数据量增长快,建议定期归档历史日志(如按月分表),查询时添加时间范围过滤,避免全表扫描;

  6. 权限与原有功能冲突:管理员角色默认拥有所有权限,需在权限校验函数中优先判断;普通用户默认权限需合理配置,避免无法查看基础数据。

六、进阶扩展(新手可选)

  1. 审批流自定义表单:集成拖拽式表单编辑器,支持用户自定义审批字段与表单布局,无需修改代码即可调整审批模板;

  2. 权限动态调整:实现权限规则的自动过期、临时权限(如仅开放2小时导出权限)、权限继承(部门负责人继承部门内所有数据权限);

  3. 日志可视化与告警:在看板中新增日志审计模块,用ECharts展示操作行为趋势、异常操作统计,对敏感操作(如批量导出、数据修改)触发企业微信告警;

  4. 多租户权限隔离:优化权限模型,支持多企业/多团队独立配置审批流程与权限规则,数据与日志完全隔离,适配SaaS化部署;

  5. 合规报表生成:自动生成权限审计报表、操作日志报表,按周/月推送至管理员,满足企业合规检查需求;支持日志导出为PDF格式,添加电子签章。

七、总结与系列合规闭环

  • AI合规校验:集成AI能力,自动识别敏感数据(如客户联系方式、手机号),审批时提示风险,日志中自动脱敏敏感信息;

  • 办公流程自动化:扩展审批流至更多办公场景(如请假、报销、采购),实现全流程自动化,与看板数据联动;

  • 国产化适配:适配国产化操作系统、数据库(如麒麟系统、达梦数据库),满足政企单位国产化合规要求。

如果这篇文章对你有帮助,欢迎点赞收藏+关注!如果在审批流配置、权限规则调试、日志审计时遇到问题,随时在评论区留言,我会逐一解答~ 新手不用怕合规管控与权限开发,跟着步骤一步步测试,就能让你的办公看板从"好用"升级为"安全可控",完全适配企业级合规办公需求!

相关推荐
ghgxm5201 小时前
Fastapi_00_学习策略与学习计划
python·学习·前端框架·npm·fastapi
AugustRed2 小时前
net.bytebuddy字节码引擎,动态生成Java类
java·开发语言
pixcarp2 小时前
Golang web工作原理详解
开发语言·后端·学习·http·golang·web
程序员:钧念2 小时前
【sh脚本与Python脚本的区别】
开发语言·人工智能·python·机器学习·语言模型·自然语言处理·transformer
CodeCraft Studio2 小时前
【能源行业案例】借助LightningChart打造高性能工业级数据可视化能力
信息可视化·lightningchart·能源数据可视化·js图表组件·web图表库·高性能图表
Pth_you2 小时前
Python权限问题终极解决方案
开发语言·python
Ulyanov2 小时前
PyVista战场可视化实战(三):雷达与目标轨迹可视化
开发语言·人工智能·python·机器学习·系统架构·tkinter·gui开发
张张努力变强2 小时前
C++ 类和对象(二):实例化、this指针、构造函数、析构函数详解
开发语言·c++
gaize12132 小时前
云计算服务和云解决方案-阿里云
开发语言·php