Appearance
章节6:模块综合实战
一、章节概述
本章将前五章的知识融会贯通,通过两个完整的实战项目巩固所学内容。项目一构建博客系统后台(ORM + Admin + 模板),采用传统的服务端渲染架构;项目二构建企业员工管理系统 API(DRF + JWT + RBAC),采用前后端分离架构。两个项目覆盖了 Django 开发的核心场景,完成后学生将具备独立开发 Django 项目的能力。
二、学习目标
| 目标分类 | 具体目标 |
|---|---|
| 知识目标 | 理解传统服务端渲染与前后端分离两种架构的设计差异;掌握完整项目从需求分析到编码实现的全流程 |
| 技能目标 | 能独立完成 Django 项目的架构设计、模型设计、业务逻辑编码与接口调试 |
| 素养目标 | 培养项目工程化思维;养成编写可读性高、可维护性强的代码习惯 |
项目一:博客系统后台(ORM + Admin + 模板)
1.1 项目需求
1.1.1 功能需求
| 功能模块 | 需求描述 |
|---|---|
| 文章管理 | 文章的增删改查;支持 Markdown 编辑;文章分类与标签;草稿/发布状态管理 |
| 分类管理 | 分类的增删改查;分类下文章数量统计 |
| 标签管理 | 标签的增删改查;标签云展示 |
| 评论管理 | 文章评论审核;嵌套评论展示 |
| 用户管理 | 用户注册登录;个人中心;头像上传 |
| Admin 后台 | 完善的后台管理界面;数据统计看板;批量操作 |
| 前端展示 | 文章列表页(分页);文章详情页(含评论);分类/标签筛选;侧栏(最新文章、热门文章、分类、标签云) |
1.1.2 技术栈
Python 3.12+ + Django 5.0+ + SQLite/PostgreSQL
模板引擎:Django Templates + Bootstrap 5
富文本编辑:django-markdownx
数据库:SQLite(开发)/ PostgreSQL(生产)1.2 项目结构
myblog/
├── manage.py
├── myblog/ # 项目配置
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── blog/ # 博客应用
│ ├── __init__.py
│ ├── admin.py # Admin 配置
│ ├── apps.py
│ ├── models.py # 数据模型
│ ├── urls.py # 路由
│ ├── views.py # 视图
│ ├── forms.py # 表单
│ ├── templatetags/ # 自定义模板标签
│ │ ├── __init__.py
│ │ └── blog_tags.py
│ ├── migrations/
│ ├── templates/ # 模板
│ │ ├── base.html
│ │ ├── blog/
│ │ │ ├── index.html
│ │ │ ├── article_list.html
│ │ │ ├── article_detail.html
│ │ │ ├── article_form.html
│ │ │ ├── category_list.html
│ │ │ └── search.html
│ │ └── includes/
│ │ ├── sidebar.html
│ │ └── pagination.html
│ └── static/
│ └── blog/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── main.js
└── media/ # 用户上传文件
└── avatars/1.3 模型设计
python
# blog/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.text import slugify
from django.urls import reverse
from markdownx.models import MarkdownxField
from markdownx.utils import markdownify
class User(AbstractUser):
"""自定义用户模型"""
bio = models.TextField(max_length=500, blank=True, verbose_name='个人简介')
avatar = models.ImageField(upload_to='avatars/%Y/%m/', blank=True, verbose_name='头像')
website = models.URLField(blank=True, verbose_name='个人网站')
class Meta:
verbose_name = '用户'
verbose_name_plural = '用户'
def __str__(self):
return self.username
class Category(models.Model):
"""文章分类"""
name = models.CharField(max_length=100, unique=True, verbose_name='分类名称')
slug = models.SlugField(max_length=120, unique=True, blank=True, verbose_name='URL 标识')
description = models.TextField(blank=True, verbose_name='分类描述')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
def article_count(self):
"""分类下的文章数量"""
return self.articles.count()
article_count.short_description = '文章数量'
class Tag(models.Model):
"""文章标签"""
name = models.CharField(max_length=50, unique=True, verbose_name='标签名称')
slug = models.SlugField(max_length=80, unique=True, blank=True, verbose_name='URL 标识')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
ordering = ['name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Article(models.Model):
"""文章模型"""
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
('archived', '已归档'),
]
title = models.CharField(max_length=200, verbose_name='标题')
slug = models.SlugField(max_length=220, unique=True, blank=True, verbose_name='URL 标识')
content = MarkdownxField(verbose_name='内容(Markdown)')
summary = models.TextField(max_length=300, blank=True, verbose_name='摘要')
category = models.ForeignKey(
Category, on_delete=models.SET_NULL, null=True,
related_name='articles', verbose_name='分类'
)
tags = models.ManyToManyField(Tag, blank=True, related_name='articles', verbose_name='标签')
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='articles', verbose_name='作者'
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name='状态')
views = models.PositiveIntegerField(default=0, verbose_name='阅读量')
likes = models.PositiveIntegerField(default=0, verbose_name='点赞数')
cover_image = models.ImageField(upload_to='articles/covers/%Y/%m/', blank=True, verbose_name='封面图')
is_pinned = models.BooleanField(default=False, verbose_name='是否置顶')
allow_comments = models.BooleanField(default=True, verbose_name='允许评论')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
published_at = models.DateTimeField(null=True, blank=True, verbose_name='发布时间')
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-is_pinned', '-published_at', '-created_at']
indexes = [
models.Index(fields=['status', 'published_at']),
models.Index(fields=['slug']),
]
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
if self.status == 'published' and not self.published_at:
from django.utils import timezone
self.published_at = timezone.now()
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('blog:article_detail', kwargs={'slug': self.slug})
@property
def content_html(self):
"""将 Markdown 渲染为 HTML"""
return markdownify(self.content)
@property
def summary_text(self):
"""自动生成摘要"""
if self.summary:
return self.summary
# 去除 Markdown 标记,取前 200 字符
import re
text = re.sub(r'[#*>`\[\]\(\)!\-]', '', self.content)
return text[:200] + '...' if len(text) > 200 else text
def __str__(self):
return self.title
class Comment(models.Model):
"""文章评论"""
article = models.ForeignKey(
Article, on_delete=models.CASCADE, related_name='comments', verbose_name='文章'
)
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name='comments', verbose_name='评论者'
)
parent = models.ForeignKey(
'self', on_delete=models.CASCADE, null=True, blank=True,
related_name='replies', verbose_name='父评论'
)
content = models.TextField(verbose_name='评论内容')
is_approved = models.BooleanField(default=False, verbose_name='是否审核通过')
ip_address = models.GenericIPAddressField(blank=True, null=True, verbose_name='IP 地址')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
class Meta:
verbose_name = '评论'
verbose_name_plural = '评论'
ordering = ['created_at']
def __str__(self):
return f'{self.author.username} 评论 {self.article.title}'1.4 Admin 配置
python
# blog/admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.utils import timezone
from django.db.models import Count, Sum
from .models import Article, Category, Tag, Comment, User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ['username', 'email', 'article_count', 'comment_count', 'date_joined']
search_fields = ['username', 'email']
def article_count(self, obj):
return obj.articles.count()
article_count.short_description = '文章数'
def comment_count(self, obj):
return obj.comments.count()
comment_count.short_description = '评论数'
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'article_count', 'created_at']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def article_count(self, obj):
count = obj.articles.filter(status='published').count()
url = f'/admin/blog/article/?category__id__exact={obj.id}'
return format_html('<a href="{}">{}</a>', url, count)
article_count.short_description = '文章数'
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'article_count', 'created_at']
search_fields = ['name']
prepopulated_fields = {'slug': ('name',)}
def article_count(self, obj):
return obj.articles.count()
article_count.short_description = '文章数'
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
fields = ['author', 'content_preview', 'is_approved', 'created_at']
readonly_fields = ['created_at']
autocomplete_fields = ['author']
def content_preview(self, obj):
return obj.content[:60] + '...' if len(obj.content) > 60 else obj.content
content_preview.short_description = '评论内容'
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = [
'title', 'category', 'author', 'status_badge',
'views', 'likes', 'comment_count', 'created_at',
]
list_filter = ['status', 'category', 'is_pinned', 'author', 'created_at']
search_fields = ['title', 'content']
list_per_page = 20
date_hierarchy = 'created_at'
filter_horizontal = ['tags']
autocomplete_fields = ['category', 'author']
prepopulated_fields = {'slug': ('title',)}
readonly_fields = ['views', 'likes', 'created_at', 'updated_at', 'published_at', 'content_preview']
fieldsets = [
('基本信息', {
'fields': ['title', 'slug', 'category', 'tags', 'author'],
}),
('内容', {
'fields': ['content', 'summary', 'cover_image'],
}),
('发布设置', {
'fields': ['status', 'is_pinned', 'allow_comments', 'published_at'],
'classes': ['collapse'],
}),
('统计信息', {
'fields': ['views', 'likes', 'created_at', 'updated_at'],
'classes': ['collapse'],
}),
]
inlines = [CommentInline]
def status_badge(self, obj):
colors = {
'draft': 'gray',
'published': 'green',
'archived': 'orange',
}
labels = {
'draft': '草稿',
'published': '已发布',
'archived': '已归档',
}
return format_html(
'<span style="color:{};font-weight:bold;">● {}</span>',
colors.get(obj.status, 'gray'),
labels.get(obj.status, obj.status),
)
status_badge.short_description = '状态'
def comment_count(self, obj):
count = obj.comments.filter(is_approved=True).count()
return count
comment_count.short_description = '评论数'
def content_preview(self, obj):
if obj.content:
return format_html(
'<div style="max-height:300px;overflow-y:auto;padding:8px;'
'background:#f8f9fa;border-radius:4px;">{}</div>',
obj.content_html
)
return '-'
content_preview.short_description = '内容预览'
@admin.action(description='批量发布')
def make_published(self, request, queryset):
updated = queryset.update(
status='published',
published_at=timezone.now(),
)
self.message_user(request, f'已发布 {updated} 篇文章')
@admin.action(description='批量归档')
def make_archived(self, request, queryset):
updated = queryset.update(status='archived')
self.message_user(request, f'已归档 {updated} 篇文章')
actions = ['make_published', 'make_archived']
def save_model(self, request, obj, form, change):
if not change:
obj.author = request.user
super().save_model(request, obj, form, change)
def get_queryset(self, request):
qs = super().get_queryset(request)
if not request.user.is_superuser:
qs = qs.filter(author=request.user)
return qs
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['content_preview', 'article', 'author', 'is_approved_status', 'created_at']
list_filter = ['is_approved', 'created_at', 'article']
search_fields = ['content', 'author__username']
autocomplete_fields = ['article', 'author', 'parent']
list_editable = ['is_approved']
def content_preview(self, obj):
return obj.content[:80] + '...' if len(obj.content) > 80 else obj.content
content_preview.short_description = '评论内容'
def is_approved_status(self, obj):
if obj.is_approved:
return format_html('<span style="color:green;">✓ 已审核</span>')
return format_html('<span style="color:orange;">✗ 待审核</span>')
is_approved_status.short_description = '审核状态'
@admin.action(description='批量通过审核')
def approve_comments(self, request, queryset):
updated = queryset.update(is_approved=True)
self.message_user(request, f'已审核通过 {updated} 条评论')
actions = ['approve_comments']1.5 视图与路由
python
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib import messages
from django.db.models import Count, Q
from django.urls import reverse_lazy
from .models import Article, Category, Tag, Comment
class IndexView(ListView):
"""首页:展示已发布文章列表"""
model = Article
template_name = 'blog/index.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
return Article.objects.filter(status='published').select_related(
'author', 'category'
).prefetch_related('tags')
class ArticleDetailView(DetailView):
"""文章详情"""
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
slug_field = 'slug'
def get_queryset(self):
return Article.objects.select_related('author', 'category').prefetch_related(
'tags', 'comments__author'
)
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
# 阅读量 +1(使用 Cookie 防止重复计数)
if not request.COOKIES.get(f'viewed_article_{self.object.pk}'):
Article.objects.filter(pk=self.object.pk).update(views=models.F('views') + 1)
response.set_cookie(f'viewed_article_{self.object.pk}', '1', max_age=3600)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
article = self.object
# 上一篇/下一篇
context['prev_article'] = Article.objects.filter(
status='published', published_at__lt=article.published_at
).first()
context['next_article'] = Article.objects.filter(
status='published', published_at__gt=article.published_at
).last()
# 评论(仅审核通过的)
context['comments'] = article.comments.filter(is_approved=True, parent=None)
return context
class CategoryView(ListView):
"""按分类展示文章"""
template_name = 'blog/category_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
self.category = get_object_or_404(Category, slug=self.kwargs['slug'])
return Article.objects.filter(
status='published', category=self.category
).select_related('author')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = self.category
return context
class TagView(ListView):
"""按标签展示文章"""
template_name = 'blog/tag_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
self.tag = get_object_or_404(Tag, slug=self.kwargs['slug'])
return self.tag.articles.filter(status='published').select_related('author')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['tag'] = self.tag
return context
class SearchView(ListView):
"""搜索文章"""
template_name = 'blog/search.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
self.query = self.request.GET.get('q', '').strip()
if self.query:
return Article.objects.filter(
Q(status='published'),
Q(title__icontains=self.query) | Q(content__icontains=self.query)
).select_related('author')
return Article.objects.none()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['query'] = self.query
return context
@login_required
def add_comment(request, slug):
"""添加评论"""
article = get_object_or_404(Article, slug=slug, status='published')
if request.method == 'POST':
content = request.POST.get('content', '').strip()
parent_id = request.POST.get('parent_id')
if content:
comment = Comment.objects.create(
article=article,
author=request.user,
content=content,
parent_id=parent_id if parent_id else None,
)
messages.success(request, '评论提交成功,等待审核')
else:
messages.error(request, '评论内容不能为空')
return redirect('blog:article_detail', slug=slug)python
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('article/<slug:slug>/', views.ArticleDetailView.as_view(), name='article_detail'),
path('category/<slug:slug>/', views.CategoryView.as_view(), name='category'),
path('tag/<slug:slug>/', views.TagView.as_view(), name='tag'),
path('search/', views.SearchView.as_view(), name='search'),
path('article/<slug:slug>/comment/', views.add_comment, name='add_comment'),
]1.6 模板示例
html
{# templates/blog/article_detail.html #}
{% extends 'base.html' %}
{% load blog_tags %}
{% load static %}
{% block title %}{{ article.title }} - 我的博客{% endblock %}
{% block content %}
<article class="article-detail">
<header class="article-header">
<h1>{{ article.title }}</h1>
<div class="meta">
<span class="author">
<img src="{{ article.author.avatar.url|default:'/static/images/default-avatar.png' }}"
alt="" class="avatar-sm">
{{ article.author.username }}
</span>
<span class="date">{{ article.published_at|date:'Y年m月d日' }}</span>
<span class="category">
<a href="{% url 'blog:category' article.category.slug %}">
{{ article.category.name }}
</a>
</span>
<span class="views">👁 {{ article.views }}</span>
<span class="likes">❤ {{ article.likes }}</span>
</div>
{% if article.tags.all %}
<div class="tags">
{% for tag in article.tags.all %}
<a href="{% url 'blog:tag' tag.slug %}" class="tag">#{{ tag.name }}</a>
{% endfor %}
</div>
{% endif %}
</header>
{% if article.cover_image %}
<div class="cover-image">
<img src="{{ article.cover_image.url }}" alt="{{ article.title }}">
</div>
{% endif %}
<div class="article-content">
{{ article.content_html|safe }}
</div>
</article>
<section class="comments">
<h3>评论 ({{ comments|length }})</h3>
{% if user.is_authenticated %}
<form method="post" action="{% url 'blog:add_comment' article.slug %}" class="comment-form">
{% csrf_token %}
<textarea name="content" rows="3" placeholder="写下你的评论..." required></textarea>
<button type="submit">提交评论</button>
</form>
{% else %}
<p><a href="{% url 'login' %}">登录</a>后可以发表评论</p>
{% endif %}
{% for comment in comments %}
<div class="comment">
<div class="comment-header">
<strong>{{ comment.author.username }}</strong>
<span>{{ comment.created_at|timesince }}前</span>
</div>
<p>{{ comment.content }}</p>
{% for reply in comment.replies.all %}
<div class="reply">
<strong>{{ reply.author.username }}</strong>
<p>{{ reply.content }}</p>
</div>
{% endfor %}
</div>
{% empty %}
<p>暂无评论</p>
{% endfor %}
</section>
{% endblock %}1.7 启动运行
bash
# 1. 创建虚拟环境
python -m venv venv
venv\Scripts\activate # Windows
source venv/bin/activate # Mac/Linux
# 2. 安装依赖
pip install django==5.0.2 django-markdownx Pillow
# 3. 迁移数据库
python manage.py makemigrations
python manage.py migrate
# 4. 创建超级管理员
python manage.py createsuperuser
# 5. 启动开发服务器
python manage.py runserver
# 访问:http://127.0.0.1:8000/
# 后台:http://127.0.0.1:8000/admin/项目二:企业员工管理系统 API(DRF + JWT + RBAC)
2.1 项目需求
2.1.1 功能需求
| 功能模块 | 需求描述 |
|---|---|
| 员工管理 | 员工的增删改查;支持多条件筛选(部门/职级/状态);员工详情页包含个人信息、工作信息、薪资信息 |
| 部门管理 | 部门的增删改查;树形部门结构(父子部门);部门人数统计 |
| 职级管理 | 职级的增删改查;职级晋升记录 |
| 考勤管理 | 每日打卡(签到/签退);考勤统计(出勤率、迟到/早退次数) |
| 薪资管理 | 员工薪资记录;薪资调整历史;薪资统计报表 |
| 用户认证 | JWT 认证;角色权限(管理员/HR/部门经理/员工);用户注册与个人信息管理 |
2.1.2 技术栈
Python 3.12+ + Django 5.0+ + DRF + PostgreSQL
认证:djangorestframework-simplejwt
权限:RBAC(Django Group + Permission)
文档:drf-spectacular
测试:pytest + pytest-django2.2 项目结构
employee_management/
├── manage.py
├── employee_management/ # 项目配置
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── accounts/ # 用户认证应用
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ ├── views.py
│ └── permissions.py
├── employee/ # 员工管理应用
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ ├── views.py
│ ├── filters.py
│ └── permissions.py
├── attendance/ # 考勤管理应用
│ ├── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
├── salary/ # 薪资管理应用
│ ├── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
└── requirements.txt2.3 模型设计
python
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""企业用户模型"""
ROLE_CHOICES = [
('admin', '系统管理员'),
('hr', '人力资源'),
('manager', '部门经理'),
('employee', '普通员工'),
]
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='employee', verbose_name='角色')
phone = models.CharField(max_length=20, unique=True, verbose_name='手机号')
avatar = models.ImageField(upload_to='avatars/', blank=True, verbose_name='头像')
is_active_account = models.BooleanField(default=True, verbose_name='账号是否激活')
class Meta:
verbose_name = '用户'
verbose_name_plural = '用户'
def __str__(self):
return f'{self.username} ({self.get_role_display()})'python
# employee/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Department(models.Model):
"""部门"""
name = models.CharField(max_length=100, unique=True, verbose_name='部门名称')
parent = models.ForeignKey(
'self', on_delete=models.SET_NULL, null=True, blank=True,
related_name='children', verbose_name='上级部门'
)
manager = models.OneToOneField(
User, on_delete=models.SET_NULL, null=True, blank=True,
related_name='managed_department', verbose_name='部门经理'
)
description = models.TextField(blank=True, verbose_name='部门描述')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
verbose_name = '部门'
verbose_name_plural = '部门'
ordering = ['name']
def __str__(self):
return self.name
@property
def employee_count(self):
return self.employees.filter(status='active').count()
class Rank(models.Model):
"""职级"""
name = models.CharField(max_length=50, unique=True, verbose_name='职级名称')
level = models.IntegerField(unique=True, verbose_name='职级等级(数字越小越高)')
description = models.TextField(blank=True, verbose_name='职级描述')
min_salary = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='最低薪资')
max_salary = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='最高薪资')
class Meta:
verbose_name = '职级'
verbose_name_plural = '职级'
ordering = ['level']
def __str__(self):
return f'{self.name} (Level {self.level})'
class Employee(models.Model):
"""员工"""
GENDER_CHOICES = [('M', '男'), ('F', '女')]
STATUS_CHOICES = [
('active', '在职'),
('probation', '试用期'),
('leave', '离职'),
('retired', '退休'),
]
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name='employee_profile', verbose_name='关联用户'
)
# 基本信息
employee_id = models.CharField(max_length=20, unique=True, verbose_name='工号')
name = models.CharField(max_length=100, verbose_name='姓名')
gender = models.CharField(max_length=2, choices=GENDER_CHOICES, verbose_name='性别')
birth_date = models.DateField(verbose_name='出生日期')
id_card = models.CharField(max_length=18, unique=True, verbose_name='身份证号')
# 工作信息
department = models.ForeignKey(
Department, on_delete=models.SET_NULL, null=True,
related_name='employees', verbose_name='所属部门'
)
rank = models.ForeignKey(
Rank, on_delete=models.SET_NULL, null=True,
related_name='employees', verbose_name='职级'
)
position = models.CharField(max_length=100, verbose_name='职位')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', verbose_name='状态')
# 联系信息
email = models.EmailField(verbose_name='邮箱')
phone = models.CharField(max_length=20, verbose_name='联系电话')
address = models.TextField(blank=True, verbose_name='家庭地址')
# 时间信息
hire_date = models.DateField(verbose_name='入职日期')
probation_end = models.DateField(null=True, blank=True, verbose_name='试用期结束日期')
resign_date = models.DateField(null=True, blank=True, verbose_name='离职日期')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '员工'
verbose_name_plural = '员工'
ordering = ['employee_id']
indexes = [
models.Index(fields=['status', 'department']),
models.Index(fields=['employee_id']),
]
def __str__(self):
return f'{self.name} ({self.employee_id})'
@property
def work_duration(self):
"""工作年限"""
from datetime import date
if self.resign_date:
end = self.resign_date
else:
end = date.today()
return (end - self.hire_date).days // 365
class PromotionRecord(models.Model):
"""晋升记录"""
employee = models.ForeignKey(
Employee, on_delete=models.CASCADE, related_name='promotions', verbose_name='员工'
)
from_rank = models.ForeignKey(
Rank, on_delete=models.SET_NULL, null=True, related_name='+', verbose_name='原职级'
)
to_rank = models.ForeignKey(
Rank, on_delete=models.SET_NULL, null=True, related_name='+', verbose_name='新职级'
)
approved_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name='审批人'
)
reason = models.TextField(verbose_name='晋升原因')
effective_date = models.DateField(verbose_name='生效日期')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '晋升记录'
verbose_name_plural = '晋升记录'
ordering = ['-effective_date']
def __str__(self):
return f'{self.employee.name}: {self.from_rank} → {self.to_rank}'python
# attendance/models.py
from django.db import models
from django.contrib.auth import get_user_model
from employee.models import Employee
class Attendance(models.Model):
"""考勤记录"""
employee = models.ForeignKey(
Employee, on_delete=models.CASCADE, related_name='attendances', verbose_name='员工'
)
date = models.DateField(verbose_name='日期')
check_in = models.TimeField(null=True, blank=True, verbose_name='签到时间')
check_out = models.TimeField(null=True, blank=True, verbose_name='签退时间')
STATUS_CHOICES = [
('normal', '正常'),
('late', '迟到'),
('early', '早退'),
('absent', '缺勤'),
('leave', '请假'),
('overtime', '加班'),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='normal', verbose_name='状态')
remark = models.TextField(blank=True, verbose_name='备注')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '考勤记录'
verbose_name_plural = '考勤记录'
unique_together = ['employee', 'date']
ordering = ['-date']
def __str__(self):
return f'{self.employee.name} - {self.date}'python
# salary/models.py
from django.db import models
from employee.models import Employee
class Salary(models.Model):
"""薪资记录"""
employee = models.ForeignKey(
Employee, on_delete=models.CASCADE, related_name='salaries', verbose_name='员工'
)
month = models.DateField(verbose_name='薪资月份') # 存储月份的第一天
base_salary = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='基本工资')
performance_bonus = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='绩效奖金')
overtime_pay = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='加班费')
allowance = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='补贴')
insurance = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='社保扣款')
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='个税扣款')
other_deductions = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='其他扣款')
@property
def gross_pay(self):
"""应发工资"""
return self.base_salary + self.performance_bonus + self.overtime_pay + self.allowance
@property
def net_pay(self):
"""实发工资"""
return self.gross_pay - self.insurance - self.tax - self.other_deductions
is_paid = models.BooleanField(default=False, verbose_name='是否已发放')
paid_at = models.DateTimeField(null=True, blank=True, verbose_name='发放时间')
remark = models.TextField(blank=True, verbose_name='备注')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = '薪资记录'
verbose_name_plural = '薪资记录'
unique_together = ['employee', 'month']
ordering = ['-month']
def __str__(self):
return f'{self.employee.name} - {self.month.strftime("%Y年%m月")}'2.4 序列化器
python
# employee/serializers.py
from rest_framework import serializers
from .models import Department, Rank, Employee, PromotionRecord
class DepartmentSerializer(serializers.ModelSerializer):
"""部门序列化器"""
employee_count = serializers.IntegerField(read_only=True)
manager_name = serializers.CharField(source='manager.username', read_only=True)
children_count = serializers.SerializerMethodField()
class Meta:
model = Department
fields = '__all__'
def get_children_count(self, obj):
return obj.children.count()
class DepartmentTreeSerializer(serializers.ModelSerializer):
"""部门树形序列化器"""
children = serializers.SerializerMethodField()
employee_count = serializers.IntegerField(read_only=True)
class Meta:
model = Department
fields = ['id', 'name', 'employee_count', 'children']
def get_children(self, obj):
children = obj.children.all()
if children:
return DepartmentTreeSerializer(children, many=True).data
return []
class RankSerializer(serializers.ModelSerializer):
"""职级序列化器"""
employee_count = serializers.SerializerMethodField()
class Meta:
model = Rank
fields = '__all__'
def get_employee_count(self, obj):
return obj.employees.count()
class EmployeeListSerializer(serializers.ModelSerializer):
"""员工列表序列化器(精简)"""
department_name = serializers.CharField(source='department.name', read_only=True)
rank_name = serializers.CharField(source='rank.name', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Employee
fields = [
'id', 'employee_id', 'name', 'gender', 'department_name',
'rank_name', 'position', 'status', 'status_display',
'hire_date', 'phone',
]
class EmployeeDetailSerializer(serializers.ModelSerializer):
"""员工详情序列化器(完整)"""
department = DepartmentSerializer(read_only=True)
rank = RankSerializer(read_only=True)
gender_display = serializers.CharField(source='get_gender_display', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
work_duration = serializers.IntegerField(read_only=True)
class Meta:
model = Employee
fields = '__all__'
read_only_fields = ['employee_id', 'created_at', 'updated_at']
class EmployeeCreateSerializer(serializers.ModelSerializer):
"""员工创建序列化器"""
username = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
class Meta:
model = Employee
fields = '__all__'
read_only_fields = ['employee_id', 'created_at', 'updated_at']
def create(self, validated_data):
username = validated_data.pop('username')
password = validated_data.pop('password')
# 创建关联用户账号
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.create_user(
username=username,
password=password,
phone=validated_data.get('phone', ''),
)
# 创建员工
validated_data['user'] = user
employee = Employee.objects.create(**validated_data)
return employee
class PromotionRecordSerializer(serializers.ModelSerializer):
"""晋升记录序列化器"""
employee_name = serializers.CharField(source='employee.name', read_only=True)
from_rank_name = serializers.CharField(source='from_rank.name', read_only=True)
to_rank_name = serializers.CharField(source='to_rank.name', read_only=True)
approved_by_name = serializers.CharField(source='approved_by.username', read_only=True)
class Meta:
model = PromotionRecord
fields = '__all__'
read_only_fields = ['created_at']2.5 视图集与权限
python
# employee/permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAdminOrHR(BasePermission):
"""系统管理员或 HR 权限"""
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.role in ['admin', 'hr']
class IsManagerOrAbove(BasePermission):
"""部门经理及以上权限"""
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
return request.user.role in ['admin', 'hr', 'manager']
class CanManageEmployee(BasePermission):
"""可以管理员工信息的权限"""
def has_permission(self, request, view):
user = request.user
if not user.is_authenticated:
return False
# 管理员和 HR 可以管理所有员工
if user.role in ['admin', 'hr']:
return True
# 部门经理只能管理本部门员工
if user.role == 'manager':
return True
return False
def has_object_permission(self, request, view, obj):
user = request.user
if user.role == 'admin':
return True
if user.role == 'hr':
return True
# 部门经理只能操作本部门员工
if user.role == 'manager':
managed_dept = getattr(user, 'managed_department', None)
if managed_dept and obj.department == managed_dept:
return True
# 员工只能查看自己的信息
if user.role == 'employee':
return obj.user == user or request.method in SAFE_METHODS
return Falsepython
# employee/views.py
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from .models import Department, Rank, Employee
from .serializers import (
DepartmentSerializer, DepartmentTreeSerializer,
RankSerializer, EmployeeListSerializer,
EmployeeDetailSerializer, EmployeeCreateSerializer,
)
from .permissions import IsAdminOrHR, IsManagerOrAbove, CanManageEmployee
from .filters import EmployeeFilter
class DepartmentViewSet(viewsets.ModelViewSet):
"""部门管理 API"""
queryset = Department.objects.annotate(
employee_count=Count('employees', filter=Q(employees__status='active'))
)
serializer_class = DepartmentSerializer
permission_classes = [IsAuthenticated, IsManagerOrAbove]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name']
ordering_fields = ['name', 'employee_count']
def get_permissions(self):
if self.action in ('create', 'update', 'partial_update', 'destroy'):
self.permission_classes = [IsAuthenticated, IsAdminOrHR]
return [p() for p in self.permission_classes]
@action(detail=False, methods=['get'])
def tree(self, request):
"""部门树形结构"""
departments = Department.objects.filter(parent=None).annotate(
employee_count=Count('employees', filter=Q(employees__status='active'))
)
serializer = DepartmentTreeSerializer(departments, many=True)
return Response(serializer.data)
class EmployeeViewSet(viewsets.ModelViewSet):
"""员工管理 API"""
queryset = Employee.objects.select_related(
'department', 'rank', 'user'
).prefetch_related('promotions')
permission_classes = [IsAuthenticated, CanManageEmployee]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = EmployeeFilter
search_fields = ['name', 'employee_id', 'phone', 'email']
ordering_fields = ['employee_id', 'hire_date', 'name']
ordering = ['employee_id']
def get_serializer_class(self):
if self.action == 'list':
return EmployeeListSerializer
elif self.action == 'create':
return EmployeeCreateSerializer
return EmployeeDetailSerializer
def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
# 部门经理只能看到本部门员工
if user.role == 'manager':
managed_dept = getattr(user, 'managed_department', None)
if managed_dept:
queryset = queryset.filter(department=managed_dept)
# 员工只能看到自己
elif user.role == 'employee':
queryset = queryset.filter(user=user)
return queryset
@action(detail=False, methods=['get'])
def stats(self, request):
"""员工统计信息"""
total = Employee.objects.count()
active = Employee.objects.filter(status='active').count()
probation = Employee.objects.filter(status='probation').count()
# 各部门人数统计
dept_stats = Department.objects.annotate(
active_count=Count('employees', filter=Q(employees__status='active'))
).values('name', 'active_count')
# 各职级人数统计
rank_stats = Rank.objects.annotate(
employee_count=Count('employees')
).values('name', 'employee_count')
return Response({
'total': total,
'active': active,
'probation': probation,
'by_department': dept_stats,
'by_rank': rank_stats,
})
@action(detail=True, methods=['post'])
def promote(self, request, pk=None):
"""员工晋升"""
employee = self.get_object()
to_rank_id = request.data.get('to_rank_id')
reason = request.data.get('reason', '')
if not to_rank_id:
return Response({'error': '请指定目标职级'}, status=400)
try:
to_rank = Rank.objects.get(id=to_rank_id)
except Rank.DoesNotExist:
return Response({'error': '职级不存在'}, status=404)
# 创建晋升记录
PromotionRecord.objects.create(
employee=employee,
from_rank=employee.rank,
to_rank=to_rank,
approved_by=request.user,
reason=reason,
effective_date=request.data.get('effective_date'),
)
# 更新员工职级
employee.rank = to_rank
employee.save()
serializer = EmployeeDetailSerializer(employee)
return Response(serializer.data)python
# employee/filters.py
import django_filters
from .models import Employee
class EmployeeFilter(django_filters.FilterSet):
"""员工过滤器"""
name = django_filters.CharFilter(lookup_expr='icontains')
department = django_filters.NumberFilter(field_name='department_id')
department_name = django_filters.CharFilter(field_name='department__name', lookup_expr='icontains')
rank = django_filters.NumberFilter(field_name='rank_id')
rank_level = django_filters.NumberFilter(field_name='rank__level')
status = django_filters.ChoiceFilter(choices=Employee.STATUS_CHOICES)
gender = django_filters.ChoiceFilter(choices=Employee.GENDER_CHOICES)
hire_date_after = django_filters.DateFilter(field_name='hire_date', lookup_expr='gte')
hire_date_before = django_filters.DateFilter(field_name='hire_date', lookup_expr='lte')
min_age = django_filters.NumberFilter(method='filter_min_age')
max_age = django_filters.NumberFilter(method='filter_max_age')
search = django_filters.CharFilter(method='filter_search')
class Meta:
model = Employee
fields = [
'name', 'department', 'department_name', 'rank', 'rank_level',
'status', 'gender', 'hire_date_after', 'hire_date_before',
]
def filter_min_age(self, queryset, name, value):
from datetime import date, timedelta
min_birth = date.today() - timedelta(days=value * 365)
return queryset.filter(birth_date__lte=min_birth)
def filter_max_age(self, queryset, name, value):
from datetime import date, timedelta
max_birth = date.today() - timedelta(days=value * 365)
return queryset.filter(birth_date__gte=max_birth)
def filter_search(self, queryset, name, value):
return queryset.filter(
models.Q(name__icontains=value) |
models.Q(employee_id__icontains=value) |
models.Q(phone__icontains=value)
)2.6 考勤与薪资 API
python
# attendance/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.utils import timezone
from django.db.models import Count, Q
from datetime import date, timedelta
from .models import Attendance
from .serializers import AttendanceSerializer
from employee.permissions import IsAdminOrHR, IsManagerOrAbove
class AttendanceViewSet(viewsets.ModelViewSet):
"""考勤管理 API"""
serializer_class = AttendanceSerializer
permission_classes = [IsAuthenticated, IsManagerOrAbove]
def get_queryset(self):
queryset = Attendance.objects.select_related('employee').all()
user = self.request.user
if user.role == 'employee':
queryset = queryset.filter(employee__user=user)
elif user.role == 'manager':
managed_dept = getattr(user, 'managed_department', None)
if managed_dept:
queryset = queryset.filter(employee__department=managed_dept)
# 日期过滤
date_from = self.request.query_params.get('date_from')
date_to = self.request.query_params.get('date_to')
if date_from:
queryset = queryset.filter(date__gte=date_from)
if date_to:
queryset = queryset.filter(date__lte=date_to)
return queryset
@action(detail=False, methods=['post'])
def check_in(self, request):
"""签到"""
employee = request.user.employee_profile
today = date.today()
# 检查是否已签到
attendance, created = Attendance.objects.get_or_create(
employee=employee,
date=today,
defaults={'check_in': timezone.localtime().time()},
)
if not created:
return Response({'message': '今日已签到', 'check_in': str(attendance.check_in)})
# 判断是否迟到(假设 9:00 上班)
from datetime import time
if attendance.check_in and attendance.check_in > time(9, 0):
attendance.status = 'late'
attendance.save()
return Response(AttendanceSerializer(attendance).data, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['post'])
def check_out(self, request):
"""签退"""
employee = request.user.employee_profile
today = date.today()
try:
attendance = Attendance.objects.get(employee=employee, date=today)
except Attendance.DoesNotExist:
return Response({'error': '请先签到'}, status=400)
attendance.check_out = timezone.localtime().time()
# 判断是否早退(假设 18:00 下班)
from datetime import time
if attendance.check_out < time(18, 0):
if attendance.status == 'normal':
attendance.status = 'early'
attendance.save()
return Response(AttendanceSerializer(attendance).data)
@action(detail=False, methods=['get'])
def monthly_stats(self, request):
"""月度考勤统计"""
year = int(request.query_params.get('year', timezone.now().year))
month = int(request.query_params.get('month', timezone.now().month))
employee = request.user.employee_profile
attendances = Attendance.objects.filter(
employee=employee,
date__year=year,
date__month=month,
)
total_days = attendances.count()
stats = attendances.values('status').annotate(
count=Count('id')
)
return Response({
'year': year,
'month': month,
'total_days': total_days,
'stats': {s['status']: s['count'] for s in stats},
})python
# salary/views.py
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Sum, Avg
from .models import Salary
from .serializers import SalarySerializer
from employee.permissions import IsAdminOrHR, IsManagerOrAbove
class SalaryViewSet(viewsets.ModelViewSet):
"""薪资管理 API"""
serializer_class = SalarySerializer
permission_classes = [IsAuthenticated, IsAdminOrHR]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['employee', 'month', 'is_paid']
ordering_fields = ['month', 'base_salary']
ordering = ['-month']
def get_queryset(self):
queryset = Salary.objects.select_related('employee').all()
user = self.request.user
# 员工只能查看自己的薪资
if user.role == 'employee':
queryset = queryset.filter(employee__user=user)
return queryset
@action(detail=False, methods=['get'])
def summary(self, request):
"""薪资汇总统计"""
year = int(request.query_params.get('year'))
month = int(request.query_params.get('month'))
salaries = Salary.objects.filter(
month__year=year,
month__month=month,
)
summary = salaries.aggregate(
total_base=Sum('base_salary'),
total_bonus=Sum('performance_bonus'),
total_overtime=Sum('overtime_pay'),
total_allowance=Sum('allowance'),
total_insurance=Sum('insurance'),
total_tax=Sum('tax'),
total_net=Sum('net_pay'),
avg_net=Avg('net_pay'),
count=Sum('is_paid') / Sum('id'), # 发放比例
)
return Response({
'year': year,
'month': month,
'summary': summary,
})2.7 路由与配置
python
# employee_management/urls.py
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
urlpatterns = [
path('admin/', admin.site.urls),
# API
path('api/v1/', include('accounts.urls')),
path('api/v1/', include('employee.urls')),
path('api/v1/', include('attendance.urls')),
path('api/v1/', include('salary.urls')),
# API 文档
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]python
# accounts/urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
from . import views
urlpatterns = [
path('auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('auth/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('auth/register/', views.UserRegistrationView.as_view(), name='user_register'),
path('auth/profile/', views.UserProfileView.as_view(), name='user_profile'),
path('auth/change-password/', views.ChangePasswordView.as_view(), name='change_password'),
]python
# employee/urls.py
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'departments', views.DepartmentViewSet, basename='department')
router.register(r'ranks', views.RankViewSet, basename='rank')
router.register(r'employees', views.EmployeeViewSet, basename='employee')
urlpatterns = router.urlspython
# settings.py 关键配置
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 第三方应用
'rest_framework',
'rest_framework_simplejwt',
'django_filters',
'drf_spectacular',
'corsheaders',
# 自定义应用
'accounts',
'employee',
'attendance',
'salary',
]
AUTH_USER_MODEL = 'accounts.User'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '30/minute',
'user': '300/minute',
},
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=2),
'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
'ROTATE_REFRESH_TOKENS': True,
'AUTH_HEADER_TYPES': ('Bearer',),
}
SPECTACULAR_SETTINGS = {
'TITLE': '企业员工管理系统 API',
'DESCRIPTION': '企业人事管理系统的 RESTful API 接口文档',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}2.8 启动与测试
bash
# 1. 安装依赖
pip install django djangorestframework djangorestframework-simplejwt \
django-filter drf-spectacular django-cors-headers Pillow
# 2. 迁移数据库
python manage.py makemigrations accounts employee attendance salary
python manage.py migrate
# 3. 创建初始数据
python manage.py shellpython
# 初始化脚本:创建角色和初始数据
from django.contrib.auth.models import Group, Permission
from accounts.models import User
from employee.models import Department, Rank, Employee
# 创建角色组
admin_group, _ = Group.objects.get_or_create(name='系统管理员')
hr_group, _ = Group.objects.get_or_create(name='人力资源')
manager_group, _ = Group.objects.get_or_create(name='部门经理')
# 创建超级管理员
admin = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='admin123',
role='admin',
phone='13800000000',
)
# 创建部门
dept_it = Department.objects.create(name='技术部', description='负责技术研发')
dept_hr = Department.objects.create(name='人力资源部', description='负责人事管理')
dept_finance = Department.objects.create(name='财务部', description='负责财务管理')
# 创建职级
Rank.objects.create(name='初级', level=5, min_salary=5000, max_salary=10000)
Rank.objects.create(name='中级', level=4, min_salary=8000, max_salary=15000)
Rank.objects.create(name='高级', level=3, min_salary=12000, max_salary=25000)
Rank.objects.create(name='资深', level=2, min_salary=20000, max_salary=40000)
Rank.objects.create(name='专家', level=1, min_salary=35000, max_salary=80000)
print('初始数据创建完成!')bash
# 4. 启动服务
python manage.py runserver
# API 访问示例:
# POST /api/v1/auth/login/ → 获取 JWT Token
# GET /api/v1/employees/ → 员工列表(需 Token)
# GET /api/v1/departments/ → 部门列表
# GET /api/docs/ → Swagger API 文档四、小结
| 项目 | 核心技术 | 架构模式 | 适用场景 |
|---|---|---|---|
| 博客系统后台 | Django ORM + Admin + 模板 + Markdown | 传统 MVT 服务端渲染 | 内容管理系统、企业官网、个人博客 |
| 企业员工管理系统 | DRF + JWT + RBAC + drf-spectacular | 前后端分离 RESTful API | 企业级管理系统、移动端后台 API |
两个项目的核心差异
| 维度 | 博客系统(项目一) | 员工管理系统(项目二) |
|---|---|---|
| 渲染方式 | Django Template 服务端渲染 | DRF JSON + 前端渲染 |
| 认证方式 | Session + Cookie | JWT Token |
| 权限模型 | 简单用户/管理员 | 完整 RBAC(4种角色) |
| 数据集 | 轻量级 | 复杂关联(树形部门、职级体系) |
| 文档 | 人工维护 | drf-spectacular 自动生成 |
| 接口形式 | 页面 URL | RESTful API 端点 |
五、练习
项目一扩展练习
- 添加 RSS 订阅功能:为博客添加 RSS/Atom 订阅
- 文章阅读排行榜:在侧栏展示本周/本月热门文章
- 邮件通知:新评论时发送邮件通知文章作者
- 文章导出:支持将文章导出为 PDF 或 Markdown 文件
项目二扩展练习
- 请假管理:添加请假申请/审批流程(年假、事假、病假等)
- 报表导出:支持导出考勤统计报表、薪资报表为 Excel
- 审批工作流:实现多级审批流程(员工申请 → 部门经理 → HR → 管理员)
- 仪表盘统计:开发部门人员变动趋势、薪资分布统计等可视化 API
综合挑战
- 将两个项目整合:在博客系统中使用 JWT 认证,将员工管理系统作为博客的后台内容管理 API
- 部署上线:使用 Docker + Nginx + Gunicorn + PostgreSQL 部署两个项目