你有没有遇到过这样的场景:
想搭一个个人博客 + 后台管理系统,却在 Django 的重量级、Vue 的工程化、Node 的版本兼容之间反复横跳?最后发现,我只是想安安静静写个博客、管管待办、记个日程而已啊!
今天给大家介绍的 XAdmin,就是这样一款"反内卷"的作品 —— 用最朴素的技术栈,做最实用的功能,代码量不大,但每一行都踩过真实的坑。
一、项目概览:XAdmin 到底是什么?
XAdmin 是一个基于 Flask + Layui 构建的个人博客 + 后台管理一体化系统,目前已部署在 shenco.wang 上稳定运行。
它不追求微服务、不搞前后端分离(没有 npm install 的等待焦虑)、不依赖 Redis/Elasticsearch 等重型中间件 —— 下载下来,pip install -r requirements.txt,python app.py 就能跑起来。
1.1 技术栈全景图
| 层级 | 技术选型 | 版本 | 为什么这么选? |
|---|---|---|---|
| 后端框架 | Flask | 3.1.1 | 轻量、灵活,Python 生态最成熟的微框架 |
| ORM | Flask-SQLAlchemy | 3.1.1 | 不用写原生 SQL,但又保留足够控制力 |
| 数据库 | SQLite | 3.x | 零配置!单文件部署,个人博客完全够用 |
| 用户认证 | Flask-Login | 0.6.3 | 会话管理、记住我、登录保护一应俱全 |
| 前端 UI | Layui | 2.x | 经典模块化前端框架,后台系统的老朋友 |
| 图表 | ECharts | 5.x | 百度出品,仪表盘数据可视化必备 |
| Markdown | Python-Markdown + Editor.md | - | 写作体验友好,代码高亮、目录自动生成 |
| 生产服务器 | Gunicorn + gevent | 23.0.0 | 协程模式,小内存机器也能跑高并发 |
| 反向代理 | Nginx | - | 静态资源分离、HTTPS、Gzip 压缩 |
| AI 能力 | 百度人脸识别 API | - | 支持刷脸登录,黑科技加成 |
1.2 一睹芳容





