做企业管理系统的开发绕不开一个经典场景工作计划的制定、下达、执行、跟踪和复盘。这几年我经手过不少类似的内部系统从 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_length50, uniqueTrue) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namechildren) leader models.OneToOneField(settings.AUTH_USER_MODEL, nullTrue, on_deletemodels.SET_NULL, related_nameled_department) class EmployeeProfile(models.Model): user models.OneToOneField(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_nameprofile) department models.ForeignKey(Department, on_deletemodels.PROTECT, related_nameemployees) job_title models.CharField(max_length100) 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_length200) description models.TextField(blankTrue) creator models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_namecreated_plans) owner models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_nameowned_plans) department models.ForeignKey(Department, on_deletemodels.PROTECT) status models.CharField(max_length20, choicesPlanStatus.choices, defaultPlanStatus.DRAFT) start_date models.DateField() end_date models.DateField() priority models.CharField(max_length10, choicesPriority.choices, defaultPriority.MEDIUM) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class PlanItem(models.Model): plan models.ForeignKey(WorkPlan, on_deletemodels.CASCADE, related_nameitems) content models.CharField(max_length500) progress models.IntegerField(default0, validators[MinValueValidator(0), MaxValueValidator(100)]) due_date models.DateField(nullTrue, blankTrue) completed_at models.DateTimeField(nullTrue, blankTrue) order models.IntegerField(default0) class ApprovalRecord(models.Model): plan models.ForeignKey(WorkPlan, on_deletemodels.CASCADE, related_nameapproval_records) approver models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT) action models.CharField(max_length10, choicesApprovalAction.choices) comment models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Notification(models.Model): recipient models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namenotifications) content models.CharField(max_length500) is_read models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) plan models.ForeignKey(WorkPlan, nullTrue, blankTrue, on_deletemodels.CASCADE)这里有几个关键决策creator 和 owner 分开。创建人不一定是执行人比如部门负责人替下属创建计划或管理员代建计划这在企业场景里很常见。status 用 CharField choices 而不是单独建状态表。工作计划的流转状态是有限且固定的用枚举即可简单可靠。progress 只保存 0-100 的整数。为什么不用小数因为进度在业务上通常是手动更新精确到整数已经足够保留小数反而容易造成无意义的精确。order 字段用于明细排序。必须显式排序否则明细的展示顺序在数据库里是不保证的。审批记录单独建表是为了审计。计划从提交到审批通过中间可能有多次驳回和重新提交每一次动作都要有凭据。Notification 表承担站内信功能它是延期提醒、审批结果通知等消息的存储载体。3.3 状态机的设计与流转规则工作计划的生命周期我定义了这些状态状态含义允许流转到draft草稿submittedsubmitted已提交待审批approved, rejectedapproved已批准in_progress, cancelledin_progress执行中completed, overduecompleted已完成-rejected已驳回draftcancelled已取消-状态流转的核心约束我用一个字典维护在模型层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_exceptionTrue) 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( statusPlanStatus.IN_PROGRESS, end_date__lttoday ) 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(departmentdept).order_by(-created_at)如果数据量小这样没问题。但一旦计划表超过几万行这种写法就会暴露出 N1 查询和全表排序的问题。我在项目里做的优化有三个第一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_plansCount(plans, filterQ(plans__statusin_progress)) )这条语句生成的 SQL 是带子查询的在数据量大时要注意性能。可以在实际执行后用 .query 查看生成的 SQL再决定是否需要调整。第二个是 values 和 values_list 的使用场景。如果你只需要某几个字段比如导出 Excel 时的数据列用 values_list 会比加载整个对象快得多也更省内存plans WorkPlan.objects.filter(statuscompleted).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(idplan_id, statusin_progress).update(progressF(progress) 10)这类原子操作保证了并发场景下的数据一致性。虽然企业内部系统并发量通常不高但养成这个习惯没坏处。6. 部署上线与日常运维6.1 部署方案从开发机到服务器部署一个 Django 项目网上教程很多但跟着做的过程中经常卡在环境问题上。这里说下我实践下来最顺的一条路。服务器用 Ubuntu 20.04 / 22.04Python 通过 pyenv 或 apt 安装到 3.10。项目依赖用 venv 虚拟环境隔离不要让不同项目共享全局的 Python 包——不同项目的依赖版本经常打架这是新手最容易踩的坑。Web 服务用 Gunicorn 跑 Djangogunicorn 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] DescriptionGunicorn instance for plan_manage Afternetwork.target [Service] Userplan_user Groupwww-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 Restartalways [Install] WantedBymulti-user.targetCelery worker 和 beat 也需要同样托管。我曾经遇到过一次服务器重启后定时任务不运行的情况就是因为忘记设 Restartalways手动启动的 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_restorepg_restore -U plan_user -d plan_db --clean plan_db_xxx.dump备份文件要传到另一台机器或对象存储千万别和数据库放在同一台服务器上。真遇到磁盘损坏同机备份一样没命。日志方面Django 的 logging 配置要区分级别和去向。我的做法是INFO 级别写到应用日志文件业务操作的关键记录比如审批通过打成 INFOERROR 和 WARNING 单独写文件并接入内部告警通道出现 5xx 或数据库异常时第一时间知道。这些配置看似不起眼但真出了线上问题它们是排查的第一手材料。我接手过不少项目连最基本的 access log 都没有出了问题全靠现场复现那才叫举步维艰。7. 实际开发中踩过的坑与排查速查表7.1 典型问题与解决方案把我在项目中遇到的问题整理成一张速查表每条都是真金白银换来的问题现象根本原因解决方案表单提交后 403CSRF token 缺失尤其 AJAX 请求没带 token在 AJAX headers 里带 X-CSRFToken或使用 js-cookie 读取 cookieformset 只收到一行数据前端没提交 management form 隐藏字段保留 management form 的 TOTAL_FORMS 等隐藏字段删除外键关联的部门报错有员工的部门不能直接删用 PROTECT 外键先转移员工或标记部门停用列表页响应特别慢N1 查询 无索引select_related 预取联合索引减少默认查询范围Celery 定时任务不触发timezone 配置不一致统一 TIME_ZONE 和 Celery timezone跨天日期计算差一天用了 date.today() 而不是时区本地日期用 timezone.localdate() 获取业务本地日期批量删除后日志丢失QuerySet.delete() 不触发 model.delete()循环单个删除或改用自定义管理方法登录后 session 偶尔丢失多个进程之间 SESSION_ENGINE 未指向共享存储使用 Redis 或数据库作为 session backendNginx 下重定向循环没配 SECURE_PROXY_SSL_HEADER按上文配置代理头图片上传后访问 404静态文件配置不对检查 MEDIA_ROOT/MEDIA_URL 和 Nginx alias这张表不是让读者死记硬背而是要建立一种排查思路先确认现象再判断是前端的锅、Django 的锅还是服务器的锅最后用日志和调试工具定位。7.2 几个我坚持了几年的实战习惯最后分享几个在整这个项目过程中验证过的习惯可能看起来都太基础了但长期收益非常大。第一所有业务状态变更必须走 service 函数。不要在视图里直接改 model 字段。哪怕只是改一个 status也统一封装。这样当你需要在状态变更时加日志、发通知、更新统计时只改一个地方。第二能用 Debug Toolbar 调优的就尽量在开发期解决。django-debug-toolbar 能在浏览器侧边栏直接展示每次请求的 SQL 条数、时间和重复查询它的价值不是看个热闹而是帮你把明显的 N1 和慢查询消灭在发布之前。我见过太多项目上线之后才发现列表页要 5 秒就是因为开发期没有这种检查工具。第三写 migrations 之前一定想清楚字段类型。PostgreSQL 上改字段类型的代价比大多数人想象的大。比如把 CharField 改成 TextField数据量级上百万时锁表时间足够让业务瘫痪一阵。所以一开始就不要偷懒该用 TextField 的就用 TextField别预留以后再说的隐患。第四Cookie 和 Token 相关的问题先在浏览器开发者工具里看实际发送的请求头。很多时候我们盯着代码看半天其实问题就出在请求里根本没带上期望的 header。按 F12 打开 Network 面板请求、响应、cookie、状态码全都一目了然。这套 Django 系统从需求梳理到上线运行前后大概花了一个半月。说实话Django 的学习曲线并不陡真正花时间的是业务建模和边界情况的设计。如果你正准备做一个企业内部的业务系统希望这篇内容能帮你避开一些我已经踩过的坑。哪天你也在做类似的东西回来聊聊你怎么处理审批流和状态机的细节。
阅读完成 · 觉得有帮助?