工单系统教程:Flask + SQLite 的轻量级工单管理
小团队接用户反馈、内部报障,靠微信群和 Excel 迟早会乱:谁报的问题、处理到哪一步、最后怎么解决的,全凭记忆。工单系统就是把这个流程管起来的轻量工具:基于 Flask + SQLite,创建、流转、评论、附件、抄送、通知一应俱全,部署只需要 Python 和一个 SQLite 文件。本文从安装、生产部署、权限模型到日常使用,把这套工单系统完整讲一遍。
一、项目简介
工单系统(仓库名 ticket-system)是 git.cnbugs.com 上 AI-Agent 组织的一个开源项目,定位是“个人或小团队用的轻量级工单系统”。它不追求 Jira、禅道那种重量级功能,核心就是一件事:把问题从提交到关闭的整个过程记录下来、可追溯。
- 仓库:git.cnbugs.com/AI-Agent/ticket-system
- 后端:Flask + SQLAlchemy
- 数据库:SQLite(instance/tickets.db,自动创建)
- 前端:Jinja2 模板 + 原生 JavaScript
- 部署:Gunicorn + systemd
- 许可证:MIT
二、功能特性
| 模块 | 功能 |
|---|---|
| 工单管理 | 创建、编辑、删除、关闭工单;状态流转;分类、优先级、自由标签 |
| 富文本描述 | contenteditable 富文本编辑器,支持加粗、斜体,Ctrl+V 粘贴截图直接嵌入描述 |
| 附件管理 | 支持图片、文档、压缩包等多种文件,最大 20 MB,图片可点击放大预览 |
| 用户系统 | 注册、登录、登出,密码哈希存储,默认管理员 admin / admin |
| 权限控制 | 管理员看所有工单;普通用户只能看自己参与(提交人/处理人/抄送人)的工单,无权限返回 403 |
| 抄送功能 | 多选抄送人,自定义下拉选择器,抄送人自动收到通知、可查看工单与附件 |
| 通知系统 | 工单分配、评论、关闭等操作自动通知相关人员,导航栏显示未读角标 |
| 统计看板 | 工单总数、按状态/优先级/分类聚合、最近工单列表、我的工单计数 |
三、技术架构与数据模型
项目结构很简单,核心逻辑基本都在 app.py 一个文件里,wsgi.py 是 Gunicorn 入口,templates/ 是 Jinja2 模板,static/ 只有一份样式文件:
ticket-system/
├── app.py # Flask 主程序(模型 + 路由)
├── wsgi.py # Gunicorn WSGI 入口
├── requirements.txt # 依赖列表
├── README.md # 项目说明
├── instance/
│ └── tickets.db # SQLite 数据库(自动创建)
├── uploads/ # 上传文件目录
│ └── <ticket_id>/ # 每个工单独立的附件目录
├── templates/ # Jinja2 模板
│ ├── base.html # 基础布局模板
│ ├── login.html # 登录页
│ ├── register.html # 注册页
│ ├── dashboard.html # 统计看板
│ ├── ticket_list.html # 工单列表
│ ├── ticket_form.html # 新建/编辑工单
│ ├── ticket_detail.html # 工单详情
│ ├── notifications.html # 通知列表
│ └── error.html # 错误页
└── static/
└── style.css # 样式文件
数据模型一共五张表,关系很直观:
| 表 | 关键字段 | 说明 |
|---|---|---|
| User(用户) | id、username、password_hash、role、created_at | role 分 admin 与 user 两种 |
| Ticket(工单) | id、title、description、status、priority、category、assignee、requester、cc_users、tags、created_at、updated_at | description 支持 HTML(富文本);cc_users 与 tags 用逗号分隔存储 |
| Comment(评论) | id、ticket_id、author、content、created_at | 工单下的讨论记录 |
| Attachment(附件) | id、ticket_id、filename、original_name、content_type、size、is_image、uploaded_by、uploaded_at | 存储文件名用 UUID,避免重名冲突 |
| Notification(通知) | id、user_id、title、content、ticket_id、read、created_at | 站内通知,read 标记已读未读 |
附件按工单分目录存放(uploads/<ticket_id>/),文件名用 UUID 存储,原始文件名单独记在 original_name 里,下载时恢复原名。这种设计的好处是附件和工单一一对应,备份与清理都很方便。
四、适用场景
- 小团队内部报障:运维、开发、客服之间的问题流转,谁提的、谁在处理、处理到哪一步一目了然。
- 客户支持:客户提交技术故障、功能需求、咨询,处理过程全程留痕。
- 个人事务跟踪:一个人的待办与问题清单,用工单的状态流转代替零散的备忘录。
- 事件处理记录:故障、变更、值班事件都建工单,事后复盘有完整时间线。
- 不需要重型 ITSM 的场景:几个人到几十个人的规模,不想上 Jira、禅道、ServiceNow 这种重系统时,这个轻量方案刚好够用。
五、安装与启动
# 克隆仓库
git clone https://git.cnbugs.com/AI-Agent/ticket-system.git
cd ticket-system
# 安装依赖(Debian/Ubuntu 新版 Python 需要 --break-system-packages,
# 更推荐用 venv 虚拟环境,见下方)
pip install -r requirements.txt --break-system-packages
# 启动开发服务器
python app.py
更干净的做法是用虚拟环境,避免污染系统 Python:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.py
启动后打开浏览器访问 http://localhost:5000,首次启动会自动创建默认管理员账号:用户名 admin,密码 admin。登录后第一件事就是改掉这个默认密码。
六、生产部署:Gunicorn + systemd
开发服务器(python app.py)只适合本地调试,生产环境用 Gunicorn 跑两个 worker,再用 systemd 托管:
# 生产启动命令
gunicorn --workers 2 --bind 0.0.0.0:5000 wsgi:app
# /etc/systemd/system/ticket-system.service
[Unit]
Description=Ticket System
After=network.target
[Service]
Type=exec
WorkingDirectory=/path/to/ticket-system
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
Environment="PYTHONUNBUFFERED=1"
ExecStart=/usr/local/bin/gunicorn --workers 2 --bind 0.0.0.0:5000 --access-logfile /var/log/ticket-system/access.log --error-logfile /var/log/ticket-system/error.log wsgi:app
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
# 创建日志目录
mkdir -p /var/log/ticket-system
# 启动服务
systemctl daemon-reload
systemctl enable --now ticket-system
systemctl status ticket-system
# 看日志
tail -f /var/log/ticket-system/error.log
注意把 WorkingDirectory 与 ExecStart 里的路径换成实际部署路径;如果用 venv,ExecStart 里 gunicorn 要写虚拟环境的绝对路径(如 /opt/ticket-system/venv/bin/gunicorn),或者在 Environment 里把 venv 的 bin 目录加进 PATH。
七、Nginx 反向代理
server {
listen 80;
server_name ticket.example.com;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /uploads/ {
alias /path/to/ticket-system/uploads/;
}
}
uploads/ 单独用 alias 映射出来,附件下载与图片预览直接走 Nginx 静态文件,不经过 Flask,速度更快。上了 Nginx 之后顺手配 HTTPS(证书可以用 AutoSSL 自动管理),不要把 5000 端口直接暴露在公网。
八、用户与权限模型
| 角色 | 能看什么 | 能做什么 |
|---|---|---|
| 管理员(admin) | 所有工单 | 创建、编辑、删除、关闭、分配任意工单,管理用户 |
| 普通用户(user) | 仅自己参与的工单(提交人、处理人、抄送人是自己的) | 创建工单,编辑与评论自己参与的工单 |
权限控制在代码里是硬规则:普通用户访问不属于自己的工单会返回 403 错误,不是前端隐藏。注册是开放的,任何人注册后都是普通用户;管理员账号是首次启动自动创建的 admin/admin,生产环境必须第一时间改密码,并在 app.py 里把 SECRET_KEY 改成随机强值,否则 session 可以被伪造。
九、工单全生命周期使用示例
以“网站打不开”这个典型故障为例,走一遍完整流程:
- 1. 新建:客服或用户新建工单,标题“官网 https://example.com 打不开”,分类选“技术故障”,优先级选“高”,描述里写清楚现象、报错截图(Ctrl+V 直接粘贴截图,图片自动上传并嵌入描述),处理人选运维同学,抄送选值班经理。
- 2. 进行中:运维接单后把状态改成“进行中”,在评论里记录排查过程:先 curl 看返回码,再看 Nginx 日志,定位到证书过期。
- 3. 待处理:如果需要等外部配合(比如等域名服务商处理),状态改成“待处理”,并在评论里写明在等什么。
- 4. 已完成:证书续期后恢复访问,运维把状态改成“已完成”,评论里写清根因与处理步骤,附件里附上续期前后的证书信息截图。
- 5. 已关闭:提交人确认恢复后关闭工单,整个过程留档,以后同类问题直接搜工单就能找到处理方法。
| 状态 | 含义 | 谁可以改 |
|---|---|---|
| 新建 | 刚提交,还没人接手 | 管理员、处理人 |
| 进行中 | 已接手,正在处理 | 管理员、处理人 |
| 待处理 | 在等外部条件或他人配合 | 管理员、处理人 |
| 已完成 | 处理完毕,等提交人确认 | 管理员、处理人 |
| 已关闭 | 确认解决,归档 | 管理员、提交人 |
分类有五种:技术故障、功能需求、咨询、事件处理、其他;优先级四档:低、中、高、紧急;再加上自由标签,可以按“紧急 + 技术故障 + 标签 nginx”这种组合快速过滤。
十、通知与统计看板
工单分配、评论、关闭等操作会自动产生站内通知,相关人员在导航栏能看到未读角标,点进通知列表查看详情。抄送人也会自动收到通知,并且能查看工单与附件,适合“让领导知情但不参与处理”的场景。
统计看板在首页展示:工单总数、按状态/优先级/分类的聚合数、最近工单列表、我的工单计数。每天早上看一眼看板,就能知道还有多少未关闭的高优先级工单,比翻列表更直观。
十一、常见问题
| 问题 | 原因与处理 |
|---|---|
| 访问某个工单报 403 | 普通用户只能看自己参与的工单,让管理员把你加为处理人或抄送人 |
| 附件上传失败 | 检查 uploads/ 目录是否存在、进程有没有写权限;单个文件最大 20 MB,可在 app.py 里改 MAX_CONTENT_LENGTH |
| 粘贴的图片不显示 | 图片是先上传到服务器再嵌入描述的,检查 uploads/ 权限与磁盘空间 |
| 登录后提示 session 失效 | SECRET_KEY 被改过或服务重启后 session 失效,重新登录即可;生产环境固定 SECRET_KEY |
| 数据库字段不够用 | 新增字段需要手动添加到 SQLite 数据库,没有自动迁移,改之前先备份 instance/tickets.db |
| 忘记管理员密码 | 密码是哈希存储的,无法直接查看;可以用 Python 进 Flask shell 重置,或直接新建一个管理员账号 |
十二、运维与安全建议
- 第一时间改默认密码与 SECRET_KEY:admin/admin 是公开写在 README 里的,部署到内网也要改;SECRET_KEY 决定 session 的安全性,必须换成随机强值。
- 备份数据库与附件:instance/tickets.db 与 uploads/ 两个目录定时备份,恢复时一起放回去。
- 不要直接暴露 5000 端口:用 Nginx 反代,顺手上 HTTPS,限制 uploads/ 目录的执行权限,防止上传的脚本被执行。
- 附件大小与类型:默认最大 20 MB,够用就别放宽;如果允许上传压缩包,定期清理过期附件,避免磁盘被撑满。
- 升级流程:停服务 → 备份数据库与 uploads/ → 拉新代码 → 重启验证;新增字段要手动改库,升级前务必备份。
- 定期归档:已关闭超过半年的工单可以导出后归档,保持列表清爽,查询也更快。
工单系统没有花哨的功能,但把小团队最需要的流程做全了:提交、流转、评论、附件、抄送、通知、统计,一个 SQLite 文件就能跑起来。它的价值不在功能多,而在“所有问题都有记录、都能追溯”。如果你团队的问题处理还靠微信群和口头传达,先把这套系统跑起来,用一个月,流程自然就顺了。





