大家好!我是CSDN的Python新手博主~ 上一篇我们完成了看板的多数据源整合与ECharts交互式可视化,提升了数据探查能力,但很多企业用户反馈三个核心痛点:① 缺乏审批流程,权限申请、敏感数据导出等操作无管控,易造成数据泄露;② 权限管控颗粒度粗,仅按"管理员/部门负责人/普通员工"分级,无法实现"某员工仅看本部门某类数据"的精细化需求;③ 无操作日志追溯,数据修改、导出、权限变更等行为无法审计,不符合企业合规要求。今天就带来超落地的新手实战项目------办公看板集成智能审批流+精细化权限管控+操作日志审计!
本次基于之前的"多源数据可视化看板"代码,新增4大核心功能:① 企业微信审批流集成(支持权限申请、数据导出、数据修改三类审批,自动推送审批通知);② 精细化权限管控(按"数据维度+功能维度"双重授权,支持自定义权限规则);③ 全链路操作日志审计(记录用户登录、数据查看、导出、审批等所有行为,支持日志查询与导出);④ 合规校验(审批流程未通过时禁止敏感操作,日志留存满足合规追溯要求)。全程基于现有技术栈(Flask+MySQL+企业微信API),新增审批模型、权限规则引擎、日志装饰器,代码注释详细,新手只需配置审批模板与权限规则,跟着步骤复制操作就能成功,让办公看板完全适配企业合规办公场景~
一、本次学习目标
-
掌握企业微信审批流API的调用方法,实现审批模板创建、审批单发起、审批结果回调全流程;
-
理解精细化权限管控逻辑,实现"数据维度(部门/日期/字段)+功能维度(查看/导出/修改)"双重授权;
-
学会用Python装饰器封装日志记录功能,实现全链路操作日志的自动采集、存储与查询;
-
掌握合规校验逻辑,确保敏感操作必须经过审批,操作行为可追溯、可审计;
-
实现审批、权限、日志与现有功能(AI分析、多源数据、可视化)的无缝衔接,不影响原有业务流程。
二、前期准备
- 安装核心依赖库
安装核心依赖(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装饰器实现,轻量化无侵入。
- 企业微信审批模板配置
-
登录企业微信后台,进入「应用管理」→「审批」→「创建模板」;
-
创建3个模板(对应三类审批):
-
权限申请模板:字段含"申请人、申请部门、申请权限类型(数据查看/导出)、具体权限范围(部门/字段)、申请理由";
-
数据导出模板:字段含"申请人、导出数据日期范围、导出字段、导出用途、接收邮箱";
-
数据修改模板:字段含"申请人、修改数据日期、修改内容(原数据/新数据)、修改理由"。
-
-
记录每个模板的「模板ID(template_id)」,配置审批流程(如普通员工申请→部门负责人审批→管理员终审);
-
设置审批回调地址(服务器公网IP/域名+接口路径,如"http://47.108.xxx.xxx/wecom/approval/callback"),用于接收审批结果。
- 数据库表设计与创建
-- 连接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='操作日志表';
三、实战:审批流+权限管控+日志审计集成
- 第一步:封装企业微信审批流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
- 第二步:创建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 {}
- 第三步:实现精细化权限管控,封装权限校验装饰器
-- 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, "未知")
- 第四步:封装操作日志装饰器,实现全链路日志审计
-- 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, "未知操作")
- 第五步:整合到主应用,对接现有功能并测试
在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() # 首次运行时执行,创建所有表
- 第六步:测试验证功能
重启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
-
审批流功能:普通员工发起权限申请/数据导出申请,部门负责人/管理员在企业微信收到审批通知,审批通过/驳回后,系统自动更新审批状态并执行对应操作(开通权限/生成授权令牌);
-
精细化权限:用普通员工账号登录,验证仅能查看授权部门/字段/日期范围的数据,无导出权限时无法调用导出接口;
-
操作日志:执行登录、查看数据、导出、审批等操作,查询日志接口,验证所有行为均被记录,日志信息完整(操作人、时间、内容、结果);
-
合规校验:无审批通过的导出权限时,调用导出接口提示"无权限";修改数据时无有效授权令牌,提示"无修改授权";
-
权限有效期:设置权限规则过期时间,过期后验证用户权限自动失效,无法访问授权数据。
五、新手避坑小贴士
-
审批回调验证失败:企业微信审批回调需实现AES解密与签名验证,严格按照官方文档编写解密逻辑,避免回调数据被篡改;回调地址必须公网可访问,且端口为80/443;
-
权限规则过滤异常:确保数据维度权限中的字段、部门名称与数据源一致(大小写、格式统一),日期范围格式为"YYYY-MM-DD",避免过滤逻辑报错;
-
日志记录重复/缺失:装饰器需放在Flask-Login的@login_required之后,确保能获取当前用户信息;异常捕获时需在finally中存储日志,避免操作失败无日志记录;
-
JWT令牌失效:生成导出令牌时,确保JWT密钥一致,有效期设置合理(建议不超过24小时);令牌过期后需重新发起审批;
-
数据库性能问题:操作日志表数据量增长快,建议定期归档历史日志(如按月分表),查询时添加时间范围过滤,避免全表扫描;
-
权限与原有功能冲突:管理员角色默认拥有所有权限,需在权限校验函数中优先判断;普通用户默认权限需合理配置,避免无法查看基础数据。
六、进阶扩展(新手可选)
-
审批流自定义表单:集成拖拽式表单编辑器,支持用户自定义审批字段与表单布局,无需修改代码即可调整审批模板;
-
权限动态调整:实现权限规则的自动过期、临时权限(如仅开放2小时导出权限)、权限继承(部门负责人继承部门内所有数据权限);
-
日志可视化与告警:在看板中新增日志审计模块,用ECharts展示操作行为趋势、异常操作统计,对敏感操作(如批量导出、数据修改)触发企业微信告警;
-
多租户权限隔离:优化权限模型,支持多企业/多团队独立配置审批流程与权限规则,数据与日志完全隔离,适配SaaS化部署;
-
合规报表生成:自动生成权限审计报表、操作日志报表,按周/月推送至管理员,满足企业合规检查需求;支持日志导出为PDF格式,添加电子签章。
七、总结与系列合规闭环
-
AI合规校验:集成AI能力,自动识别敏感数据(如客户联系方式、手机号),审批时提示风险,日志中自动脱敏敏感信息;
-
办公流程自动化:扩展审批流至更多办公场景(如请假、报销、采购),实现全流程自动化,与看板数据联动;
-
国产化适配:适配国产化操作系统、数据库(如麒麟系统、达梦数据库),满足政企单位国产化合规要求。
如果这篇文章对你有帮助,欢迎点赞收藏+关注!如果在审批流配置、权限规则调试、日志审计时遇到问题,随时在评论区留言,我会逐一解答~ 新手不用怕合规管控与权限开发,跟着步骤一步步测试,就能让你的办公看板从"好用"升级为"安全可控",完全适配企业级合规办公需求!