# 企业OA管理系统 **Repository Path**: ha_shi_qi/OA-System ## Basic Information - **Project Name**: 企业OA管理系统 - **Description**: 基于Django的企业级OA管理系统 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-30 - **Last Updated**: 2026-05-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 哈士奇OA管理系统 - 后端 基于 Django 5.0 + Django REST Framework 构建的企业级办公自动化(OA)系统后端,提供用户认证、请假审批、通知公告、员工管理、文件上传和首页仪表盘等核心功能。 --- ## 技术栈 | 分类 | 技术 | 说明 | |------|------|------| | Web框架 | Django 5.0.3 | MTV 架构 | | API框架 | Django REST Framework | RESTful API | | 数据库 | MySQL | 通过 `django.db.backends.mysql` 连接 | | 任务队列 | Celery | 异步邮件发送,Redis 做 Broker | | 缓存 | Redis | django-redis,页面缓存 15 分钟 | | 认证 | PyJWT | HS256 算法,7 天过期 | | 加密 | PyCryptodome | AES-256-CBC,用于激活链接 token | | 跨域 | django-cors-headers | 开发环境全放通 | | UUID | django-shortuuidfield | 用户主键 | | 文件处理 | pandas / openpyxl | 员工 Excel 批量导入导出 | | 邮件 | Django SMTP | QQ 邮箱 | **requirements.txt:** ``` django djangorestframework django-cors-headers celery django-shortuuidfield pyjwt ``` > 实际运行还需安装: `pycryptodome`, `pandas`, `openpyxl`, `redis`, `mysqlclient` --- ## 项目结构 ``` oaback/ ├── manage.py # Django 命令行入口 ├── requirements.txt # Python 依赖 ├── celery.log # Celery 运行日志 ├── oaback/ # 项目配置包 │ ├── settings.py # 全局配置(数据库、缓存、邮件、DRF等) │ ├── urls.py # 路由聚合入口 │ ├── wsgi.py / asgi.py # 部署入口 │ └── celery.py # Celery 实例化与配置 ├── apps/ # 业务模块(6个App) │ ├── oaauth/ # ① 认证与用户系统 │ ├── absent/ # ② 请假/考勤审批 │ ├── inform/ # ③ 通知公告 │ ├── staff/ # ④ 员工管理 │ ├── image/ # ⑤ 图片上传 │ └── home/ # ⑥ 首页仪表盘 ├── utils/ │ └── aeser.py # AES 加解密工具 ├── static/ # 静态文件(Bootstrap 5) ├── media/ # 用户上传的图片 └── templates/ # Django 模板(active.html 激活页面) ``` --- ## 一、认证模块 `apps/oaauth` ### 1.1 自定义 User 模型 **文件:** `apps/oaauth/models.py` 重写 Django 默认 User 模型,核心字段: | 字段 | 类型 | 说明 | |------|------|------| | `uid` | ShortUUIDField | 主键,短 UUID | | `realname` | CharField(150) | 真实姓名 | | `email` | EmailField(unique) | 登录凭据(USERNAME_FIELD) | | `telephone` | CharField(20) | 电话(可选) | | `password` | CharField | Django 自动哈希存储 | | `is_staff` | BooleanField | 默认 True | | `is_active` | BooleanField | 默认 True | | `status` | IntegerField | 1=已激活 / 2=未激活 / 3=锁定 | | `date_joined` | DateTimeField | 入职时间(auto_now_add) | | `department` | FK → OADepartment | 所属部门 | **OAUserManager 自定义管理器:** - `create_user(realname, email, password)` — 创建普通用户,`make_password` 自动哈希 - `create_superuser(realname, email, password)` — 创建超管,强制 `is_superuser=True`, `status=ACTIVED` **OADepartment 模型:** | 字段 | 类型 | 说明 | |------|------|------| | `name` | CharField(100) | 部门名称 | | `intro` | CharField(200) | 部门简介 | | `leader` | OneToOneField → OAUser | 部门负责人 | | `manager` | ForeignKey → OAUser | 分管领导 | ### 1.2 JWT 认证流程 #### Token 生成 `apps/oaauth/authentications.py` ```python def generate_jwt(user): expire_time = time.time() + 60 * 60 * 24 * 7 # 7 天过期 return jwt.encode({"userid": user.pk, "exp": expire_time}, key=settings.SECRET_KEY) ``` - 算法: HS256,密钥为 Django 的 `SECRET_KEY` - 过期触发 `ExpiredSignatureError` #### LoginCheckMiddleware 中间件 **文件:** `apps/oaauth/middlewares.py` — 全局认证核心 ``` 每个请求到达 │ ├── 路径在白名单中? (/auth/login, /staff/active, /media/*) │ └── 是 → 设置 AnonymousUser,放行 │ └── 不在白名单 │ ├── 解析请求头 Authorization: JWT ├── jwt.decode() 验证签名与过期 ├── 从 payload 提取 userid,查询 OAUser ├── 绑定 request.user 和 request.auth └── 失败 → 返回 403 {"detail": "请先登录!"} ``` 系统实现了两套 DRF 认证类: - `UserTokenAuthentication` — 全局默认(从 `request._request.user` 读取,依赖中间件已绑定的用户) - `JWTAuthentication` — 备用独立 JWT 解析 ### 1.3 登录接口 **POST `/auth/login`** 请求: ```json {"email": "zhangsan@qq.com", "password": "111111"} ``` `views.py:LoginView.post()` 处理流程: 1. `LoginSerializer` 校验邮箱格式与密码长度 2. `validate()` 中: `OAUser.objects.filter(email=email).first()` 查用户 → `user.check_password(password)` 验证密码 → 检查 status(UNACTIVE / LOCKED 会拒绝) 3. 更新 `user.last_login` 4. 调用 `generate_jwt(user)` 生成 token 5. 返回 token + 用户信息(`UserSerializer`) ### 1.4 修改密码 **POST `/auth/resetpwd`** 请求: ```json {"oldpwd": "111111", "pwd1": "222222", "pwd2": "222222"} ``` `ResetPwdSerializer` 验证: `check_password(oldpwd)` → 检查 `pwd1 == pwd2` → `request.user.set_password(pwd1)` 保存 --- ## 二、请假模块 `apps/absent` ### 2.1 数据模型 **AbsentType(请假类型):** - `name` CharField(100) — 如"事假""婚假""病假" - `create_time` DateTimeField(auto_now_add) **Absent(请假单):** | 字段 | 类型 | 说明 | |------|------|------| | `title` | CharField(200) | 请假标题 | | `request_content` | TextField | 请假详细内容 | | `absent_type` | FK → AbsentType | 请假类型 | | `requester` | FK → OAUser | 发起人(related_name='my_absents') | | `responder` | FK → OAUser | 审批人(related_name='sub_absents',可为空) | | `status` | IntegerField | 1=审批中 / 2=通过 / 3=拒绝 | | `start_date` | DateField | 开始日期 | | `end_date` | DateField | 结束日期 | | `create_time` | DateTimeField | 发起时间 | | `response_content` | TextField | 审批回复 | 排序: `ordering = ('-create_time',)` — 最新申请在前 ### 2.2 审批人分配逻辑 **文件:** `apps/absent/utils.py` — `get_responder(request)` ``` 发起人 == 部门 leader? ├── 是 → 发起人在董事会? │ ├── 是 → responder = None(无需审批,直接通过) │ └── 否 → responder = department.manager(分管领导审批) └── 否 → responder = department.leader(部门领导审批) ``` 组织架构示例: ``` 董事会(东东 Leader,多多) ├─ manager = None ├── 产品开发部(张三 Leader) ← manager: 东东 │ ├─ 普通员工请假 → 张三审批 │ └─ 张三请假 → 东东审批 ├── 运营部(李四 Leader) ← manager: 东东 ├── 销售部(孙七 Leader) ← manager: 东东 ├── 人事部(王五 Leader) ← manager: 多多 └── 财务部(赵六 Leader) ← manager: 多多 ``` ### 2.3 序列化器 **文件:** `apps/absent/serializers.py` **读/写分离设计:** ```python class AbsentSerializer(serializers.ModelSerializer): absent_type = AbsentTypeSerializer(read_only=True) # 输出: 嵌套详情 absent_type_id = serializers.IntegerField(write_only=True) # 输入: 只传ID requester = UserSerializer(read_only=True) # 自动填充 responder = UserSerializer(read_only=True) # 自动填充 ``` **create() 流程:** 获取当前用户 → `get_responder()` 确定审批人 → 董事会 leader 则 `status=PASS`, 否则 `status=AUDITING` → 创建记录 **update() 流程:** 检查当前状态必须是 AUDITING → 检查 `request.user == instance.responder` → 更新 status 和 response_content ### 2.4 API 接口 | 方法 | 路径 | 功能 | |------|------|------| | `GET` | `/absent/absent?who=my` | 我的请假列表(分页) | | `GET` | `/absent/absent?who=sub` | 下属的请假列表(分页) | | `POST` | `/absent/absent` | 发起请假 | | `PATCH` | `/absent/absent/{id}` | 审批请假(部分更新) | | `GET` | `/absent/type` | 请假类型列表 | | `GET` | `/absent/responder` | 获取审批人信息 | --- ## 三、通知公告模块 `apps/inform` ### 3.1 数据模型 **Inform(通知):** | 字段 | 类型 | 说明 | |------|------|------| | `title` | CharField(100) | 通知标题 | | `content` | TextField | 通知内容 | | `create_time` | DateTimeField | 发布时间 | | `public` | BooleanField | 是否全员可见 | | `author` | FK → OAUser | 发布者 | | `departments` | ManyToMany → OADepartment | 可见部门 | **InformRead(已读记录):** - `inform` FK → Inform + `user` FK → OAUser + `read_time` - `unique_together = ('inform', 'user')` — 同一人同条通知不重复 ### 3.2 通知可见性规则 `views.py:InformViewSet.get_queryset()` 中构造的 ORM 查询: ```python Inform.objects.filter( Q(public=True) # 全员可见 | Q(departments=user.department) # 发布给了用户所在部门 | Q(author=user) # 用户自己发布的 ).distinct() ``` 通过 `select_related('author')` 和 `prefetch_related('reads', 'departments')` 优化查询。 ### 3.3 发布通知的部门处理 `serializers.py:InformSerializer.create()`: - 前端传入 `department_ids` 列表 - 若包含 `0` → `public=True`(全员可见) - 否则 → `departments.set(OADepartment.objects.filter(id__in=department_ids))` ### 3.4 API 接口 | 方法 | 路径 | 功能 | |------|------|------| | `GET` | `/inform/inform` | 可见通知列表 | | `POST` | `/inform/inform` | 发布新通知 | | `GET` | `/inform/inform/{id}` | 通知详情(含已读人数) | | `PATCH` | `/inform/inform/{id}` | 编辑通知 | | `DELETE` | `/inform/inform/{id}` | 删除通知(仅作者) | | `POST` | `/inform/inform/read` | 标记已读 | 删除权限: 只有 `instance.author.uid == request.user.uid` 的作者可删,否则 401。 --- ## 四、员工管理模块 `apps/staff` ### 4.1 员工列表 **GET `/staff/staff`** — 权限控制: ``` 用户所属部门 ├── 董事会 → 查看所有员工(可选 department_id 过滤) ├── 非董事会 + 部门leader → 只查看本部门员工 └── 非leader → 403 Forbidden ``` 查询参数: `department_id`, `realname`(模糊搜索), `date_joined[]`(日期范围), `page`/`size`(分页,默认每页2条) ### 4.2 新增员工流程 **POST `/staff/staff`** ``` 1. AddStaffSerializer 校验 ├── 邮箱未被注册 └── 当前用户必须是部门 leader 2. OAUser.objects.create_user() 创建用户 ├── department = 当前用户部门 └── status = UNACTIVE 3. send_active_email() 发送激活邮件 ├── AES-256-CBC 加密邮箱 → token ├── 拼接激活URL: /staff/active?token= └── Celery 异步发送: send_mail_task.delay(email, subject, message) ``` ### 4.3 账号激活 **GET `/staff/active?token=xxx`** → 返回 active.html 页面,token 存入 Cookie **POST `/staff/active`** → 从 Cookie 取 token → AES 解密得邮箱 → 对比表单邮箱 → 一致则 `user.status = ACTIVED` ### 4.4 批量导入/导出 **POST `/staff/upload`** — Excel 批量导入: - `StaffUploadSerializer` 校验文件格式(.xlsx/.xls) - `pandas.read_excel()` 读取 - 逐行读取姓名、邮箱、部门 → 默认密码 111111 - `transaction.atomic()` 事务包裹 `bulk_create` - 异步发激活邮件 **GET `/staff/download?pks=["uid1","uid2"]`** — Excel 导出: - 解析 JSON 数组 - 权限过滤 → pandas DataFrame → 中文字段映射 → `to_excel()` → HttpResponse(application/xlsx) ### 4.5 异步邮件任务 **文件:** `apps/staff/tasks.py` ```python @celery_app.task(name='send_mail_task') def send_mail_task(email, subject, message): send_mail(subject, recipient_list=[email], message=message, from_email=settings.DEFAULT_FROM_EMAIL) ``` ### 4.6 API 接口汇总 | 方法 | 路径 | 功能 | |------|------|------| | `GET` | `/staff/staff` | 员工列表(分页+过滤) | | `POST` | `/staff/staff` | 新增员工 | | `PUT` | `/staff/staff/{uid}` | 编辑员工 | | `GET` | `/staff/departments` | 部门列表 | | `GET` | `/staff/active` | 激活页面 | | `POST` | `/staff/active` | 提交激活 | | `GET` | `/staff/download` | 导出 Excel | | `POST` | `/staff/upload` | 批量导入 Excel | --- ## 五、图片上传模块 `apps/image` **POST `/image/upload`** ``` 1. UploadImageSerializer 校验 ├── ImageField: Django 自动校验是否为合法图片 ├── FileExtensionValidator: 仅允许 png/jpg/jpeg/gif └── 自定义验证: 文件 ≤ 0.5MB 2. 文件重命名: shortuuid() + 原扩展名 (abc.png → aB3xK9m2.png) 3. 分块写入: for chunk in file.chunks(): fp.write(chunk) 4. 返回(适配富文本编辑器): {"errno": 0, "data": {"url": "/media/aB3xK9m2.png", "alt": "", "href": "/media/aB3xK9m2.png"}} ``` --- ## 六、首页仪表盘 `apps/home` 三个聚合接口,均使用 `@method_decorator(cache_page(60 * 15))` 做 15 分钟 Redis 缓存: | 方法 | 路径 | 功能 | 说明 | |------|------|------|------| | `GET` | `/home/latest/inform` | 最新10条通知 | 过滤当前用户可见 | | `GET` | `/home/latest/absent` | 最新10条考勤 | 董事会看全员,否则看本部门 | | `GET` | `/home/department/staff/count` | 各部门人数 | `Count("staffs")` 注解统计 | --- ## 数据库关系图 ``` OADepartment (部门) ├── leader (OneToOne → OAUser) └── manager (FK → OAUser) │ OAUser (用户) ◄──── department (FK) ├── my_absents → Absent (发起请假) ├── sub_absents → Absent (审批请假) ├── informs → Inform (发布通知) ├── reads → InformRead (已读记录) ├── leader_department → OADepartment (领导部门) └── manager_departments → OADepartment (分管部门) Absent ────→ AbsentType (请假类型) ├── requester → OAUser └── responder → OAUser Inform ◄──多对多──→ OADepartment (可见部门) └── reads → InformRead └── user → OAUser ``` --- ## Celery 配置 `oaback/celery.py`: - Broker: `redis://127.0.0.1:6379/1` - Backend: `redis://127.0.0.1:6379/2` - `autodiscover_tasks()` 自动发现各 App 下的 tasks.py - 日志: FileHandler → `celery.log`,DEBUG 级别 --- ## 初始化命令 ```bash # 1. 初始化部门(6个部门) python manage.py initdepartments # 2. 初始化用户(7个初始用户 + 领导关系) python manage.py inituser # 3. 初始化请假类型 python manage.py initabsenttype ``` **初始化账户:** | 角色 | 邮箱 | 密码 | 部门 | |------|------|------|------| | 东东(超管) | dongdong@qq.com | 111111 | 董事会 | | 多多(超管) | duoduo@qq.com | 111111 | 董事会 | | 张三 | zhangsan@qq.com | 111111 | 产品开发部 | | 李四 | lisi@qq.com | 111111 | 运营部 | | 孙七 | sunqi@qq.com | 111111 | 销售部 | | 王五 | wangwu@qq.com | 111111 | 人事部 | | 赵六 | zhaoliu@qq.com | 111111 | 财务部 | --- ## 全局路由表 ``` /auth/login → oaauth.LoginView (无需认证) /auth/resetpwd → oaauth.ResetPwdView (需认证) /absent/absent → absent.AbsentViewSet (需认证) /absent/type → absent.AbsentTypeView (需认证) /absent/responder → absent.ResponderView (需认证) /inform/inform → inform.InformViewSet (需认证) /inform/inform/read → inform.ReadInformView (需认证) /staff/staff → staff.StaffViewSet (需认证) /staff/departments → staff.DepartmentListView (需认证) /staff/active → staff.ActiveStaffView (无需认证) /staff/download → staff.StaffDownloadView (需认证) /staff/upload → staff.StaffUploadView (需认证) /image/upload → image.UploadImageView (需认证) /home/latest/inform → home.LatestInformView (需认证) /home/latest/absent → home.LatestAbsentView (需认证) /home/department/staff/count → home.DepartmentStaffCountView (需认证) ``` --- ## 认证使用方式 所有非白名单接口携带: ``` Authorization: JWT ``` 白名单(无需 token): `POST /auth/login`, `GET/POST /staff/active`, `GET /media/*` --- ## 启动步骤 ```bash # 1. 安装依赖 pip install django djangorestframework django-cors-headers celery \ django-shortuuidfield pyjwt pycryptodome pandas openpyxl \ redis mysqlclient # 2. 确保 MySQL 和 Redis 已启动 # MySQL 建库: CREATE DATABASE `哈士奇oa管理系统` DEFAULT CHARSET utf8mb4; # 3. 数据库初始化 python manage.py makemigrations python manage.py migrate python manage.py initdepartments python manage.py inituser python manage.py initabsenttype # 4. 启动 Django python manage.py runserver # 5. 启动 Celery Worker(另一个终端) celery -A oaback worker -l info -P gevent ``` --- ## 注意事项 1. **settings.py 敏感信息已硬编码**(数据库密码、邮箱密码、SECRET_KEY),生产环境应改为环境变量 2. **DEBUG = True**,生产环境需设为 False 3. **ALLOWED_HOSTS = []**,部署时需配置实际域名 4. **CORS_ALLOW_ALL_ORIGINS = True**,生产环境应限制具体域名 5. **CSRF 保护已关闭**,安全性依赖 JWT 令牌 6. **已关闭 Django Admin、Session、Message 框架** 7. **APPEND_SLASH = False**,所有 URL 末尾不带斜杠 8. **Celery 异步邮件** 依赖 Redis 和 Worker 进程运行