工单系统教程: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_atrole 分 admin 与 user 两种
Ticket(工单)id、title、description、status、priority、category、assignee、requester、cc_users、tags、created_at、updated_atdescription 支持 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 文件就能跑起来。它的价值不在功能多,而在“所有问题都有记录、都能追溯”。如果你团队的问题处理还靠微信群和口头传达,先把这套系统跑起来,用一个月,流程自然就顺了。

发表回复

后才能评论