使用 Python + Flask 构建钉钉部门和人员管理系统

摘要: 本文详细介绍如何使用 Python 和 Flask 框架开发一个完整的钉钉组织架构管理系统,包括从钉钉API同步数据、构建Web管理界面、实现部门人员查看和编辑等功能。

一、项目背景

在企业日常管理中,钉钉的组织架构数据是非常重要的信息。为了方便管理和查看钉钉的部门和人员信息,我开发了一个基于 Python + Flask 的Web管理系统。该系统可以实现:

  • ✅ 自动从钉钉API同步部门和人员数据
  • ✅ 可视化展示组织架构
  • ✅ 按部门筛选查看人员
  • ✅ 搜索和编辑人员信息
  • ✅ 实时统计部门人数

二、技术栈

  • 后端框架: Flask 3.x
  • 前端技术: HTML5 + CSS3 + JavaScript (原生)
  • 数据存储: JSON文件
  • 钉钉API: 钉钉开放平台API v2
  • 开发语言: Python 3.x

三、系统架构

钉钉部门和人员管理系统
── dingtalk_sync.py      # 钉钉数据同步脚本
├── app.py                # Flask Web应用
├── templates/
│   └── index.html        # 前端管理界面
├── departments.json      # 部门数据
├── users.json            # 人员数据
└── requirements.txt      # 依赖包

四、核心功能实现

4.1 钉钉数据同步模块

首先,我们需要从钉钉API获取组织架构数据。钉钉提供了完善的API接口,我们可以通过以下步骤获取数据:

1. 获取 Access Token
def get_access_token(self) -> str:
    """获取访问令牌"""
    url = f"{self.base_url}/gettoken"
    params = {
        "appkey": self.app_key,
        "appsecret": self.app_secret
    }
    
    response = requests.get(url, params=params)
    result = response.json()
    
    if result.get("errcode") == 0:
        self.access_token = result.get("access_token")
        return self.access_token

关键点:

  • Access Token 有效期为 2 小时
  • 需要缓存 Token,避免频繁请求
  • 使用 AppKey 和 AppSecret 进行认证
2. 获取部门列表
def get_all_departments(self) -> List[Dict]:
    """获取所有部门列表"""
    access_token = self.get_access_token()
    url = f"{self.base_url}/topapi/v2/department/listsub"
    
    # 获取根部门(dept_id=1)下的所有子部门
    data = {"dept_id": 1}
    
    response = requests.post(url, params=params, json=data)
    result = response.json()
    
    if result.get("errcode") == 0:
        return result.get("result", [])

钉钉API说明:

  • 接口地址: /topapi/v2/department/listsub
  • 根部门 ID 固定为 1
  • 返回所有子部门列表
3. 获取部门用户 (支持分页)

钉钉的用户列表接口支持分页查询,需要处理分页逻辑:

def get_department_users(self, dept_id: int) -> List[Dict]:
    """获取指定部门下的用户列表"""
    all_users = []
    cursor = 0
    size = 100  # 每页最多100条
    
    while True:
        data = {
            "dept_id": dept_id,
            "cursor": cursor,
            "size": size
        }
        
        response = requests.post(url, params=params, json=data)
        result = response.json()
        
        if result.get("errcode") == 0:
            result_data = result.get("result", {})
            users = result_data.get("list", [])
            all_users.extend(users)
            
            # 检查是否有更多数据
            if not result_data.get("has_more", False):
                break
            
            cursor = result_data.get("next_cursor", 0)
    
    return all_users

分页处理要点:

  • 使用 cursor 进行分页
  • 每页最多获取 100 条数据
  • 通过 has_more 判断是否还有下一页
  • 使用 next_cursor 获取下一页的游标
4. 完整同步流程
def get_all_users(self) -> List[Dict]:
    """获取所有用户信息"""
    departments = self.get_all_departments()
    all_users = []
    
    # 获取根部门用户
    root_users = self.get_department_users(1)
    all_users.extend(root_users)
    
    # 获取所有子部门的用户
    for dept in departments:
        dept_id = dept.get("dept_id")
        users = self.get_department_users(dept_id)
        all_users.extend(users)
    
    return all_users

4.2 Flask Web 后端

使用 Flask 框架构建 RESTful API,提供数据查询和更新接口。

1. 基础配置
from flask import Flask, render_template, jsonify, request
import json
import os

app = Flask(__name__)

# 数据文件路径
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
DEPARTMENTS_FILE = os.path.join(DATA_DIR, 'departments.json')
USERS_FILE = os.path.join(DATA_DIR, 'users.json')
2. 部门列表 API (带人数统计)
@app.route('/api/departments')
def get_departments():
    """API - 获取所有部门"""
    departments = load_json(DEPARTMENTS_FILE)
    users = load_json(USERS_FILE)
    
    # 计算每个部门的人数
    dept_user_count = {}
    for user in users:
        dept_ids = user.get('dept_id_list', [])
        for dept_id in dept_ids:
            dept_user_count[dept_id] = dept_user_count.get(dept_id, 0) + 1
    
    # 为每个部门添加人数统计
    for dept in departments:
        dept_id = dept.get('dept_id')
        dept['user_count'] = dept_user_count.get(dept_id, 0)
    
    return jsonify({
        'success': True,
        'data': departments,
        'total': len(departments)
    })

