python版钉钉回调

说明:

因为公司项目有两个系统这两个系统都需要同步审批回调事件所以些了这个钉钉回调分发的中转服务。

话不多说开始上代码
1.首先创建出需要的模型字段

from django.db import models


class DingTag(models.Model):
    """钉钉事件"""
    url = models.ForeignKey('Url', models.DO_NOTHING)
    tag = models.CharField(max_length=30, blank=True, null=True)
    remark = models.CharField(max_length=255, blank=True, null=True)
    create_date = models.DateTimeField(blank=True, null=True)
    modify_date = models.DateTimeField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'ding_tag'


class Url(models.Model):
    """需要回调的url"""
    url = models.CharField(max_length=200, blank=True, null=True)
    remark = models.CharField(max_length=50, blank=True, null=True)
    create_date = models.DateTimeField(blank=True, null=True)
    modify_date = models.DateTimeField(blank=True, null=True)
    path = models.CharField(max_length=255)
    db_name = models.CharField(max_length=255, blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'url'

2.封装一个类来加密解密和封装钉钉需要的success返回值

class DingTalkCrypto():
    def __init__(self, encodingAesKey, key):
        self.encodingAesKey = encodingAesKey
        self.key = key
        self.aesKey = base64.b64decode(self.encodingAesKey + '=')
        self.token = '12345'

    def encrypt(self, content):
        """加密"""
        msg_len = self.length(content)
        content = self.generateRandomKey(16) + msg_len.decode() + content + self.key
        contentEncode = self.pks7encode(content)
        iv = self.aesKey[:16]
        aesEncode = AES.new(self.aesKey, AES.MODE_CBC, iv)
        aesEncrypt = aesEncode.encrypt(contentEncode)
        return base64.b64encode(aesEncrypt).decode().replace('\n', '')

    def length(self, content):
        """将msg_len转为符合要求的四位字节长度"""
        l = len(content)
        return struct.pack('>l', l)

    def pks7encode(self, content):
        """安装PKCS#7标准填充字符串"""
        l = len(content)
        output = io.StringIO()
        val = 32 - (l % 32)
        for _ in range(val):
            output.write('%02x' % val)
        return bytes(content, 'utf-8') + binascii.unhexlify(output.getvalue())

    def pks7decode(self, content):
        nl = len(content)
        val = int(binascii.hexlify(content[-1].encode()), 16)
        if val > 32:
            raise ValueError('Input is not padded or padding is corrupt')
        l = nl - val
        return content[:l]

    def decrypt(self, content):
        """解密数据"""
        # 钉钉返回的消息体
        content = base64.b64decode(content)
        iv = self.aesKey[:16]  # 初始向量
        aesDecode = AES.new(self.aesKey, AES.MODE_CBC, iv)
        decodeRes = aesDecode.decrypt(content)[20:].decode().replace(self.key, '')
        return self.pks7decode(decodeRes)

    def generateRandomKey(self, size,
                          chars=string.ascii_letters + string.ascii_lowercase + string.ascii_uppercase + string.digits):
        """生成加密所需要的随机字符串"""
        return ''.join(choice(chars) for i in range(size))

    def generateSignature(self, nonce, timestamp, token, msg_encrypt):
        """生成签名"""
        signList = ''.join(sorted([nonce, timestamp, token, msg_encrypt])).encode()
        return hashlib.sha1(signList).hexdigest()


def result_success(encode_aes_key, token, corp_id):
    """封装success返回值"""
    dtc = DingTalkCrypto(encode_aes_key,corp_id)
    encrypt = dtc.encrypt('success')
    timestamp = str(int(round(time.time())))
    nonce = dtc.generateRandomKey(8)
    # 生成签名
    signature = dtc.generateSignature(nonce, timestamp, token, encrypt)
    # 构造返回数据
    new_data = {
        'msg_signature': signature,
        'timeStamp': timestamp,
        'nonce': nonce,
        'encrypt': encrypt
    }
    return new_data


def encrypt_result(encode_aes_key, din_corpid, encrypt):
    """解密钉钉回调的返回值"""
    dtc = DingTalkCrypto(encode_aes_key, din_corpid)
    return dtc.decrypt(encrypt)


def getDecryptMsg(msg_signature, timeStamp, nonce, content, encode_aes_key, din_corpid):
    dtc = DingTalkCrypto(encode_aes_key, din_corpid)
    sign = dtc.generateSignature(nonce, timeStamp, keys.TOKEN, content)
    if msg_signature != sign:
        raise ValueError('signature check error')

3.向钉钉服务器注册审批事件

def register_call(request):
    access_token = get_token()  # 获取微应用后台免登的access_token
    url = 'https://oapi.dingtalk.com/call_back/update_call_back?access_token=%s' % access_token
    data = {
        'call_back_tag': ['user_add_org', 'user_modify_org', 'user_leave_org', 'user_active_org', 'org_admin_add',
                          'org_admin_remove', 'org_dept_create', 'org_dept_modify', 'org_dept_remove', 'org_remove',
                          'org_change', 'label_user_change', 'label_conf_add', 'label_conf_del', 'label_conf_modify',
                          'edu_user_insert', 'edu_user_update', 'edu_user_delete', 'edu_user_relation_insert',
                          'edu_user_relation_update', 'edu_user_relation_delete', 'edu_dept_insert', 'edu_dept_update',
                          'edu_dept_delete', 'bpms_task_change', 'bpms_instance_change', 'chat_add_member',
                          'chat_remove_member', 'chat_quit', 'chat_update_owner', 'chat_update_title', 'chat_disband',
                          'check_in', 'attendance_check_record', 'attendance_schedule_change', 'attendance_overtime_duration',
                          'meetingroom_book', 'meetingroom_room_info'],
        'token': 自己的token,
        'aes_key': 自己的AES_KEY,
        'url': '你自己的url',
        'access_token': access_token,
      }
      res = requests.post(url, json=data)
      return HttpResponse(res)

3.分发审批事件

def call_back(request):
    """分发审批事件"""
    token = keys.TOKEN
    result_success(keys.AES_KEY, token, keys.CORP_ID)
    data = request.body
    json_data = json.loads(data)
    request_data = request.GET
    getDecryptMsg(request_data['signature'], request_data['timestamp'], request_data['nonce'],
                                 json_data['encrypt'], keys.AES_KEY, keys.CORP_ID)
    type_info = encrypt_result(keys.AES_KEY, keys.CORP_ID, json_data['encrypt'])
    type_name = json.loads(type_info)
    name = type_name.get('EventType')
    print(name)
    # 查询事件
    state = None
    ding_tag = DingTag.objects.filter(tag=name)
    for url in ding_tag:
        urls = url.url.url
        session = requests.session()

        try:
            if url.url.db_name:
                session.get(url=urls + 'web?db={}'.format(url.url.db_name))
                state = session.post(urls + url.url.path, json=json_data)

            else:
                url2 = urls + url.url.path + '?timestamp={0}&nonce={1}&signature={2}'.format(request_data['timestamp'],
                                                                                    request_data['nonce'], request_data['signature'])
                state = requests.post(url2, json=json_data)
        except ConnectionAbortedError:
            logger.error(ConnectionAbortedError)
        if state is None:
            response = result_success(keys.AES_KEY, token, keys.CORP_ID)
            return JsonResponse(response)
    return HttpResponse(state.content)

4.获取请求数数据

@csrf_exempt
def get_data(request):
    """获取请求数据"""
    data = request.POST

    token = get_token()  # 获取微应用后台免登的access_token
    url = 'https://oapi.dingtalk.com/topapi/processinstance/create?access_token=%s' % token
    res = requests.post(url, data)
    response = json.loads(res.text)

    return JsonResponse(response)

钉钉回调分发到此结束,第二次写写的不好之处还望指正。

<think>好的,用户想了解如何使用Python通过钉钉API发送邮件的最佳实践。首先,我需要确认钉钉是否直接支持邮件发送的API。根据我的知识,钉钉的主要API接口更侧重于消息通知、工作流管理等功能,而邮件发送通常是通过SMTP协议或其他邮件服务API实现的。可能需要检查钉钉的官方文档是否有相关接口。 如果钉钉本身不提供发送邮件的API,那么用户可能需要使用钉钉的机器人或Webhook来发送通知,而邮件发送则需集成其他服务,比如企业自建的邮件服务器或第三方邮件API(如SendGrid、阿里云邮件服务等)。这时候需要明确用户的具体需求:是希望通过钉钉内部直接发送邮件,还是将钉钉与其他邮件服务结合使用。 接下来,考虑Python的实现方式。假设用户需要的是在钉钉中触发邮件发送,可能需要分步骤:首先通过钉钉机器人接收指令,然后调用邮件服务API发送邮件。例如,使用requests库调用钉钉的Webhook发送消息,同时使用smtplib或第三方库发送邮件。 另外,需要注意安全性问题,比如API密钥的保护、OAuth认证等。引用[2]中提到设计API时应考虑安全性,如API密钥保护和请求限制,这在实现时需要遵守。例如,将敏感信息存储在环境变量中,而不是硬编码在代码里。 还要考虑错误处理和性能优化,确保API的可靠性和响应速度。例如,添加重试机制、合理的超时设置,以及异步处理邮件发送任务,避免阻塞主线程。 最后,可能需要参考钉钉的官方文档和邮件服务提供商的文档,确保接口调用方式正确,参数符合要求。同时,结合引用[3]中提到的PyFlink API作业开发示例,用户可能需要将邮件发送功能集成到更大的数据处理流程中,因此代码的模块化和可维护性也很重要。 总结步骤:1. 确认钉钉API是否支持邮件发送;2. 如不支持,选择并集成第三方邮件服务;3. 使用Python调用钉钉Webhook和邮件API;4. 处理安全和错误问题;5. 编写并测试代码。</think>### 使用Python通过钉钉API发送邮件的最佳实践 钉钉原生API未直接提供邮件发送功能,但可通过集成钉钉机器人+第三方邮件服务实现。以下是分步实现方案: #### 一、准备工作 1. **创建钉钉机器人** - 在钉钉群组中添加自定义机器人,获取`Webhook URL`和`加签密钥`[^2] - 权限设置建议:选择"自定义关键词",如设定「邮件通知」 2. **选择邮件服务** - 企业场景:使用自建SMTP服务(如Exchange) - 云服务推荐:阿里云DirectMail、SendGrid、AWS SES等 #### 二、Python实现示例(以阿里云邮件服务+钉钉机器人为例) ```python import os import requests from aliyunsdkcore.client import AcsClient from aliyunsdkdm.request.v20151123 import SingleSendMailRequest # 钉钉机器人通知 def dingtalk_alert(content): webhook = os.getenv("DINGTALK_WEBHOOK") secret = os.getenv("DINGTALK_SECRET") headers = {"Content-Type": "application/json"} payload = { "msgtype": "text", "text": {"content": f"邮件系统通知:{content}"} } try: resp = requests.post(webhook, json=payload, headers=headers) resp.raise_for_status() except Exception as e: print(f"钉钉通知失败:{str(e)}") # 发送邮件 def send_email(subject, body, to_address): client = AcsClient( os.getenv("ALI_ACCESS_KEY"), os.getenv("ALI_SECRET"), 'cn-hangzhou' ) request = SingleSendMailRequest.SingleSendMailRequest() request.set_AccountName("noreply@yourdomain.com") request.set_AddressType(1) request.set_ToAddress(to_address) request.set_Subject(subject) request.set_ReplyToAddress(False) request.set_HtmlBody(body) try: client.do_action_with_exception(request) dingtalk_alert(f"已发送邮件至{to_address}") except Exception as e: dingtalk_alert(f"邮件发送失败:{str(e)}") raise ``` #### 三、最佳实践要点 1. **安全控制** - 使用环境变量存储敏感信息(如API密钥)[^2] ```bash export DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx" export ALI_SECRET="xxx" ``` 2. **可靠性保障** - 添加重试机制(建议使用`tenacity`库) - 设置邮件发送速率限制(如使用令牌桶算法) 3. **性能优化** - 使用异步发送(结合`asyncio`和`aiohttp`) - 批量发送时采用连接池 4. **状态跟踪** - 通过钉钉机器人回调接口实现发送状态追踪 - 建议记录邮件发送日志到数据库 #### 四、验证测试 ```python # 测试用例 if __name__ == "__main__": import dotenv dotenv.load_dotenv() send_email( subject="测试邮件", body="<h1>这是一封测试邮件</h1>", to_address="user@example.com" ) ```
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值