1. 项目背景与需求分析
在现代化企业办公场景中,请假审批流程的数字化管理已成为刚需。传统纸质审批或简单线上表单往往存在流程割裂、数据孤岛的问题。我们基于Hyperf框架与企业微信开放平台,设计了一套自动化审批流系统,核心解决以下痛点:
- 流程断点问题:员工在OA系统提交申请后,仍需手动在企业微信发起审批,操作冗余
- 数据关联难题:本地系统生成的申请单与企业微信审批单缺乏关联标识,回调时无法精准匹配
- 状态同步延迟:审批结果无法实时同步回业务系统,需要人工二次确认
典型业务流示例:
code复制员工提交申请 → 生成本地apply_id → 自动创建企业微信审批单 → 审批结果回调 → 通过apply_id关联原工单 → 更新状态
2. 技术架构设计
2.1 系统组件拓扑
mermaid复制graph TD
A[Hyperf应用] -->|调用API| B[企业微信服务端]
B -->|回调通知| C[Webhook接口]
C --> D[MySQL数据库]
A --> D
2.2 关键数据流设计
-
ID关联机制:
- 本地系统生成唯一
apply_id(建议雪花算法) - 创建审批单时将
apply_id写入备注字段(apply_id需Base64编码防特殊字符干扰) - 回调时解析备注字段获取原始ID
- 本地系统生成唯一
-
字段映射表:
| 本地字段 | 企业微信字段 | 处理方式 |
|---|---|---|
| apply_id | remark | Base64编码 |
| 请假类型 | template_content.1 | 枚举值映射 |
| 开始/结束时间 | template_content.2 | 时间戳转"YYYY-MM-DD HH:mm"格式 |
3. 企业微信审批对接实现
3.1 审批模板配置
- 登录企业微信管理后台
- 进入「应用管理」→「审批」→「自定义审批模板」
- 新建请假审批模板时需注意:
- 添加"备注"字段(用于存储apply_id)
- 设置必填字段的校验规则
- 记录模板ID(后续API调用需要)
重要提示:模板发布后需等待5分钟生效,期间API调用可能报错(600011)
3.2 API调用实现
php复制// Hyperf服务代码示例
public function createApproval($applyId, $leaveData)
{
$corpId = config('wechat.corp_id');
$secret = config('wechat.app_secret');
// 获取access_token
$token = $this->getAccessToken($corpId, $secret);
$params = [
'creator_userid' => $leaveData['user_id'],
'template_id' => 'TL-xxxxxxxx', // 审批模板ID
'use_template_approver' => 1,
'approver' => [
// 审批人数据
],
'notifyer' => [
// 抄送人数据
],
'apply_data' => [
'contents' => [
// 审批表单内容
[
'control' => 'Text',
'id' => 'remark',
'value' => [
'text' => base64_encode($applyId) // 关键关联字段
]
]
]
]
];
$client = make(Client::class);
$response = $client->post(
"https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token={$token}",
['json' => $params]
);
// 错误处理逻辑...
}
3.3 高频问题解决方案
-
字段内容超长:
- 企业微信备注字段默认限制512字节
- 解决方案:采用
apply_id压缩存储(原始ID+CRC32校验码)
-
特殊字符处理:
php复制// 安全编码方案 $safeRemark = urlencode( chunk_split( base64_encode($applyId), 76, ' ' ) ); -
审批人动态指定:
- 根据部门架构自动匹配:
php复制$approvers = $this->getDepartmentLeaders( $leaveData['department_id'], $leaveData['leave_days'] );
4. 回调处理机制
4.1 Webhook配置要点
-
URL验证处理:
php复制public function verifyUrl(ServerRequestInterface $request) { $params = $request->getQueryParams(); if (isset($params['echostr'])) { return $params['echostr']; } // ...正常业务处理 } -
回调事件解密:
php复制$encryptMsg = $request->getParsedBody()['Encrypt']; $decryptMsg = $this->decryptMsg( $encryptMsg, config('wechat.encoding_aes_key') ); $eventData = json_decode($decryptMsg, true);
4.2 审批状态同步
典型回调数据结构处理:
php复制$applyId = base64_decode($eventData['Remark']);
$statusMap = [
'1' => 'approved',
'2' => 'rejected',
'3' => 'canceled'
];
$this->updateLeaveStatus(
$applyId,
$statusMap[$eventData['Status']],
$eventData['Approver']['userid']
);
4.3 异常处理策略
-
重复回调:
- 使用Redis记录已处理事件ID
php复制$eventId = $eventData['EventId']; if ($redis->exists("wechat_event:{$eventId}")) { return ['code' => 0]; } $redis->setex("wechat_event:{$eventId}", 86400, 1); -
数据不一致:
- 定时任务补偿机制
php复制$pendingApprovals = $this->getPendingApprovals(); foreach ($pendingApprovals as $apply) { $this->syncApprovalStatus($apply['apply_id']); }
5. 性能优化实践
5.1 请求合并策略
对于批量请假场景:
php复制public function batchCreateApprovals(array $applies)
{
$chunks = array_chunk($applies, 20); // 企业微信批量接口限制
foreach ($chunks as $batch) {
$this->sendBatchRequest($batch);
}
}
5.2 缓存设计
-
AccessToken缓存:
php复制$cacheKey = "wechat:token:{$corpId}"; if ($cache->has($cacheKey)) { return $cache->get($cacheKey); } $token = $this->fetchNewToken($corpId, $secret); $cache->set($cacheKey, $token, 7000); // 提前过期 -
审批模板缓存:
php复制$templates = $cache->remember( 'wechat:approval_templates', 3600, fn() => $this->getAllTemplates() );
6. 安全防护措施
6.1 请求签名验证
php复制$signature = $request->getHeaderLine('wx-signature');
$timestamp = $request->getQueryParams()['timestamp'];
$nonce = $request->getQueryParams()['nonce'];
$valid = $this->checkSignature(
$signature,
$timestamp,
$nonce,
config('wechat.token')
);
if (!$valid) {
throw new InvalidSignatureException();
}
6.2 敏感数据保护
-
审批人信息脱敏:
php复制$approvers = array_map( fn($user) => substr($user['userid'], 0, 3) . '***', $approvers ); -
数据库加密存储:
php复制$encrypted = openssl_encrypt( $remark, 'aes-256-cbc', config('app.encrypt_key'), 0, config('app.iv') );
7. 监控与日志
7.1 关键指标监控
-
成功率看板:
promql复制sum(rate(wechat_approval_created_total{status="success"}[5m])) / sum(rate(wechat_approval_created_total[5m])) -
延迟报警:
php复制$histogram = new Histogram( 'wechat_api_latency_seconds', 'API latency distribution', ['method'] ); $histogram->observe($latency, ['applyevent']);
7.2 诊断日志规范
php复制$this->logger->info('Approval created', [
'apply_id' => $applyId,
'wx_sp_no' => $response['sp_no'],
'cost' => $timer->end(),
'operator' => $user->id
]);
日志字段说明:
wx_sp_no: 企业微信审批单编号cost: 接口耗时(ms)operator: 操作人ID(用于审计)
8. 扩展性设计
8.1 多租户支持
php复制$tenantConfig = $this->getTenantConfig($tenantId);
$client = new Client([
'base_uri' => $tenantConfig['wechat_api_endpoint'],
'timeout' => $tenantConfig['timeout'] ?? 5.0
]);
8.2 审批流插件化
php复制interface ApprovalAdapterInterface
{
public function create(array $data): Response;
public function handleCallback(array $event): void;
}
// 企业微信实现
class WeChatApprovalAdapter implements ApprovalAdapterInterface
{
// 实现具体方法
}
9. 测试策略
9.1 单元测试重点
php复制public function testApplyIdInRemark()
{
$applyId = 'APP20230701123456';
$response = $this->post('/leave', [
'apply_id' => $applyId,
// 其他字段...
]);
$this->assertStringContainsString(
base64_encode($applyId),
$response['remark']
);
}
9.2 集成测试方案
-
Mock服务设计:
php复制$mock = new MockHandler([ new Response(200, [], json_encode([ 'errcode' => 0, 'sp_no' => '202307010001' ])) ]); $client = new Client(['handler' => $mock]); -
回调测试工具:
bash复制curl -X POST \ -H "wx-signature: xxx" \ -d '{"Encrypt":"xxx"}' \ "http://localhost/callback"
10. 部署实践
10.1 容器化配置
dockerfile复制FROM hyperf/hyperf:8.0-alpine
# 安装企业微信SDK依赖
RUN apk add --no-cache libxml2-dev \
&& pecl install xmlrpc \
&& docker-php-ext-enable xmlrpc
COPY . /var/www
WORKDIR /var/www
CMD ["php", "bin/hyperf.php", "start"]
10.2 高可用方案
-
多实例部署:
yaml复制# docker-compose.yml services: worker: image: your-image deploy: replicas: 3 -
消息队列消峰:
php复制$queue->push(new CreateApprovalJob($applyData));
11. 经验总结
在实际落地过程中,我们总结了以下关键经验:
-
ID编码陷阱:
- 发现企业微信备注字段会过滤某些特殊字符
- 最终采用
base64_encode(hex2bin($applyId))方案
-
时区问题:
php复制// 必须显式设置时区 date_default_timezone_set('Asia/Shanghai'); $formatted = date('Y-m-d H:i', $timestamp); -
审批人缓存:
- 企业微信架构API有频率限制(600次/分钟)
- 实现部门领导信息的本地缓存后,性能提升40倍
-
测试环境隔离:
- 使用企业微信「沙箱环境」时发现与实际API存在差异
- 解决方案:维护两套配置模板,通过环境变量切换
-
性能压测数据:
- 单节点处理能力:~120 req/s
- 平均延迟:78ms (P95 < 200ms)
- 建议生产环境至少部署2个worker实例