💡 作者的设计哲学:技术选型不是越新越好,而是"够用、稳定、好维护"。 对于个人站点来说,SQLite 比 MySQL 少了一个进程的烦恼; 服务端渲染比 SPA 少了 Node 构建的复杂度; Layui 比 React/Vue 少了 webpack 的学习成本。
二、目录结构:一目了然的代码组织
XAdmin/
├── app.py # 应用工厂入口:注册蓝图、初始化扩展、自动建表
├── config.py # 配置类:密钥、数据库路径、上传限制、Session 安全
├── requirements.txt # 依赖清单(核心仅 12 个包!)
├── gunicorn_config.py # 生产环境 Gunicorn 配置(gevent 协程)
│
├── models/ # 数据模型层(SQLAlchemy ORM)
│ ├── __init__.py # 统一导入所有模型,db.create_all() 一键建表
│ ├── user.py # 用户 + 用户角色关联表
│ ├── role.py # 角色 + 角色菜单关联表
│ ├── menu.py # 菜单(目录/菜单/按钮三级)
│ ├── todo.py # 待办事项(5 状态看板)
│ ├── schedule.py # 日程安排(日历视图)
│ ├── note.py # 笔记/备忘录(Markdown)
│ ├── article.py # 博客文章
│ ├── filestash.py # 文件暂存(分享码功能)
│ ├── log.py # 操作日志
│ ├── login_log.py # 登录日志
│ └── ...
│
├── routes/ # 路由层(Flask Blueprint 蓝图)
│ ├── auth.py # 登录/登出/人脸登录/个人中心
│ ├── dashboard.py # 仪表盘 + 统计 API
│ ├── todo.py # 待办 CRUD + 看板拖拽
│ ├── schedule.py # 日程 CRUD
│ ├── user.py # 用户管理
│ ├── role.py # 角色管理
│ ├── menu.py # 菜单管理
│ ├── frontweb.py # 前台博客(文章列表/详情/评论/搜索)
│ └── ...
│
├── templates/ # Jinja2 模板
│ ├── admin/ # 后台管理页面
│ │ ├── index.html # 后台主框架(侧边栏、顶部导航)
│ │ ├── dashboard.html # 仪表盘
│ │ ├── login.html # 登录页(含人脸登录)
│ │ ├── todo.html # 待办看板
│ │ └── ...
│ └── frontweb/ # 前台博客页面
│ ├── index.html # 首页
│ ├── blog.html # 文章列表
│ ├── article.html # 文章详情
│ └── ...
│
├── static/ # 静态资源
│ ├── libs/ # 第三方库(Layui、ECharts、Font Awesome、Editor.md)
│ ├── css/、js/ # 自定义样式和脚本
│ ├── uploads/ # 用户上传文件
│ └── ui/ # 前台主题资源
│
├── utils/ # 工具模块
│ ├── auth.py # 权限装饰器(login_required_ajax、permission_required)
│ ├── logger.py # 操作日志装饰器 @log_operation
│ ├── file.py # 文件上传/删除
│ ├── captcha.py # 验证码生成与校验
│ └── web_auth.py # 前台用户认证(和后台独立)
│
├── deploy/ # 部署相关
│ ├── deploy.sh # 一键部署脚本(7 步全自动)
│ ├── xadmin-gunicorn.service # systemd 服务配置
│ └── xadmin.conf # Nginx 反向代理配置
│
└── instance/
└── xadmin.db # SQLite 数据库文件
🔍 架构亮点:采用"模型-路由-模板"三层分离,每个业务模块一个 model 文件 + 一个 route 文件,职责清晰。20+ 个蓝图(Blueprint)模块化注册,避免单文件上千行的"屎山"。
三、核心架构拆解:从代码看设计思路
3.1 应用工厂模式:create_app() 的妙用
打开 app.py,你会看到经典的 Flask 应用工厂模式:
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
# 确保上传目录存在
os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
# 初始化数据库
db.init_app(app)
# 初始化登录管理
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "auth.login"
# 注册 23 个蓝图(Blueprint)
from routes.auth import auth_bp
from routes.dashboard import dashboard_bp
# ... 省略其他 import
app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp)
# ...
return app
为什么用工厂模式? 1. 便于测试:可以创建多个 app 实例跑不同配置 2. 延迟初始化:db、login_manager 先创建,后绑定 app 3. 清晰的启动流程:配置 → 扩展 → 蓝图 → 错误处理,顺序一目了然
3.2 最聪明的"自动增量迁移"
很多人吐槽 Flask 没有 Django 那样的 migrations,XAdmin 的解决方案让我眼前一亮 —— 启动时自动检测并添加缺失字段:
with app.app_context():
db.create_all() # 已存在的表不会重建,无副作用
# 增量迁移:为 todo 表添加 user_id 字段(已存在则跳过)
try:
from sqlalchemy import inspect, text
cols = [c["name"] for c in inspect(db.engine).get_columns("todo")]
if "user_id" not in cols:
db.session.execute(text("ALTER TABLE todo ADD COLUMN user_id INTEGER"))
db.session.commit()
except Exception:
db.session.rollback()
这太接地气了! 对于个人项目:
- ✅ 不需要装 Alembic,不需要执行
flask db migrate - ✅ 加字段只需加个 try 块,代码里自带迁移逻辑
- ✅
db.create_all()幂等,反复执行不会丢数据 - ✅ 部署到新环境自动建表,老环境自动补字段
踩过的坑:之前 Todo 的 user_id 字段就是这样加上的,老数据还做了一次回填(66 条记录的 user_id=1),这些都是实战里摸出来的经验。
3.3 RBAC 权限系统:用户-角色-菜单三级联动
权限系统是后台管理的灵魂,XAdmin 的实现简洁而不简单:
数据模型是经典的多对多关系:
User (用户) ←→ user_role ←→ Role (角色) ←→ role_menu ←→ Menu (菜单/权限)
核心代码在 models/user.py:
def get_menu_ids(self):
"""获取用户所有菜单ID(合并所有角色的菜单)"""
menu_ids = set()
for role in self.roles:
for menu in role.menus:
menu_ids.add(menu.id)
return list(menu_ids)
权限控制有三层保护:
- 页面级:
@login_required装饰器检查是否登录 - 菜单级:routes/auth.py 中
/api/menu/tree接口根据角色返回菜单树,并且自动向上补全父级目录:
# 向上补全父级:即使只给了子菜单权限,父目录也自动显示
allowed_ids = set(menu_ids)
menu_map_all = {m.id: m for m in menus}
for mid in list(allowed_ids):
pid = menu_map_all[mid].parent_id
while pid and pid in menu_map_all and pid not in allowed_ids:
allowed_ids.add(pid)
pid = menu_map_all[pid].parent_id
- 接口级:utils/auth.py 中的装饰器:
def permission_required(permission_code):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not current_user.is_authenticated:
return jsonify(code=401, msg="请先登录"), 401
if current_user.username == "admin": # admin 超级用户
return f(*args, **kwargs)
if not current_user.has_role(permission_code):
return jsonify(code=403, msg="没有操作权限"), 403
return f(*args, **kwargs)
return decorator
return decorator
⚠️ 踩过的坑:项目记忆里有一条血泪教训 —— "不能只用
has_role('admin')判断权限"!正确做法是用get_menu_ids()检查是否有菜单权限,否则会出现"角色分配了权限但进不了后台"的 Bug。
3.4 操作日志:一个装饰器搞定审计
utils/logger.py 提供了一个非常优雅的 AOP 式日志记录:
def log_operation(module, action):
"""操作日志记录装饰器"""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
g.start_time = time.time()
result = f(*args, **kwargs) # 先执行业务逻辑
try:
duration = time.time() - g.start_time
log = OperationLog(
user_id=current_user.id,
username=current_user.username,
module=module, # 如 "待办事项"
action=action, # 如 "新增"
method=request.method,
url=request.url,
ip=request.remote_addr,
duration=duration, # 记录响应耗时
)
db.session.add(log)
db.session.commit()
except Exception:
db.session.rollback()
return result
return decorated
return decorator
使用起来极其简单,加在路由函数上就行:
@todo_bp.route("/api/add", methods=["POST"])
@login_required_ajax
@log_operation("待办事项", "新增") # 就这一行!
def add():
# ... 业务逻辑
谁、在什么时候、做了什么操作、IP 是多少、耗时多久,全部记录在案。出了问题查日志,一清二楚。
四、功能亮点:那些让人会心一笑的细节
4.1 待办看板:5 状态工作流 + 拖拽操作
models/todo.py 定义了 5 个状态:
| 状态值 | 名称 | 含义 |
|---|---|---|
| 0 | 待办池 | 想法收集,还没安排 |
| 1 | 待处理 | 已计划,准备做 |
| 2 | 进行中 | 正在做 |
| 3 | 审计中 | 做完了,等检查 |
| 4 | 已完成 | 搞定! |
快速切换状态的接口很巧妙 —— 状态循环:
@todo_bp.route("/api/toggle-status", methods=["POST"])
def toggle_status():
todo = Todo.query.filter_by(id=todo_id, user_id=current_user.id).first_or_404()
todo.status = (todo.status + 1) % 5 # 0→1→2→3→4→0 循环
db.session.commit()
配合前端 Layui 的看板拖拽,体验丝滑。而且所有查询都严格过滤 user_id=current_user.id,多用户之间数据完全隔离 —— 这也是踩过坑之后加上的(之前 66 条待办的 user_id 为空,导致用户看不到自己的数据)。
4.2 仪表盘:ECharts 数据可视化
打开 /dashboard/console,你会看到一个信息密度很高的仪表盘:
- 统计卡片:用户数、角色数、菜单数、今日日志、待办完成数、逾期数
- 近 7 天操作趋势:折线图
- 工作饱满度分析:近 22 个工作日的多维度数据(日程数、完成待办、操作日志、登录次数、文章阅读)
- 今日待办、即将到来的日程、最近笔记、热门文章列表
- 最近操作日志
核心 API 在 routes/dashboard.py /api/stats,一个接口返回所有数据,前端一次性渲染。工作量数据生成函数考虑了跳过周末:
def get_workload(days, skip_weekend=True):
"""生成指定天数的工作量数据,自动跳过周末"""
data = []
while target < days and i < max_iter:
d = today - timedelta(days=i)
if skip_weekend and d.weekday() >= 5: # 5=周六, 6=周日
continue
# ... 统计当天数据
4.3 人脸识别登录:黑科技加成
XAdmin 集成了百度 AI 的人脸识别 API,支持刷脸登录和人脸录入:
routes/auth.py 实现了完整的人脸功能:
- Token 缓存:百度 access_token 有有效期,代码里做了内存缓存,避免每次请求都去获取
- 人脸登录:前端调用摄像头拍照 → Base64 上传 → 调百度人脸搜索 → 匹配到用户就登录
- 人脸录入:管理员可以录入人脸,绑定到账号
- 人脸删除:支持移除已录入的人脸
def _baidu_face_api(url, payload):
"""调用百度人脸 API 通用方法,统一处理 token 和错误"""
token = _get_baidu_token()
try:
resp = requests.post(url, params={"access_token": token}, json=payload, timeout=15)
return resp.json()
except Exception as e:
return {"error_code": -2, "error_msg": str(e)}
4.4 登录安全:锁定机制 + 验证码
暴力破解是后台系统的常见威胁,XAdmin 做了两层防护:
- 失败计数锁定:连续失败 N 次(可配置),账号锁定 M 分钟
- 验证码开关:后台可以配置是否开启图形验证码
- 密码哈希存储:使用 Werkzeug 的
generate_password_hash,不存明文 - Session 安全属性:HttpOnly + SameSite=Lax,生产环境开启 Secure
SESSION_COOKIE_HTTPONLY = True # 防止 XSS 窃取 Cookie
SESSION_COOKIE_SAMESITE = "Lax" # 防止 CSRF
SESSION_COOKIE_SECURE = True # HTTPS 下开启
锁定逻辑在 routes/auth.py,用 session 记录失败次数,不需要 Redis:
def _record_login_fail(username, sec_cfg):
"""记录登录失败次数,达到阈值就锁定"""
fails = session.get(fail_key, 0) + 1
session[fail_key] = fails
if fails >= sec_cfg["login_max_retry"]:
session[f"admin_login_lock_time_{username}"] = time.time()
return True, f"连续失败 {fails} 次,账号已锁定 {sec_cfg['login_lock_time']} 分钟"
return False, f"用户名或密码错误,还可尝试 {sec_cfg['login_max_retry'] - fails} 次"
4.5 前台博客:Markdown + 代码高亮 + 评论嵌套
frontweb.py 实现了一个功能完备的博客前台:
Markdown 渲染带懒加载优化:
def _lazy_img(html):
"""给 Markdown 渲染出的 <img> 标签添加懒加载属性"""
def _add_attrs(m):
tag = m.group(0)
if 'loading=' in tag:
return tag
return tag[:-1] + ' loading="lazy" decoding="async">'
return re.sub(r'<img(?:(?!loading=)[^>])*?>', _add_attrs, html)
多图文章加载慢?加上原生 loading="lazy" 就搞定,不需要第三方库!
评论系统支持:
- 嵌套回复(父子结构)
- 点赞功能
- 违禁词自动审核(有违禁词进入待审核,否则直接显示)
- 图片上传
- 后台 User 和前台 WebUser 双体系,头像自动匹配
- 评论区 Markdown 输入
文章功能:
- 密码保护文章(输入密码才能看)
- 阅读量统计(用 update 语句避免 ORM 并发问题)
- 上一篇/下一篇导航
- 分类浏览、关键词搜索
- Banner 推荐位
- 全局搜索(文章+模板+软件三类结果一起返回)
4.6 进程内缓存:简单但有效
没有 Redis?没关系,XAdmin 用一个字典实现了轻量缓存:
_CACHE = {} # {key: (value, expire_ts)}
def _cache_get(key, max_age=300):
item = _CACHE.get(key)
if not item:
return None
value, expire_ts = item
if time.time() > expire_ts:
_CACHE.pop(key, None)
return None
return value
网站配置缓存 5 分钟、文章分类缓存 10 分钟。对于个人博客单进程部署完全够用,省去了 Redis 的部署维护成本。
五、生产部署:一键脚本 + Gunicorn + Nginx
项目在 deploy/ 目录下提供了完整的生产部署方案。
5.1 Gunicorn + gevent 高并发配置
gunicorn_config.py 的配置很专业:
import gevent.monkey
gevent.monkey.patch_all() # monkey-patch,必须在最前面!
bind = '127.0.0.1:8001' # 仅监听本地,Nginx 反代
workers = 4 # 工作进程数
worker_class = 'gevent' # 协程模式
worker_connections = 1000 # 每个 worker 千级连接
timeout = 120
max_requests = 2000 # 处理 2000 请求后自动重启,防内存泄漏
max_requests_jitter = 50 # 抖动,避免同时重启
preload_app = True # 预加载,节省内存
💡 为什么用 gevent? Flask 默认是同步阻塞的,一个请求卡住整个 worker 就等住。gevent 用协程实现并发,哪怕只有 4 个 worker,也能同时处理上千个连接。对于个人博客这种 IO 密集型应用,性能提升明显。
5.2 Nginx 配置:静态资源分离 + HTTPS
xadmin.conf 做了几件事:
- 静态资源直接由 Nginx 服务,设置 7 天缓存,不经过 Gunicorn
- 上传目录单独配置,3 天缓存
- 动态请求反代到 Gunicorn 8001 端口
- HTTP 自动跳转 HTTPS,用 Let's Encrypt 免费证书
- 新旧站共存:shenco.wang 是新站,v2.shenco.wang 是旧站,一个 Nginx 搞定
location /static/ {
alias /opt/Xadmin/static/;
expires 7d;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
5.3 一键部署脚本:7 步全自动
deploy.sh 把部署流程完全自动化了:
- 准备目录:创建 /opt/Xadmin、logs、instance
- 检查用户:自动创建运行用户 gggaiitx
- 创建虚拟环境:python3 -m venv
- 安装依赖:pip install -r requirements.txt
- 注册 systemd 服务:自动生成 SECRET_KEY
- 部署 Nginx 配置:nginx -t 测试,reload
- 启动服务:systemctl restart,设置开机自启
部署完了还会贴心提示常用命令:
常用命令:
查看状态:systemctl status xadmin-gunicorn
重启服务:systemctl restart xadmin-gunicorn
查看日志:journalctl -u xadmin-gunicorn -f
从 git clone 到网站上线,只需要执行一条命令:
sudo bash deploy/deploy.sh
六、那些工程上的"小心思"
读 XAdmin 的代码,你会发现很多"小地方"体现了作者的经验:
1. 数据库唯一字段用 NULL 不用空字符串
email = db.Column(db.String(120), unique=True, nullable=True) # ✅
如果设成 nullable=False, default="",多个用户邮箱为空时会触发唯一约束冲突!这是 SQLite/MySQL 的经典坑。
2. 空字符串 vs NULL 的取舍
nickname、avatar、phone 这些可选字段默认空字符串,查询时用 or "" 处理;而 email 这种需要 unique 的字段用 NULL。
3. 分页查询用 Flask-SQLAlchemy 的 paginate
pagination = query.paginate(page=page, per_page=limit, error_out=False)
return jsonify({
"code": 0, "count": pagination.total,
"data": [t.to_dict() for t in pagination.items],
})
标准的 Layui 表格响应格式,前端 table 组件直接能用。
4. to_dict() 方法统一序列化
每个 Model 都有 to_dict() 方法,日期格式化成字符串,关联对象取名称,API 返回永远用它 —— 再也不会出现"直接返回 ORM 对象导致 JSON 序列化失败"的问题。
5. 日期解析兼容多种格式
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
due_date = datetime.strptime(due_date_str, fmt)
break
except ValueError:
continue
前端传的日期格式可能不一致?多试几种格式就好了,鲁棒性满分。
6. 查询严格过滤 user_id
所有个人数据(Todo、Schedule、Note、FileStash)的查询都带 filter_by(user_id=current_user.id),这是多用户系统的基本素养,也是踩过"数据越权访问"的坑之后养成的习惯。
七、总结:XAdmin 给我们的启示
读完 XAdmin 的整个代码库,我最大的感受是:"适合的才是最好的。"
在这个"微服务+K8s+大模型"概念满天飞的时代,XAdmin 用 Flask + SQLite + Layui 这样朴素的技术栈,做出了一个功能完备、稳定运行、易于部署的全栈应用。它没有追逐最新的框架,而是在自己的技术选型里做到了:
- 代码清晰:20+ 个蓝图模块化,每个文件职责单一
- 功能实用:权限、待办、日程、笔记、博客、评论、人脸登录... 你需要的它都有
- 部署简单:SQLite 零配置,一键部署脚本,1G 内存的 VPS 就能跑
- 考虑周全:安全防护、日志审计、增量迁移、错误处理,细节里见真章
- 易于扩展:加一个新功能?加个 model、加个 route、加个 template,三步搞定
适合谁用?
- 🏠 个人博主:想搭一个自己的博客站点,又不想折腾复杂技术栈
- 📚 Python 初学者:想学习 Flask 项目结构,这是一个非常好的参考
- 🛠️ 中小企业内部系统:快速搭一个 OA/CRM/管理后台,权限、日志、CRUD 都现成的
- 🎓 毕业设计/课程项目:代码量适中,功能完整,答辩有东西讲
最后想说:好的开源项目不一定是 star 数最多的,也不一定是技术栈最潮的。那些真正解决了问题、代码经得起推敲、维护者在用爱发电持续更新的项目,同样值得我们尊重和学习。
XAdmin 就是这样一个项目 —— 它不完美,但它真诚。
评论
0