设计思路:

  • 同时加载部门和用户数据
  • 统计每个部门的用户数量
  • 将人数信息附加到部门对象中
3. 按部门查询用户
@app.route('/api/users/by-department/<int:dept_id>')
def get_users_by_department(dept_id):
    """API - 获取指定部门的用户"""
    users = load_json(USERS_FILE)
    dept_users = [u for u in users if dept_id in u.get('dept_id_list', [])]
    
    return jsonify({
        'success': True,
        'data': dept_users,
        'total': len(dept_users)
    })
4. 更新用户信息
@app.route('/api/user/<string:user_id>', methods=['PUT'])
def update_user(user_id):
    """API - 更新用户信息"""
    users = load_json(USERS_FILE)
    
    for user in users:
        if user.get('userid') == user_id:
            data = request.json
            # 更新允许的字段
            if 'name' in data:
                user['name'] = data['name']
            if 'mobile' in data:
                user['mobile'] = data['mobile']
            if 'email' in data:
                user['email'] = data['email']
            if 'job_number' in data:
                user['job_number'] = data['job_number']
            if 'title' in data:
                user['title'] = data['title']
            
            save_json(USERS_FILE, users)
            return jsonify({'success': True, 'message': '用户更新成功'})
    
    return jsonify({'success': False, 'message': '用户不存在'}), 404
5. 触发数据同步
@app.route('/api/sync', methods=['POST'])
def sync_data():
    """API - 触发数据同步"""
    try:
        import subprocess
        result = subprocess.run(
            ['python3', 'dingtalk_sync.py'],
            capture_output=True,
            text=True,
            cwd=DATA_DIR
        )
        
        if result.returncode == 0:
            return jsonify({
                'success': True,
                'message': '同步成功',
                'output': result.stdout
            })
        else:
            return jsonify({
                'success': False,
                'message': '同步失败',
                'error': result.stderr
            }), 500
    except Exception as e:
        return jsonify({
            'success': False,
            'message': f'同步异常: {str(e)}'
        }), 500

实现原理:

  • 通过 subprocess 调用同步脚本
  • 捕获标准输出和错误输出
  • 返回执行结果给前端

4.3 前端界面实现

前端使用原生 HTML + CSS + JavaScript,无需额外的前端框架,轻量且高效。

1. 页面布局
<div class="container">
    <header>
        <h1>钉钉部门和人员管理系统</h1>
        <div class="header-actions">
            <button class="btn btn-success" onclick="syncData()">同步数据</button>
            <button class="btn btn-primary" onclick="refreshData()">刷新</button>
        </div>
    </header>
    
    <div class="stats-grid">
        <!-- 统计卡片 -->
    </div>
    
    <div class="main-content">
        <div class="panel">
            <!-- 部门列表 -->
        </div>
        <div class="panel">
            <!-- 人员列表 -->
        </div>
    </div>
</div>
2. 加载统计数据
async function loadStatistics() {
    const response = await fetch('/api/statistics');
    const result = await response.json();
    
    if (result.success) {
        const stats = result.data;
        document.getElementById('statsGrid').innerHTML = `
            <div class="stat-card">
                <h3>部门总数</h3>
                <div class="value">${stats.total_departments}</div>
            </div>
            <div class="stat-card">
                <h3>人员总数</h3>
                <div class="value">${stats.total_users}</div>
            </div>
            <div class="stat-card">
                <h3>平均部门人数</h3>
                <div class="value">${Math.round(stats.total_users / stats.total_departments)}</div>
            </div>
        `;
    }
}
3. 加载所有用户 (默认显示)
async function loadAllUsers() {
    const response = await fetch('/api/users');
    const result = await response.json();
    
    if (result.success) {
        allUsers = result.data;
        filteredUsers = [...allUsers];
        renderUsers(filteredUsers);
    }
}
4. 按部门筛选用户
async function selectDepartment(deptId) {
    currentDeptId = deptId;
    
    const response = await fetch(`/api/users/by-department/${deptId}`);
    const result = await response.json();
    
    if (result.success) {
        allUsers = result.data;
        filteredUsers = [...allUsers];
        renderUsers(filteredUsers);
    }
}
5. 搜索功能
function filterUsers() {
    const keyword = document.getElementById('userSearch').value.toLowerCase();
    filteredUsers = allUsers.filter(user => {
        return (user.name && user.name.toLowerCase().includes(keyword)) ||
               (user.job_number && user.job_number.toLowerCase().includes(keyword)) ||
               (user.mobile && user.mobile.includes(keyword));
    });
    renderUsers(filteredUsers);
}

搜索逻辑:

  • 支持按姓名搜索
  • 支持按工号搜索
  • 支持按手机号搜索
  • 实时过滤,无需刷新页面
