做企业管理系统的开发,绕不开一个经典场景:工作计划的制定、下达、执行、跟踪和复盘。这几年我经手过不少类似的内部系统,从 OA 里的插件到独立部署的 Web 平台都有,说实话,真正让业务部门用得舒服的并不多。原因很简单——不少团队要么直接买一套大而全的 OA,配置复杂到没人愿意用;要么用 Excel 表格传来传去,计划到底进展到哪一步,月底对账时全靠记忆。
这个基于 Python 和 Django 的企业员工工作计划管理系统,就是我在实际项目中沉淀下来的一套方案。它的定位很明确:不追求功能堆砌,而是把"计划—执行—检查—复盘"这条主链路做扎实。顺手说一下,Django 在这里扮演的角色是整个系统的地基,从数据模型、后台管理到认证授权,它都给到了足够成熟的方案,这也是我选择它的核心理由。
整篇内容我会从需求拆解开始,依次讲技术选型、数据库设计、核心功能实现、安全与性能优化、部署运维,最后把我踩过的坑和排查经验整理成一张速查表。内容偏实操,适合有一到两年 Python 基础、想上手 Django 做完整项目的开发者,也适合企业内部想快速搭建一套轻量管理平台的团队参考。
1. 先搞清楚这个系统到底要解决什么问题
1.1 企业工作计划管理的真实痛点
在动手写第一行代码之前,我习惯先花时间把业务场景彻底理清。这不是走形式,而是后面所有设计决策的基础。企业里工作计划管理最常见的现状有三类:
第一是计划散落。员工的工作计划散落在邮件、微信群、Excel 表格甚至纸质笔记本里,管理者几乎无法在任意时间点看到团队的整体进展。你说大家不努力吗?也不是,而是信息根本没有汇集到一个地方。
第二是跟踪困难。计划一旦下达,执行过程基本处于"黑盒"状态。中间有没有延期、需要哪些资源协调、遇到什么阻塞,往往要等周报、月报甚至出了事故之后才知道。对于管理者来说,这就像开车只看仪表盘上的油量,却看不到发动机转速。
第三是复盘无据。月底复盘的时候,谁做了什么、结果如何、偏差出在哪,都靠聊天记录和截图,数据无法沉淀,自然也就谈不上形成组织知识库。同一个坑踩第二次,这在不少团队里真的太常见了。
这些问题单独拿出来好像都不大,比如用群接龙也能做日报,用共享表格也能填周计划。但一旦团队超过某个规模、或者跨部门协作变多,这些"轻量方案"就开始频繁出错:表格被人误改、版本对不上、审批链路完全靠人肉提醒。这时候就需要一个结构化的系统来兜底。
另外还有一层需求容易被忽略:安全工作计划的执行情况往往直接挂钩绩效考核。这意味着系统里的数据必须可追溯、可审计。谁在什么时间创建了计划,谁批准了,谁修改过状态,每一步都要有记录。这正是纯手工管理无论如何也做不到的。
1.2 为什么不用现成的 OA 或低代码平台
聊需求的时候经常有人问我:"这功能不是买套 OA 就有吗?""低代码拖一拖不就行了?"我的回答通常是:分情况。
如果你只需要一个简单的"填表+汇总"功能,那市面上任何一款表单工具都能胜任。但如果你需要的是严肃的工作计划管理——包含多级审批、状态流转、超时提醒、权限隔离、绩效关联——通用 OA 的问题就暴露出来了:
- 配置成本高。通用 OA 的流程引擎功能确实强大,但学习曲线陡峭,配置一套符合公司实际组织架构的流程,往往需要厂商顾问介入,实施周期按月计算。
- 使用体验重。大而全的系统对普通员工来说,光找到正确的入口就需要点好几层菜单。员工不爱用,系统就变成摆设。
- 定制不灵活。想加一个"计划延期自动通知相关人"的规则,在标准 OA 里可能要走工单、排期,等几个迭代。
- 数据取用困难。业务数据和流程数据深埋在厂商的封闭模型里,想拉出来做部门维度的统计报表,经常得依赖厂商支持。
低代码平台也有类似问题。它适合快速搭原型、做简单的数据录入界面,但一涉及到复杂的业务规则、细粒度的权限模型、或者需要深度定制的前端交互,就会撞到平台的天花板。更别说数据主权和长期使用的成本,这些都是实际项目里不得不考虑的因素。
所以这个项目我选择从头用 Python 和 Django 做一个专用系统。它不需要解决一万个问题,只专心做好"工作计划管理"这一件事。表面上看,自研好像更费力,实际上因为 Django 把认证、ORM、Admin、模板引擎这些基础设施都准备好了,开发一个中等复杂度的业务系统,比大多数人想象的要快得多。
2. 技术选型与项目架构:Django 凭什么合适
2.1 Django 的核心优势在哪
先说一个很多人容易忽略的点:Django 自带的 Admin 后台,对于企业内部管理系统来说,价值巨大。企业系统有一个天然的特点——除了面向普通员工的功能页面,还需要一堆管理用的配置界面。比如维护部门树、调整审批流程的人员配置、查看系统操作日志。Django Admin 几乎零成本就能提供这些能力,省去了一大批 CRUD 页面的开发量。
其次是 Django 的 ORM。我在这个项目里经手的查询逻辑相当多,比如"查某个部门下所有进行中的计划,按最近更新排序,并连带出负责人姓名"。这类多表关联查询在 Django ORM 里用 select_related 和 annotate 很快就能写出来,代码可读性也比手写 SQL 高一个级别。当然,ORM 不是万能的,后面我会专门讲哪些场景需要回到原生 SQL。
再一个是 Django 的"一切皆 App"的项目结构。业务边界清晰,比如 accounts 管用户、plans 管计划、approvals 管审批、notifications 管消息,每个 App 内部高内聚,App 之间通过接口调用而不是直接互相改表,这对后期的维护和团队协作非常友好。
最后就是生态成熟度。Django 从 2005 年发布到现在,经历了大量生产环境的验证。第三方包覆盖了 REST API、缓存、异步任务、部署等方方面面。更关键的是,Python 社区里关于 Django 的教程和踩坑记录非常丰富,遇到问题基本都能找到解决方案,这对团队招聘和协作也是加分项。
2.2 项目结构与模块划分
这个项目我采用的是经典的单工程多 App 结构:
plan_manage/ # 项目根目录,Django project ├── manage.py ├── config/ # 项目配置 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── accounts/ # 用户、部门、角色 ├── plans/ # 计划管理核心 ├── approvals/ # 审批流程 ├── notifications/ # 消息通知 ├── reports/ # 统计报表 └── templates/ # 共用模板这种划分的核心思想是让每个业务领域有清晰的归属。日常开发时,团队可以按 App 分模块并行开发,互不干扰。要注意的是,App 之间不要互相 import model 来做查询,而是应该通过 service 层封装好的函数去调用。比如 plans 需要知道某个用户属于哪个部门,应该调用 accounts.services.get_user_department(user),而不是直接去 from accounts.models import Department 然后自己关联查询。这层封装看似多写了几行代码,但在项目变大之后,能少踩很多坑。
技术栈上,除了 Django 本身,我是这样搭配的:
- 数据库用 PostgreSQL,因为要处理复杂的查询和 JSON 字段的存储,PostgreSQL 的 jsonb 类型非常顺手;
- 前端没有上重型框架,用了 Django 模板 + Bootstrap + 少量原生 JavaScript;
- 缓存用 Redis,主要服务消息提醒的实时性和一些高频查询的缓存;
- 异步任务用 Celery,处理计划到期提醒邮件和通知推送。
这套组合的好处是每一层都有成熟方案,没有花哨但容易失控的东西。企业内部系统,稳定压倒一切。
依赖管理我习惯用 requirements.txt,把生产依赖和开发依赖分开。生产环境只装必要的包,开发环境再加 django-debug-toolbar、ipython 这类调试工具。这样部署镜像小,依赖冲突的概率也低。
3. 核心设计:数据模型与状态流转
3.1 用户、部门与角色模型
Django 自带的 User 模型能覆盖基础的登录认证,但企业内部系统通常还需要部门、职位、汇报关系这些信息。我的做法是抽象出一个 Department 模型,再通过 OneToOne 扩展 User 获得员工信息表:
class Department(models.Model): name = models.CharField(max_length=50, unique=True) parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children') leader = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, related_name='led_department') class EmployeeProfile(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile') department = models.ForeignKey(Department, on_delete=models.PROTECT, related_name='employees') job_title = models.CharField(max_length=100) employed_at = models.DateField()Department 用自关联来维护树形结构,可以支持多级部门。这里有一个设计细节:部门负责人的外键用 OneToOne 而非 ForeignKey,因为一个部门只有一个负责人,用 OneToOne 可以在数据库层面就挡住错误数据。
EmployeeProfile 通过 OneToOne 和 User 关联,而不是直接在 User 上加字段。这么做的好处是不去动 Django 原生的认证表,后续要集成第三方认证也好处理。
角色权限这块,Django 的 Group 机制已经够用。我建了三类默认角色:普通员工、部门负责人、管理员。权限控制不写在视图里做 if 判断,而是通过 Django 的 PermissionMixin 和装饰器来做,保证每个接口的权限边界清晰。
3.2 工作计划表的设计
工作计划是系统的核心实体。我设计了两个层次:主表保存计划的整体信息,明细表保存具体的执行项。这样设计是因为一条计划往往包含多条具体任务,如果全部塞进一个表,字段重复严重,查询和统计都不方便。
class WorkPlan(models.Model): title = models.CharField(max_length=200) description = models.TextField(blank=True) creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='created_plans') owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='owned_plans') department = models.ForeignKey(Department, on_delete=models.PROTECT) status = models.CharField(max_length=20, choices=PlanStatus.choices, default=PlanStatus.DRAFT) start_date = models.DateField() end_date = models.DateField() priority = models.CharField(max_length=10, choices=Priority.choices, default=Priority.MEDIUM) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class PlanItem(models.Model): plan = models.ForeignKey(WorkPlan, on_delete=models.CASCADE, related_name='items') content = models.CharField(max_length=500) progress = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)]) due_date = models.DateField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True) order = models.IntegerField(default=0) class ApprovalRecord(models.Model): plan = models.ForeignKey(WorkPlan, on_delete=models.CASCADE, related_name='approval_records') approver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) action = models.CharField(max_length=10, choices=ApprovalAction.choices) comment = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Notification(models.Model): recipient = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notifications') content = models.CharField(max_length=500) is_read = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) plan = models.ForeignKey(WorkPlan, null=True, blank=True, on_delete=models.CASCADE)这里有几个关键决策:
- creator 和 owner 分开。创建人不一定是执行人,比如部门负责人替下属创建计划,或管理员代建计划,这在企业场景里很常见。
- status 用 CharField + choices 而不是单独建状态表。工作计划的流转状态是有限且固定的,用枚举即可,简单可靠。
- progress 只保存 0-100 的整数。为什么不用小数?因为进度在业务上通常是手动更新,精确到整数已经足够,保留小数反而容易造成无意义的精确。
- order 字段用于明细排序。必须显式排序,否则明细的展示顺序在数据库里是不保证的。
审批记录单独建表是为了审计。计划从提交到审批通过,中间可能有多次驳回和重新提交,每一次动作都要有凭据。Notification 表承担站内信功能,它是延期提醒、审批结果通知等消息的存储载体。
3.3 状态机的设计与流转规则
工作计划的生命周期我定义了这些状态:
| 状态 | 含义 | 允许流转到 |
|---|---|---|
| draft | 草稿 | submitted |
| submitted | 已提交待审批 | approved, rejected |
| approved | 已批准 | in_progress, cancelled |
| in_progress | 执行中 | completed, overdue |
| completed | 已完成 | - |
| rejected | 已驳回 | draft |
| cancelled | 已取消 | - |
状态流转的核心约束我用一个字典维护在模型层:
class PlanStatus: DRAFT = 'draft' SUBMITTED = 'submitted' APPROVED = 'approved' IN_PROGRESS = 'in_progress' COMPLETED = 'completed' REJECTED = 'rejected' CANCELLED = 'cancelled' TRANSITIONS = { DRAFT: {SUBMITTED}, SUBMITTED: {APPROVED, REJECTED}, APPROVED: {IN_PROGRESS, CANCELLED}, IN_PROGRESS: {COMPLETED}, REJECTED: {DRAFT}, CANCELLED: set(), } @classmethod def can_transition(cls, current, target): return target in cls.TRANSITIONS.get(current, set())为什么不直接用现成的状态机库比如 django-fsm?因为项目里就这么多状态,用字典维护反而清晰,不引入额外的抽象层。django-fsm 适合状态非常多、流转规则复杂的场景,我这里杀鸡不用牛刀。
状态流转必须通过统一的 service 层函数来执行,不能直接在业务代码里 plan.status = 'approved' 然后 save()。因为我需要在每次状态变更时记录操作日志和通知相关人员。把这些操作收口到一个函数里,才能保证不遗漏。
4. 核心功能模块的落地实现
4.1 用户认证与权限控制
登录认证这块,Django 的 django.contrib.auth 已经内置了 session 认证,配置好 LOGIN_URL 和登录模板就能用。但企业内部系统往往还要求记住登录状态、支持会话超时。这里我用了两层方案:
- 常规登录使用 session,设置 SESSION_COOKIE_AGE,比如企业要求半小时无操作自动退出,就设为 1800 秒;
- 如果未来需要对接移动端或做前后端分离,可以扩展 DRF + JWT。但当前系统没有这个需求,我不会提前引入复杂度。
有一个容易被忽略的设计是登录日志。我写了一个 middleware,在每次成功登录后记录 IP、UA、时间、登录结果,存到 LoginLog 表。企业系统一旦出安全问题,排查溯源全靠这层记录。别觉得"自己公司没那么大风险",数据可审计这件事,在系统设计的第一天就应该想清楚。
权限控制方面,我用 Django 的 Group + Permission。比如"提交审批"这个操作要求用户属于 employee 组,而"审批通过"要求属于 manager 组。在视图上直接加装饰器:
@login_required @permission_required('plans.submit_plan', raise_exception=True) def submit_plan(request, plan_id): ...这里的权限名不是随便起的,Django 会根据 model 自动生成 add_xxx、change_xxx、delete_xxx、view_xxx 这组权限。自定义业务权限则需要在 model 的 Meta 里显式声明:
class WorkPlan(models.Model): ... class Meta: permissions = [ ('submit_plan', 'Can submit work plan'), ('approve_plan', 'Can approve work plan'), ]有了这层声明,Django 在 migrate 时会把权限写入 auth_permission 表,然后你就可以放心地在代码里引用它们了。
4.2 计划创建与审批流程
创建计划的时候,表单除了基本字段,还要支持动态增删明细。这个前端交互如果用纯手写 JS 会比较繁琐。我的做法是后端用 formset 处理明细,前端配合简单 JS 添加/删除行。Django 的 formset 在文档里有一套完整的示例,照着来就能跑通。
需要注意 formset 的一个坑:默认情况下,formset 会根据 POST 数据里的 management_form 来解析有多少行明细。如果前端没有正确提交 TOTAL_FORMS 字段,就会出现"明明填了 5 行,后端只收到 1 行"的问题。所以前端的 formset 渲染一定要保留 management form 的隐藏字段。
审批流程我实现了一个简化版本:计划提交后,流转到创建人所在部门的负责人。如果负责人为空,则向上找到上级部门的负责人,直到找到为止。查找逻辑写成递归函数:
def find_approver(department): if department.leader: return department.leader if department.parent: return find_approver(department.parent) return None这个逻辑虽然简单,但要注意两个边界:
- 部门负责人离职了,leader 变成 NULL,递归就会一直往上找,直到找到一个为止;
- 如果整条链都没有负责人,计划会卡在 submitted 状态。所以需要后台增加一个"超时未审批自动转管理员"的兜底定时任务。
审批通过后,系统自动给计划 owner 发送通知,并把状态置为 approved。如果是驳回,则附带审批意见,状态回到 draft,创建人可以修改后重新提交。审批意见要单独存在 ApprovalRecord 表里,因为它本身就是完整的审计记录。
4.3 执行反馈与延期提醒
计划进入执行阶段后,用户需要定期更新进度。更新操作同样要收口到 service 层,因为进度变化可能触发其他动作:比如进度达到 100% 时自动尝试把计划置为 completed,如果还有未完成的明细项,则提示无法完成。
延期提醒是这个系统里比较出彩的功能。核心逻辑是每天凌晨跑一个 Celery 定时任务,扫描所有 in_progress 状态且 end_date 小于今天的计划,给负责人和部门负责人发送提醒邮件和站内消息。代码大致这样:
def check_overdue_plans(): today = timezone.localdate() overdue_plans = WorkPlan.objects.filter( status=PlanStatus.IN_PROGRESS, end_date__lt=today ) for plan in overdue_plans: notify_users(plan) log_overdue_event(plan)这个定时任务的写法不复杂,但"什么时候跑"很重要。我踩过的坑是:第一次部署时 Celery beat 的时区没设置对,结果按照默认时区凌晨触发,国内用户一大早就收到了提醒邮件,时间感完全错位。配置文件里 TIME_ZONE 和 Celery 的 timezone 参数必须和业务用户的时区保持一致。
消息通知我用 Django 模型存了一份站内信,又通过 Celery 发邮件。这里没有引入 websocket 做实时推送,因为企业内部系统的用户粘性足够,站内信+邮件已经完全能满足需求。实时推送不仅增加复杂度,还会引入长连接运维问题,在这类系统里性价比不高。
4.4 列表查询与分页优化
计划列表是所有用户每天都会打开的功能,它的性能直接决定体验。最普通的写法是:
plans = WorkPlan.objects.filter(department=dept).order_by('-created_at')如果数据量小,这样没问题。但一旦计划表超过几万行,这种写法就会暴露出 N+1 查询和全表排序的问题。我在项目里做的优化有三个:
第一,select_related 预取外键。列表页要显示负责人姓名、部门名称、创建人姓名,如果不做预取,每一行都会触发多次查询:
plans = WorkPlan.objects.select_related('owner', 'department', 'creator').filter(...)第二,分页用 Paginator 而不是手动 LIMIT/OFFSET。Django 内置 Paginator 用起来很简单,但要注意它在 count(*) 上会有一些额外开销。数据量极大时,可以重写 get_count 方法,用 estimate 或缓存 count 结果。
第三,对高频过滤条件加索引。比如我们经常按 status 和 end_date 过滤,在数据库里建联合索引:
class Meta: indexes = [ models.Index(fields=['status', 'end_date']), ]另外,列表页默认只查一个月内的计划,通过默认时间范围直接砍掉大部分数据,这是最简单也最有效的优化手段。不要总想着把所有历史数据一股脑儿全展示出来,用户真的不需要。
5. 性能、安全与代码质量的那些细节
5.1 ORM 查询优化:从入门到会用
Django ORM 用得好不好,和写 SQL 的经验直接相关。这里我分享几个实际项目中反复用到的优化点。
第一个是 annotate 做聚合。统计每个部门有多少进行中的计划,最直观的写法:
from django.db.models import Count Department.objects.annotate( active_plans=Count('plans', filter=Q(plans__status='in_progress')) )这条语句生成的 SQL 是带子查询的,在数据量大时要注意性能。可以在实际执行后用 .query 查看生成的 SQL,再决定是否需要调整。
第二个是 values 和 values_list 的使用场景。如果你只需要某几个字段,比如导出 Excel 时的数据列,用 values_list 会比加载整个对象快得多,也更省内存:
plans = WorkPlan.objects.filter(status='completed').values_list('title', 'owner__username')第三个是尽量避免在循环里执行查询。凡是遇到"循环查库"的迹象,第一反应应该是能不能用一条查询把所有数据拿出来,再用 Python 字典做 mapping。比如批量给计划设置负责人时,先从数据库取出所有用户 id 和 username 的映射,而不是每处理一条计划就查询一次用户。
第四个是删除对象的注意事项。Django 删除对象时会自动处理 CASCADE 的外键关联,但批量删除时不会触发 model 的 delete() 方法。如果你重写了 delete() 来做级联清理或记录日志,记得用 QuerySet 的 delete 时要格外小心。官方文档里明确写了这一点,实际踩坑的人却不少。
5.2 安全防护:这些基础不能省
企业系统一旦上线,就暴露在真实的安全风险里。Django 内置的安全机制很完善,但前提是你得正确启用它们。
CSRF 防护:Django 默认启用了 CsrfViewMiddleware,模板里只要用 {% csrf_token %} 就能正常提交表单。但如果写了 AJAX POST,需要把 CSRF token 放到请求头里,这是不少人第一次联调时卡住的地方。
XSS 防护:模板引擎默认会自动转义变量,比如 {{ plan.title }} 会被转义成普通的 HTML 实体。但如果你用了 |safe 过滤器或者手动拼接 HTML,就要自己为内容的安全性负责。原则是:用户输入的文本,永远不要信任。
SQL 注入防护:只要一直在用 ORM 的参数化查询,基本不用太担心。真正有风险的是写了原生 SQL 的地方,比如 .raw() 或 connection.cursor()。任何用户输入拼进 SQL 字符串都是高危行为,必须使用 %s 占位符。
Cookie 安全:如果系统涉及敏感业务数据,建议配置:
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = TrueSESSION_COOKIE_HTTPONLY 让 JavaScript 读不到 session cookie,能挡住大部分 XSS 盗取 session 的攻击。SESSION_COOKIE_SECURE 让 cookie 只在 HTTPS 连接下传输。这两项在正式环境里应该是标配。
如果你看到网上有些文章讨论 Django 设置 Cookie Token 的写法,确实可以把认证信息放进 cookie,但前提是必须搞清楚 token 的过期策略、刷新机制和存储位置。在企业内网环境里,session 方案通常比手写 token 方案更稳妥,少给自己找麻烦。
5.3 表单处理与数据完整性
Django Form 的价值不只是帮你在后端校验字段,更重要的是你能复用同一个 Form 来做前端渲染和后端验证,避免校验逻辑写两遍。
举一个实际例子:创建计划的表单里,结束日期必须晚于开始日期。这个跨字段校验要写在 clean() 方法里:
class WorkPlanForm(forms.ModelForm): class Meta: model = WorkPlan fields = ['title', 'description', 'start_date', 'end_date', 'priority'] def clean(self): cleaned = super().clean() start = cleaned.get('start_date') end = cleaned.get('end_date') if start and end and end < start: raise ValidationError('结束日期不能早于开始日期') return cleaned这个校验一旦写错位置,比如写进单个字段的 clean_end_date(),就会拿不到 start_date 的 cleaned 数据。我一开始就犯过这个错误,查了半天才发现是方法选错了。
还有个细节是数据库层的约束兜底。如果两个用户在同一个页面对同一条计划提交了不同操作,可能出现状态竞争的极端情况。为了避免脏数据,可以在模型里用 F 表达式做原子更新:
WorkPlan.objects.filter(id=plan_id, status='in_progress').update(progress=F('progress') + 10)这类原子操作保证了并发场景下的数据一致性。虽然企业内部系统并发量通常不高,但养成这个习惯没坏处。
6. 部署上线与日常运维
6.1 部署方案:从开发机到服务器
部署一个 Django 项目,网上教程很多,但跟着做的过程中经常卡在环境问题上。这里说下我实践下来最顺的一条路。
服务器用 Ubuntu 20.04 / 22.04,Python 通过 pyenv 或 apt 安装到 3.10+。项目依赖用 venv 虚拟环境隔离,不要让不同项目共享全局的 Python 包——不同项目的依赖版本经常打架,这是新手最容易踩的坑。
Web 服务用 Gunicorn 跑 Django:
gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3workers 数量一般建议是 2-4 倍 CPU 核数。实际项目中我发现先按 CPU 核数加 1 起步比较稳,然后根据内存和压力测试结果调整。worker 太多不代表性能更好,反而会因为进程切换和内存占用把机器拖垮。
反向代理用 Nginx,把 443 端口的 HTTPS 流量转发给本机 8000。静态文件由 Nginx 直接服务,不再经过 Gunicorn,性能差距很大:
location /static/ { alias /var/www/plan_manage/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里有个重要配置:在 Django 的 settings 里除了要配 STATIC_ROOT 并执行 python manage.py collectstatic,还要设置:
USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')否则在 HTTPS 环境下 Django 会认为请求来自 http,导致 SECURE 相关的重定向和 cookie 逻辑出错。
如果需要用 systemd 把 Gunicorn 托管为常驻服务,写一个 service 文件即可:
[Unit] Description=Gunicorn instance for plan_manage After=network.target [Service] User=plan_user Group=www-data WorkingDirectory=/var/www/plan_manage ExecStart=/var/www/plan_manage/venv/bin/gunicorn config.wsgi:application --workers 3 --bind 127.0.0.1:8000 Restart=always [Install] WantedBy=multi-user.targetCelery worker 和 beat 也需要同样托管。我曾经遇到过一次服务器重启后定时任务不运行的情况,就是因为忘记设 Restart=always,手动启动的 Celery 进程随 SSH 会话断开就没了。
6.2 数据库备份、日志与监控
生产环境的可靠性,一半靠部署,一半靠运维。
备份这件事不能靠"想起来再说"。我用的是每天凌晨全量备份 + 每小时增量归档(如果对数据实时性要求不高,每天一次也够)。备份命令可以这样写:
pg_dump -U plan_user -F c plan_db > /backup/plan_db_$(date +%Y%m%d_%H%M%S).dump恢复时用 pg_restore:
pg_restore -U plan_user -d plan_db --clean plan_db_xxx.dump备份文件要传到另一台机器或对象存储,千万别和数据库放在同一台服务器上。真遇到磁盘损坏,同机备份一样没命。
日志方面,Django 的 logging 配置要区分级别和去向。我的做法是:
- INFO 级别写到应用日志文件,业务操作的关键记录(比如审批通过)打成 INFO;
- ERROR 和 WARNING 单独写文件,并接入内部告警通道,出现 5xx 或数据库异常时第一时间知道。
这些配置看似不起眼,但真出了线上问题,它们是排查的第一手材料。我接手过不少项目,连最基本的 access log 都没有,出了问题全靠现场复现,那才叫举步维艰。
7. 实际开发中踩过的坑与排查速查表
7.1 典型问题与解决方案
把我在项目中遇到的问题整理成一张速查表,每条都是真金白银换来的:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 表单提交后 403 | CSRF token 缺失,尤其 AJAX 请求没带 token | 在 AJAX headers 里带 X-CSRFToken,或使用 js-cookie 读取 cookie |
| formset 只收到一行数据 | 前端没提交 management form 隐藏字段 | 保留 management form 的 TOTAL_FORMS 等隐藏字段 |
| 删除外键关联的部门报错 | 有员工的部门不能直接删 | 用 PROTECT 外键,先转移员工或标记部门停用 |
| 列表页响应特别慢 | N+1 查询 + 无索引 | select_related 预取,联合索引,减少默认查询范围 |
| Celery 定时任务不触发 | timezone 配置不一致 | 统一 TIME_ZONE 和 Celery timezone |
| 跨天日期计算差一天 | 用了 date.today() 而不是时区本地日期 | 用 timezone.localdate() 获取业务本地日期 |
| 批量删除后日志丢失 | QuerySet.delete() 不触发 model.delete() | 循环单个删除,或改用自定义管理方法 |
| 登录后 session 偶尔丢失 | 多个进程之间 SESSION_ENGINE 未指向共享存储 | 使用 Redis 或数据库作为 session backend |
| Nginx 下重定向循环 | 没配 SECURE_PROXY_SSL_HEADER | 按上文配置代理头 |
| 图片上传后访问 404 | 静态文件配置不对 | 检查 MEDIA_ROOT/MEDIA_URL 和 Nginx alias |
这张表不是让读者死记硬背,而是要建立一种排查思路:先确认现象,再判断是前端的锅、Django 的锅还是服务器的锅,最后用日志和调试工具定位。
7.2 几个我坚持了几年的实战习惯
最后分享几个在整这个项目过程中验证过的习惯,可能看起来都太"基础"了,但长期收益非常大。
第一,所有业务状态变更必须走 service 函数。不要在视图里直接改 model 字段。哪怕只是改一个 status,也统一封装。这样当你需要在状态变更时加日志、发通知、更新统计时,只改一个地方。
第二,能用 Debug Toolbar 调优的,就尽量在开发期解决。django-debug-toolbar 能在浏览器侧边栏直接展示每次请求的 SQL 条数、时间和重复查询,它的价值不是"看个热闹",而是帮你把明显的 N+1 和慢查询消灭在发布之前。我见过太多项目上线之后才发现列表页要 5 秒,就是因为开发期没有这种检查工具。
第三,写 migrations 之前一定想清楚字段类型。PostgreSQL 上改字段类型的代价比大多数人想象的大。比如把 CharField 改成 TextField,数据量级上百万时,锁表时间足够让业务瘫痪一阵。所以一开始就不要偷懒,该用 TextField 的就用 TextField,别预留"以后再说"的隐患。
第四,Cookie 和 Token 相关的问题,先在浏览器开发者工具里看实际发送的请求头。很多时候我们盯着代码看半天,其实问题就出在请求里根本没带上期望的 header。按 F12 打开 Network 面板,请求、响应、cookie、状态码全都一目了然。
这套 Django 系统从需求梳理到上线运行,前后大概花了一个半月。说实话,Django 的学习曲线并不陡,真正花时间的是业务建模和边界情况的设计。如果你正准备做一个企业内部的业务系统,希望这篇内容能帮你避开一些我已经踩过的坑。哪天你也在做类似的东西,回来聊聊你怎么处理审批流和状态机的细节。