news 2026/10/14 1:52:25

PHP对接钉钉审批流:从发起到回调回写的完整闭环实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP对接钉钉审批流:从发起到回调回写的完整闭环实现

简介:本资源是一套基于PHP与钉钉开放平台深度集成的完整审批流实现方案,面向中高级PHP开发者及企业级应用系统维护人员,解决PHP后端主动发起审批申请、对接钉钉审核流程、同步更新业务数据等核心问题。压缩包为21KB的ZIP文件,共含12个PHP脚本,涵盖数据库连接(get_db_conn.php)、用户登录鉴权(UserLogin.php)、钉钉加解密(DingtalkCrypt.php、pkcs7Encoder.php、sha1.php)、审批请求封装(send_apply.php)、部门/基础数据获取(get_dept.php、basic_data.php)、错误码管理(errorCode.php)及全流程测试入口(test.php、PrCreate.php)等关键模块,结构清晰、职责分明,便于快速嵌入现有系统。目前已有193人学习下载,提供开箱即用的接口调用逻辑、标准化的数据组装方式与典型异常处理机制,特别适合需要快速对接钉钉OA审批、构建内外协同业务流程的中小型项目团队。

1. 钉钉审批流与 PHP 后端联动:不是调个 API 就完事,而是打通「申请—审核—回写」闭环

你有没有遇到过这种场景:用户在 PHP 系统里点一下「提交报销」,状态却卡在「待审批」,钉钉里流程走完了,PHP 数据库里的status字段还是0?或者更糟——审批人点「通过」后,PHP 端压根没收到通知,财务还得手动去后台改数据?这不是接口没通,是整个审批闭环断在了「结果回写」这一环。本资源是一套经过某公司真实产线验证的 PHP + 钉钉审批集成方案,覆盖从 PHP 主动发起审批单(含附件、多级审批人、自定义表单字段)、监听钉钉回调事件(审批通过/拒绝/转交)、解析加密签名、校验事件合法性,到最终安全更新 PHP 业务数据库的完整链路。它不依赖任何第三方 SDK 封装黑匣子,所有核心逻辑用原生 cURL + OpenSSL + JSON 实现,代码可调试、参数可追踪、错误可定位。适合正在做 OA 对接、HR 系统升级、或需要将钉钉审批深度嵌入自有 PHP 业务流的开发者——尤其当你已经踩过「回调收不到」「签名验不过」「审批人 ID 对不上」这类坑时,这份资源就是你缺的那块拼图。


2. 审批发起:用 PHP 构造合规的 dingtalk.openapi.approval.create 请求

要让钉钉真正“认”你的审批单,光填个 title 和 content 远远不够。钉钉审批 API 对请求体结构、时间戳、签名方式、审批模板 ID 绑定有强约束。本节带你手写一个可复用的审批发起类,绕过 SDK 封装带来的参数不可见问题。

2.1 审批模板 ID 获取与字段映射确认

钉钉审批流必须绑定一个已发布的「审批模板」,其 ID(process_code)不是后台 URL 里的数字,而是形如PROC-XXXXX-XXXXX-XXXXX的字符串。切记:这个 ID 必须在钉钉管理后台「工作台 > 审批 > 模板管理」中复制「模板编码」,而非「模板ID」或「URL 参数」。
获取后,需人工核对模板内字段与 PHP 提交数据的映射关系。例如:

  • 模板中「报销金额」字段类型为number,对应formComponentValues中的value必须是数字(非字符串"1200.00");
  • 「附件」字段类型为attachment,其value是钉钉上传后返回的media_id列表(非本地文件路径);
  • 「申请人部门」若为组织架构字段,value应为部门 ID(如123456789),而非部门名称。

提示:钉钉模板字段 ID(componentId)在模板编辑页「高级设置 > 字段属性」中可见,PHP 提交时formComponentValues的name必须与此完全一致,大小写敏感。

2.2 构造带签名的 POST 请求体