6. 编辑用户
function editUser(userId) {
    const user = allUsers.find(u => u.userid === userId);
    
    document.getElementById('editUserId').value = user.userid;
    document.getElementById('editName').value = user.name || '';
    document.getElementById('editJobNumber').value = user.job_number || '';
    document.getElementById('editTitle').value = user.title || '';
    document.getElementById('editMobile').value = user.mobile || '';
    document.getElementById('editEmail').value = user.email || '';
    
    document.getElementById('editModal').classList.add('show');
}

// 保存用户
document.getElementById('editForm').addEventListener('submit', async function(e) {
    e.preventDefault();
    
    const userId = document.getElementById('editUserId').value;
    const data = {
        name: document.getElementById('editName').value,
        job_number: document.getElementById('editJobNumber').value,
        title: document.getElementById('editTitle').value,
        mobile: document.getElementById('editMobile').value,
        email: document.getElementById('editEmail').value
    };
    
    const response = await fetch(`/api/user/${userId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
    });
    
    const result = await response.json();
    if (result.success) {
        showToast('保存成功', 'success');
    }
});

五、项目部署

5.1 安装依赖

# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate

# 安装依赖包
pip install requests flask

5.2 配置钉钉应用

  1. 登录 钉钉开放平台
  2. 创建应用,获取以下信息:
    • CorpId: 企业 ID
    • AppKey: 应用唯一标识
    • AppSecret: 应用密钥
  3. 配置应用权限:
    • 通讯录只读权限
    • 部门管理权限
    • 员工信息权限

5.3 修改配置文件

编辑 dingtalk_sync.py,填入你的钉钉应用信息:

CORP_ID = "your_corp_id"
APP_KEY = "your_app_key"
APP_SECRET = "your_app_secret"

5.4 运行项目

# 首次同步数据
python3 dingtalk_sync.py

# 启动 Web 服务
python3 app.py

访问 http://localhost:5001 即可使用管理系统。

六、功能展示

6.1 首页概览

  • 顶部统计卡片: 显示部门总数、人员总数、平均部门人数
  • 左侧部门列表: 展示所有部门及人数,支持搜索
  • 右侧人员列表: 默认显示所有人员,支持筛选和搜索

6.2 部门筛选

点击左侧部门,右侧自动显示该部门的所有人员。

6.3 人员编辑

点击"编辑"按钮,弹出模态框修改人员信息:

  • 姓名
  • 工号
  • 职位
  • 手机号
  • 邮箱

6.4 数据同步

点击"同步数据"按钮,自动从钉钉API获取最新组织架构数据。

七、技术亮点

7.1 分页处理

钉钉用户列表接口采用游标分页,系统自动处理所有分页数据,确保获取完整信息。

7.2 数据缓存

  • Access Token 自动缓存,避免重复请求
  • 部门人数统计在服务端计算,减轻前端压力

7.3 响应式设计

使用 CSS Grid 和 Flexbox 布局,适配不同屏幕尺寸。

7.4 实时搜索

前端搜索无需刷新页面,通过 JavaScript 实时过滤数据。

7.5 错误处理

  • API 请求失败时显示友好提示
  • 数据加载异常时显示加载状态
  • 表单提交失败时回滚数据

八、扩展功能建议

基于当前系统,还可以扩展以下功能:

  1. 定时同步: 使用 cron 或 celery 实现定时自动同步
  2. 权限管理: 添加用户登录和权限控制
  3. 数据导出: 支持导出 Excel 或 CSV 格式
  4. 组织架构树: 以树形结构展示部门层级关系
  5. 数据统计图表: 使用 ECharts 展示部门人员分布
  6. 操作日志: 记录所有编辑操作
  7. 批量导入: 支持批量导入人员信息
  8. 消息通知: 人员变动时发送通知

九、常见问题

Q1: 获取 Access Token 失败?

A: 检查 AppKey 和 AppSecret 是否正确,确保应用有足够的权限。

Q2: 部门人数显示为 0?

A: 确保已执行同步脚本,生成了 departments.jsonusers.json 文件。

Q3: 端口被占用?

A: 修改 app.py 中的端口号,例如改为 5001:

app.run(host='0.0.0.0', port=5001, debug=True)

Q4: 如何部署到生产环境?

A: 建议使用 Gunicorn + Nginx:

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 app:app

十、总结

本文详细介绍了一个完整的钉钉部门和人员管理系统的开发过程。系统采用 Python + Flask 技术栈,实现了数据同步、Web管理、人员编辑等核心功能。

项目优势:

  • 代码简洁,易于理解和维护
  • 功能完整,满足日常管理需求
  • 扩展性强,可根据需求添加新功能
  • 部署简单,无需复杂配置

适用场景:

  • 企业组织架构管理
  • 人员信息统计和分析
  • 钉钉数据本地化存储
  • 自动化办公流程集成

希望本文对你有所帮助!如果觉得不错,欢迎点赞和收藏。有任何问题或建议,欢迎在评论区留言交流。


完整源码: 项目所有代码已开源,可根据实际需求进行修改和扩展。

相关链接:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