钉钉开放平台要求所有写操作(包括创建审批单)必须携带access_token,且该 token 需通过app_key+app_secret调用/gettoken接口获取。关键点在于:token 有效期 2 小时,但 PHP 进程不能每次请求都重新获取。本方案采用内存缓存 + 时间戳校验策略:

<?php class DingTalkApprovalSender { private $appKey = 'your_app_key_here'; private $appSecret = 'your_app_secret_here'; private $tokenCacheFile = '/tmp/dt_access_token.cache'; private function getAccessToken() { // 先读缓存 if (file_exists($this->tokenCacheFile)) { $cache = json_decode(file_get_contents($this->tokenCacheFile), true); if ($cache && $cache['expires_at'] > time()) { return $cache['access_token']; } } // 缓存失效或不存在,调用接口 $url = "https://oapi.dingtalk.com/gettoken?appkey={$this->appKey}&appsecret={$this->appSecret}"; $response = json_decode(file_get_contents($url), true); if (!isset($response['access_token'])) { throw new Exception("Failed to get access token: " . json_encode($response)); } // 写入缓存(有效期 110 分钟,预留 10 分钟缓冲) $cacheData = [ 'access_token' => $response['access_token'], 'expires_at' => time() + 110 * 60 ]; file_put_contents($this->tokenCacheFile, json_encode($cacheData)); return $response['access_token']; } }

逻辑说明:

  • $this->tokenCacheFile使用绝对路径(如/tmp/),避免因 PHP 运行用户权限导致写入失败;
  • expires_at设为time() + 110 * 60(110 分钟),而非官方文档写的 120 分钟,是为网络延迟和时钟漂移留出安全余量;
  • 若file_get_contents($url)失败(如 DNS 解析超时),需捕获false并抛出异常,否则后续请求必 401。

2.3 组装审批单并发送

构造请求体时,process_code、originator_user_id(申请人钉钉 ID)、dept_id(申请人部门 ID)为必填项。formComponentValues是核心,其结构必须严格匹配模板字段:

<?php public function createApproval($approvalData) { $accessToken = $this->getAccessToken(); $url = "https://oapi.dingtalk.com/topapi/processinstance/create?access_token={$accessToken}"; // 注意:钉钉要求 originator_user_id 是用户在当前企业的唯一标识(非手机号、非邮箱) // 必须通过企业内部通讯录接口 /user/get_by_mobile 或 /user/get 获取 $payload = [ 'process_code' => 'PROC-XXXXX-XXXXX-XXXXX', // 替换为实际模板编码 'originator_user_id' => $approvalData['originator_id'], // 钉钉用户ID 'dept_id' => $approvalData['dept_id'], // 部门ID 'approvers' => [ // 审批人列表,支持多个 ['userid' => $approvalData['approver1_id']], ['userid' => $approvalData['approver2_id']] ], 'cc_list' => [$approvalData['cc_user_id']], // 抄送人 'form_component_values' => [ [ 'name' => 'amount', // 模板中「报销金额」字段的 componentId 'value' => (float)$approvalData['amount'] // 强制转 float,避免字符串导致验签失败 ], [ 'name' => 'description', 'value' => htmlspecialchars($approvalData['desc'], ENT_QUOTES, 'UTF-8') // 防 XSS ], [ 'name' => 'attachments', 'value' => json_encode($approvalData['media_ids']) // media_id 数组需 JSON 编码为字符串 ] ] ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json; charset=utf-8' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { throw new Exception("DingTalk API error {$httpCode}: {$response}"); } $result = json_decode($response, true); if (!isset($result['process_instance_id'])) { throw new Exception("Failed to create approval: " . json_encode($result)); } return $result['process_instance_id']; // 返回钉钉生成的实例ID,用于后续查询 }

参数说明:

  • approvers是二维数组,每个元素是一个['userid' => 'xxx'],表示一级审批人;若需会签,可传多个同级;若需串签,需在钉钉模板中配置审批路线;
  • form_component_values中value的类型必须与模板字段类型严格一致(number → float/int,text → string,attachment → JSON string of array);
  • curl_setopt($ch, CURLOPT_TIMEOUT, 30)设为 30 秒,避免因钉钉侧响应慢导致 PHP 进程长时间阻塞;
  • htmlspecialchars()对描述字段转义,防止用户输入<script>等恶意内容污染钉钉审批页。

3. 回调监听:接收钉钉事件并完成 PHP 数据库回写

审批单被处理后,钉钉会向你配置的「事件订阅 URL」推送 HTTPS POST 请求。这不是简单的 Webhook,而是包含 AES 加密、签名验证、事件解密三重关卡的硬核流程。跳过任一环节,你的 PHP 就永远收不到「已通过」通知。

3.1 钉钉事件订阅配置与加解密密钥

在钉钉开放平台「应用开发 > 企业内部应用 > 事件订阅」中,需配置:

  • 订阅 URL:你的 PHP 接收脚本地址(如https://yourdomain.com/dt-callback.php),必须是 HTTPS;
  • Token:自定义字符串(如my_dingtalk_token),用于生成签名;
  • AES Key:32 位 Base64 字符串(如ZmRjYzQyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg=),由钉钉生成,务必保存好,丢失无法解密;
  • 加密类型:选择AES(非明文)。

注意:AES Key 是二进制密钥经 Base64 编码后的字符串,PHP 解密时需先base64_decode()还原为 32 字节原始密钥。

3.2 完整回调入口脚本(dt-callback.php)

此脚本是整个闭环的神经中枢,必须能独立运行、无框架依赖、日志完备:

<?php // dt-callback.php define('DT_TOKEN', 'my_dingtalk_token'); define('DT_AES_KEY', 'ZmRjYzQyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg='); define('DT_ENCODING_AES_KEY', base64_decode(DT_AES_KEY)); // 解码为二进制密钥 define('LOG_FILE', '/var/log/dingtalk/callback.log'); function logToFile($msg) { $timestamp = date('Y-m-d H:i:s'); file_put_contents(LOG_FILE, "[{$timestamp}] {$msg}\n", FILE_APPEND | LOCK_EX); } // 1. 验证签名(钉钉要求:sha256 + token + timestamp + nonce) $signature = $_GET['signature'] ?? ''; $timestamp = $_GET['timestamp'] ?? ''; $nonce = $_GET['nonce'] ?? ''; if (empty($signature) || empty($timestamp) || empty($nonce)) { http_response_code(400); echo 'Missing signature/timestamp/nonce'; exit; } $expectedSignature = hash_hmac('sha256', $timestamp . $nonce . DT_TOKEN, DT_TOKEN); if ($signature !== $expectedSignature) { logToFile("Signature mismatch: got {$signature}, expected {$expectedSignature}"); http_response_code(401); echo 'Invalid signature'; exit; } // 2. 获取并解密 POST 数据(AES-256-CBC) $input = file_get_contents('php://input'); if (!$input) { logToFile('Empty POST body'); http_response_code(400); echo 'Empty body'; exit; } // 钉钉加密格式:前 16 字节 IV,后为密文 $iv = substr($input, 0, 16); $ciphertext = substr($input, 16); // AES-256-CBC 解密 $decrypted = openssl_decrypt($ciphertext, 'AES-256-CBC', DT_ENCODING_AES_KEY, OPENSSL_RAW_DATA, $iv); if ($decrypted === false) { logToFile('AES decrypt failed: ' . openssl_error_string()); http_response_code(400); echo 'Decrypt failed'; exit; } // 3. 解析 XML(钉钉事件为 XML 格式,非 JSON!) $xml = simplexml_load_string($decrypted); if ($xml === false) { logToFile('XML parse failed: ' . $decrypted); http_response_code(400); echo 'Invalid XML'; exit; } // 4. 提取关键事件字段 $eventType = (string)$xml->EventType; $processInstanceId = (string)$xml->ProcessInstanceID; $status = (string)$xml->Status; // 'agree' / 'refuse' / 'redirect' logToFile("Received event: {$eventType}, instance: {$processInstanceId}, status: {$status}"); // 5. 根据事件类型执行业务逻辑(此处仅示意,实际需查库、更新状态、发通知等) if ($eventType === 'bpms_instance_change' && $status === 'agree') { updatePhpDatabase($processInstanceId, 'approved'); } elseif ($eventType === 'bpms_instance_change' && $status === 'refuse') { updatePhpDatabase($processInstanceId, 'rejected'); } // 6. 返回 success 响应(钉钉要求,否则重试) echo 'success'; exit; function updatePhpDatabase($instanceId, $newStatus) { // 示例:根据 process_instance_id 关联到你的业务表 // SELECT * FROM approval_records WHERE dt_instance_id = ? // UPDATE approval_records SET status = ?, updated_at = NOW() WHERE dt_instance_id = ? // 此处替换为你的 PDO/MySQLi 实际操作 $pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass'); $stmt = $pdo->prepare("UPDATE approval_records SET status = ?, updated_at = NOW() WHERE dt_instance_id = ?"); $stmt->execute([$newStatus, $instanceId]); logToFile("DB updated for {$instanceId} -> {$newStatus}"); }

逻辑说明:

  • hash_hmac('sha256', ...)是钉钉签名算法,必须用sha256,不能用md5或sha1;
  • openssl_decrypt()的OPENSSL_RAW_DATA标志必须设置,否则解密失败;
  • $xml->EventType是钉钉事件类型,bpms_instance_change表示审批实例状态变更,这是你最关心的事件;
  • updatePhpDatabase()是占位函数,实际需对接你的业务表结构,强烈建议在此处加事务和幂等判断(同一ProcessInstanceID可能因网络问题重复推送)。

3.3 事件字段解析与业务映射表

钉钉推送的 XML 字段名固定,但含义需结合业务理解。以下是关键字段对照表:

XML 字段名类型说明PHP 业务映射建议
EventTypestring事件类型,如bpms_instance_change仅处理此类型,忽略check_url等测试事件
ProcessInstanceIDstring钉钉生成的唯一审批实例ID作为关联 PHP 业务表的外键(dt_instance_id)
Statusstringagree(通过)、refuse(拒绝)、redirect(转交)直接映射为status字段值(approved/rejected)
Resultstring审批意见(用户填写的「同意理由」)存入approval_comment字段,需htmlspecialchars()转义
CreateTimestring时间戳(毫秒),如1623456789000转为date('Y-m-d H:i:s', $ts/1000)存入approved_at

提示:CreateTime是毫秒级时间戳,PHPdate()函数只接受秒级,务必除以 1000。


4. 避坑指南:五个血泪经验总结的高频翻车点

钉钉审批集成最折磨人的不是代码,而是那些文档没写、报错不明确、但会让你调试三天的隐藏陷阱。以下是某公司产线实测中踩出的五个典型坑,按「现象 → 原因 → 解决」结构整理,每一条都配了可直接复用的验证方法。

4.1 现象:回调 URL 测试一直失败,钉钉控制台显示「验证失败」

原因:钉钉在首次配置事件订阅时,会向你的 URL 发送一个EventType=check_url的 GET 请求,要求返回success字符串。但你的脚本可能只处理 POST,或对 GET 请求返回了 HTML 页面、404、甚至重定向。
解决:在dt-callback.php开头添加强制 GET 验证逻辑:

if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['echostr'])) { // 钉钉验证 URL 时的 GET 请求 echo $_GET['echostr']; exit; }

验证方法:在浏览器访问https://yourdomain.com/dt-callback.php?echostr=test123,应直接输出test123,无任何额外字符或换行。

4.2 现象:PHP 收到回调,但simplexml_load_string()返回 false,日志里是乱码

原因:钉钉加密后的 XML 数据是二进制流,file_get_contents('php://input')读取后未正确处理编码,或openssl_decrypt()的 IV 提取错误(长度非 16 字节)。
解决:在解密前打印strlen($input)和bin2hex(substr($input,0,20)),确认总长 ≥16 且前 16 字节是 IV。修正解密代码:

if (strlen($input) < 16) { logToFile("Input too short: " . strlen($input)); exit; } $iv = substr($input, 0, 16); $ciphertext = substr($input, 16);

验证方法:用openssl_encrypt()在本地模拟加密,再用相同 IV 和密钥解密,确保能还原原文。

4.3 现象:审批通过后,PHP 数据库状态没更新,但日志显示Received event...

原因:updatePhpDatabase()中的 SQL 语句未匹配到记录,因为ProcessInstanceID在 PHP 侧存储时被截断或格式错误。钉钉的ProcessInstanceID是 32 位以上字符串(如i-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx),而你的数据库字段可能是VARCHAR(32)。
解决:将业务表中存储钉钉实例 ID 的字段改为VARCHAR(64)或TEXT,并检查插入时是否用了trim()去除了不可见字符。
验证方法:在日志中打印strlen($processInstanceId),确认长度 >32;执行SELECT LENGTH(dt_instance_id) FROM approval_records WHERE id=1查看字段实际长度。

4.4 现象:同一个审批单,PHP 收到两次agree事件

原因:钉钉为保证投递可靠性,会对未及时响应的回调进行重试(间隔约 1s、3s、9s)。若你的脚本执行时间 >1s(如数据库慢查询、网络请求阻塞),钉钉会认为失败并重发。
解决:在updatePhpDatabase()前加幂等判断:

$stmt = $pdo->prepare("SELECT id FROM approval_records WHERE dt_instance_id = ? AND status IN ('approved','rejected')"); $stmt->execute([$instanceId]); if ($stmt->fetch()) { logToFile("Duplicate event for {$instanceId}, skipped"); return; // 已处理过,直接退出 }

验证方法:手动在数据库中将某条记录status改为approved,再用curl模拟推送相同ProcessInstanceID的 XML,观察日志是否跳过。

4.5 现象:审批人点击「通过」,但钉钉提示「审批人无权限」,PHP 却收到了agree

原因:originator_user_id(申请人)和approvers中的userid必须属于同一企业,并且该用户在钉钉后台「通讯录」中状态为「在职」。若用测试账号(如离职员工、外部联系人)发起,钉钉前端会拦截,但事件仍可能推送。
解决:在发起审批前,调用钉钉/user/get接口校验originator_user_id和approver_userid是否有效:

$userUrl = "https://oapi.dingtalk.com/user/get?access_token={$token}&userid={$userid}"; $userResp = json_decode(file_get_contents($userUrl), true); if (!isset($userResp['userid']) || $userResp['state'] !== 'active') { throw new Exception("Invalid or inactive user: {$userid}"); }

验证方法:用 Postman 访问/user/get,传入待校验的userid,检查返回的state字段是否为active。


5. 安全加固与生产环境部署 checklist

把功能跑通只是第一步,放到生产环境必须面对并发、重放、注入、密钥泄露等现实问题。这里不讲理论,只列 7 条我在线上系统强制执行的硬性规则,每一条都来自真实故障复盘。

5.1 回调 URL 必须启用双向证书认证(mTLS)

钉钉虽不强制,但攻击者可伪造 POST 请求向你的回调地址发包。解决方案是:在 Nginx/Apache 上配置客户端证书验证,仅允许钉钉官方 IP 段(100.64.0.0/10,100.127.0.0/16)且持有钉钉 CA 签发证书的请求通过。
配置要点(Nginx):

location /dt-callback.php { ssl_client_certificate /path/to/dingtalk-ca.crt; ssl_verify_client on; ssl_verify_depth 2; # 其他 proxy_pass 配置... }

提示:钉钉 CA 证书需从钉钉开放平台下载,每年更新,必须监控过期时间。

5.2 所有钉钉用户 ID 必须二次校验,禁止直接入库

originator_user_id和approvers.userid是钉钉分配的字符串,但攻击者可篡改请求体。必须在 PHP 侧调用/user/get接口反查,且只信任返回中的userid字段(忽略mobile、email等可伪造字段)。校验缓存时间设为 5 分钟,避免频繁调用。

5.3 AES 密钥绝不硬编码,必须由运维注入

DT_AES_KEY不能写在 PHP 文件里。生产环境使用环境变量:

# 启动 PHP-FPM 前 export DT_AES_KEY="ZmRjYzQyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg="

PHP 中读取:

define('DT_AES_KEY', $_ENV['DT_AES_KEY'] ?? getenv('DT_AES_KEY'));

血泪经验:某次代码误提交到 GitHub,硬编码密钥泄露,导致攻击者可解密所有历史审批数据。

5.4 审批单创建与回调处理必须分离进程

createApproval()是同步阻塞操作,而回调处理需高可用。必须将回调逻辑拆为独立的守护进程(如用 Supervisor 管理的 PHP CLI 脚本),通过 Redis 队列接收事件,避免 Web 服务器超时或崩溃影响事件消费。
队列伪代码:

// dt-callback.php 中 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->lPush('dingtalk_events', $decrypted); // 入队 echo 'success'; // 立即返回 // worker.php 中(常驻进程) while (true) { $eventXml = $redis->rPop('dingtalk_events'); if ($eventXml) processEvent($eventXml); usleep(100000); // 100ms 间隔 }

5.5 数据库更新必须加行级锁,防并发冲突

当多个审批人同时操作同一单(如转交+通过),或钉钉重试导致并发更新,UPDATE approval_records SET status=?可能覆盖彼此。正确做法:

// 先 SELECT FOR UPDATE 锁定行 $stmt = $pdo->prepare("SELECT id, status FROM approval_records WHERE dt_instance_id = ? FOR UPDATE"); $stmt->execute([$instanceId]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { logToFile("Record not found for {$instanceId}"); return; } // 检查当前状态是否允许更新(如已通过则不再处理拒绝) if ($row['status'] === 'approved' && $newStatus === 'rejected') { logToFile("Skip rejected: already approved"); return; } // 执行更新 $stmt = $pdo->prepare("UPDATE approval_records SET status = ?, updated_at = NOW() WHERE id = ?"); $stmt->execute([$newStatus, $row['id']]);

5.6 日志必须结构化,且包含可追溯的 trace_id

所有关键操作(发起审批、收到回调、DB 更新)的日志必须包含唯一trace_id,便于全链路排查。生成方式:

$traceId = 'dt-' . date('ymd') . '-' . str_pad(rand(0, 9999), 4, '0', STR_PAD_LEFT) . '-' . uniqid('', true); logToFile("[{$traceId}] Create approval for user: {$userId}");

从那以后我每次上线新版本,都强制走一遍「模拟审批→查日志→按 trace_id 追踪全链路」,哪怕多花 10 分钟。线上问题 80% 能在 5 分钟内定位到是 PHP 还是钉钉侧的问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 1:50:19

Unity硬表面模型描边断裂排查:顶点法线与网格数据的修复实战

前段时间我在处理风格化项目时碰到一个特别折磨人的问题&#xff1a;角色模型的描边一切正常&#xff0c;但凡是硬表面模型——机械臂、枪械零件、建筑外挂件——描边就像被切成一段一段的虚线&#xff0c;尤其在棱角转角处直接断掉&#xff0c;严重的地方甚至能看见模型内部裸…

作者头像 李华
网站建设 2026/10/14 1:49:00

MFC集成libVLC实现视频局部放大:坐标映射与渲染优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:48:46

企业级定位器选型:API对接能力比硬件参数更关键

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:48:18

YOLO目标检测实战:从Faster R-CNN选型到Darknet训练避坑指南

简介&#xff1a;这份文档面向深度学习入门者与计算机视觉方向的学生、开发者&#xff0c;系统梳理基于YOLO的目标检测知识体系&#xff0c;帮助读者从算法演进到工程落地建立完整认知。内容涵盖深度学习发展脉络与目标检测关键时间节点&#xff0c;重点讲解Faster R-CNN两阶段…

作者头像 李华
网站建设 2026/10/14 1:47:45

嵌入式自学习模式超时退出机制与串口联动设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华