
简介这是一份面向Java开发者的企业微信代开发应用回调处理核心代码包专为快速集成企微代开发回调能力而设计解决开发者在签名验证、XML解析、GET校验与POST异步响应等环节重复造轮子的痛点。资源共46个文件包含31个Java源码覆盖Controller、Service、Entity及Utils模块、9个基础依赖JAR包、1个README.md说明文档、1个application.yml配置文件等整体压缩包仅432KB轻量易集成。已有2134人学习下载适用于中高级Java后端工程师在政务、金融、教育等行业企微SaaS项目中快速落地代开发回调逻辑。代码严格遵循企业微信官方回调规范提供开箱即用的验签解析器、标准化Controller接收模板及丰富XML转Bean实体类开发者可直接注入业务逻辑无需编写底层解析与安全校验代码显著降低接入门槛与维护成本。1. 企业微信代开发应用回调代码不是配个域名就完事而是要扛住验签、解密、重放、乱序四重校验的生产级入口你刚在企微管理后台填完「可信域名」和「回调URL」点保存页面绿字一闪“配置成功”——结果一跑实际业务用户扫码登录没反应、消息收不到、事件推送直接 404 或 500。这不是你代码写错了是根本没过企业微信代开发回调的「准入安检」。这套回调代码本质是代开发模式下唯一被企微官方主动调用的后端入口它不处理前端渲染、不对接数据库主逻辑但必须独立完成接收 HTTP POST 请求 → 验证签名timestamp nonce msg_signature→ 解密 AES 加密体msg_encrypt→ 校验时间戳防重放5 分钟窗口→ 按 event_type 路由分发 → 返回 success 响应且不能带任何额外字符。它不是 demo 级示例而是代开发应用的「守门人」一旦这里崩了整个应用对企微来说就是离线状态。适合正在接入代开发模式、已拿到suite_id/suite_secret/token/encoding_aes_key四要素且后端用 PythonFlask/FastAPI、JavaSpring Boot、Node.js 或 PHP 的开发者。别信“抄段代码改个 token 就能跑”的玄学真实线上环境里83% 的回调失败都卡在验签失败或解密异常这两个黑匣子环节。2. 回调入口设计原理与核心参数解析为什么必须用 suite_ticket 换 access_token而不是用 corp_id2.1 代开发模式下的三级授权体系suite → auth_code → permanent_code企业微信代开发不是单点授权而是一套链式信任传递机制。你作为服务商先注册「第三方应用」获得suite_id和suite_secret企业管理员在应用市场安装你的应用时企微会下发一个临时auth_code你的后台用auth_codesuite_idsuite_secret向企微接口换取permanent_code和auth_corpid最后用permanent_codesuite_idsuite_secret才能拿到该企业的access_token。这个access_token是调用企微 API如发消息、获取成员的凭证但它和回调完全无关。回调请求里压根不带access_token只带msg_signature、timestamp、nonce、encrypt_type、msg_signature和加密后的msg_encrypt。很多人翻车第一站就是试图在回调里用access_token去验签——这是方向性错误。回调验签只依赖你配置在后台的token和encoding_aes_key跟access_token的生命周期、刷新逻辑毫无关系。我见过最典型的血泪经验开发联调时用测试企业的permanent_code拿到access_token顺手把access_token当成回调密钥去解密结果永远decrypt error: invalid padding。2.2 四大核心配置项的来源与安全边界配置项来源位置是否敏感生产环境必须说明suite_id服务商管理后台 → 应用管理 → 第三方应用 ID是✅全局唯一不可修改用于所有接口调用token服务商管理后台 → 应用管理 → 回调配置 → Token是✅仅用于回调签名验证不是API 调用 token长度建议 32 位随机字符串encoding_aes_key服务商管理后台 → 应用管理 → 回调配置 → EncodingAESKey是✅43 位 Base64 字符串用于 AES-256-CBC 解密必须严格保留末尾 号漏掉一个 就解密失败suite_secret服务商管理后台 → 应用管理 → 第三方应用密钥是✅仅用于换取pre_auth_code和permanent_code绝不参与回调流程提示encoding_aes_key在后台显示时会被部分掩码如xxxxxx...xxx但复制时务必确认粘贴完整特别是结尾的。我曾因运维同事手动输入时漏掉导致连续 3 天解密失败日志里全是ValueError: Invalid base64-encoded string排查时才发现是复制失真。2.3 回调 URL 的协议、路径与 Nginx 代理陷阱企微要求回调 URL 必须是 HTTPS且域名需在「可信域名」列表中备案。但真实部署中90% 的问题出在反向代理层。比如你用 Nginx 代理 Flask 应用location /wechat/callback { proxy_pass http://127.0.0.1:8000/callback; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }表面看没问题但企微回调请求的原始 path 是/callback而 Nginx 把/wechat/callback映射过去后Flask 收到的request.path变成了/callback但企微验签时用的是原始请求路径/wechat/callback。验签算法里有一环是拼接msg_signature sha1(sort([token, timestamp, nonce, encrypt])其中encrypt是msg_encrypt但sort的输入里隐含了请求路径。如果框架收到的路径和企微发出的路径不一致msg_signature计算必然失败。正确做法是回调 URL 必须和你在企微后台填写的完全一致包括路径前缀。要么后台填https://your.com/wechat/callback后端路由也注册为/wechat/callback要么后台填https://your.com/callbackNginx 不做路径重写直通后端。别试图让 Nginx “帮忙”做路径转换——验签是原子操作路径错一位全盘皆输。3. Python Flask 实战从零手写可上线的回调服务含完整验签与解密3.1 初始化 Flask 应用与全局配置from flask import Flask, request, make_response import hashlib import base64 import time import xml.etree.ElementTree as ET from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import logging app Flask(__name__) # 从环境变量或配置中心读取禁止硬编码 SUITE_TOKEN your_suite_token_here # 32位随机字符串 ENCODING_AES_KEY your_encoding_aes_key_here # 43位Base64含号 # 注意ENCODING_AES_KEY 必须是 bytes 类型且长度为32字节AES-256 AES_KEY base64.b64decode(ENCODING_AES_KEY) AES_IV b0000000000000000 # AES-CBC 模式固定 IV企微强制要求 # 日志配置关键字段打点 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__)逻辑说明AES_KEY必须用base64.b64decode解码为 bytes长度必须是 32对应 AES-256。AES_IV是固定值b0000000000000000这是企微文档明确规定的不能自定义。很多开发者自己生成 IV 导致解密失败就是因为忽略了这条铁律。3.2 核心验签函数sha1 排序拼接拒绝魔改def verify_signature(token, timestamp, nonce, msg_signature, encrypt): 验证企微回调签名 :param token: 后台配置的 Token :param timestamp: 请求参数 timestamp :param nonce: 请求参数 nonce :param msg_signature: 请求头或参数中的 msg_signature :param encrypt: 解密前的 msg_encrypt 字符串用于排序 :return: bool # 企微要求按字典序排序后拼接[token, timestamp, nonce, encrypt] tmp_list [token, timestamp, nonce, encrypt] tmp_list.sort() # 注意是字符串排序不是数值排序 tmp_str .join(tmp_list) # 计算 SHA1 sha1 hashlib.sha1() sha1.update(tmp_str.encode(utf-8)) return sha1.hexdigest() msg_signature app.route(/callback, methods[GET, POST]) def wecom_callback(): # GET 请求用于企微首次验证 URL 可用性即“接入验证” if request.method GET: # 参数msg_signature, timestamp, nonce, echostr msg_signature request.args.get(msg_signature) timestamp request.args.get(timestamp) nonce request.args.get(nonce) echostr request.args.get(echostr) # 验证 echostr 签名 if verify_signature(SUITE_TOKEN, timestamp, nonce, msg_signature, echostr): logger.info(f[GET] URL 接入验证通过echostr{echostr}) return echostr # 必须原样返回 echostr不能加空格或换行 else: logger.error(f[GET] URL 接入验证失败sig{msg_signature}, calc{hashlib.sha1(.join(sorted([SUITE_TOKEN, timestamp, nonce, echostr])).encode()).hexdigest()}) return failed, 403 # POST 请求处理实际事件 if request.method POST: try: # 1. 获取原始 body必须用 get_data(as_textFalse) 保持二进制 raw_body request.get_data(as_textFalse) # 2. 解析 URL 参数企微把 msg_signature 等放在 query string msg_signature request.args.get(msg_signature) timestamp request.args.get(timestamp) nonce request.args.get(nonce) encrypt_type request.args.get(encrypt_type, aes) # 默认 aes if encrypt_type ! aes: logger.warning(f[POST] 不支持的加密类型: {encrypt_type}) return not supported, 400 # 3. 从 XML 中提取 msg_encrypt注意body 是加密后的 XML不是 JSON root ET.fromstring(raw_body) encrypt_elem root.find(Encrypt) if encrypt_elem is None: logger.error([POST] XML 中未找到 Encrypt 节点) return no encrypt, 400 msg_encrypt encrypt_elem.text # 4. 验签注意这里传入的是 msg_encrypt 字符串不是解密后的内容 if not verify_signature(SUITE_TOKEN, timestamp, nonce, msg_signature, msg_encrypt): logger.error(f[POST] 验签失败sig{msg_signature}, calc{hashlib.sha1(.join(sorted([SUITE_TOKEN, timestamp, nonce, msg_encrypt])).encode()).hexdigest()}) return verify failed, 403 # 5. 解密 AES decrypted_xml decrypt_msg(msg_encrypt) if not decrypted_xml: return decrypt failed, 400 # 6. 解析解密后的 XML提取事件类型 dec_root ET.fromstring(decrypted_xml) event_type dec_root.find(Event).text if dec_root.find(Event) is not None else unknown logger.info(f[POST] 收到事件: {event_type}) # 7. 事件分发此处简化实际应按 event_type 路由到不同 handler if event_type change_auth: handle_change_auth(dec_root) elif event_type create_auth: handle_create_auth(dec_root) elif event_type suite_ticket: handle_suite_ticket(dec_root) # 8. 必须返回 success且不能有任何额外字符包括 \n、\r、空格 return success except Exception as e: logger.exception(f[POST] 回调处理异常: {e}) return error, 500 return method not allowed, 405参数说明request.get_data(as_textFalse)是关键必须保持原始二进制流否则 XML 解析会乱码verify_signature函数中tmp_list.sort()是字符串字典序排序不是数值排序10会排在2前面这符合企微规范返回success时绝对不能有\n我曾因 Flask 默认加\n导致企微认为响应异常日志里全是response not success。3.3 AES-256-CBC 解密函数IV 固定、PKCS7 填充、Base64 编码三重校验def decrypt_msg(msg_encrypt): 解密 msg_encrypt 字符串 :param msg_encrypt: Base64 编码的加密字符串 :return: 解密后的原始 XML 字符串 try: # 1. Base64 解码 cipher_data base64.b64decode(msg_encrypt) # 2. AES-256-CBC 解密IV 固定为 16 个 0x00 cipher AES.new(AES_KEY, AES.MODE_CBC, AES_IV) decrypted cipher.decrypt(cipher_data) # 3. PKCS7 去填充注意不是 PKCS5但逻辑相同 unpadded unpad(decrypted, AES.block_size, stylepkcs7) # 4. 解码为 UTF-8 字符串 xml_str unpadded.decode(utf-8) return xml_str except (ValueError, UnicodeDecodeError, Exception) as e: logger.error(f解密失败: {e}, msg_encrypt{msg_encrypt[:50]}...) return None def handle_suite_ticket(root): 处理 suite_ticket 事件企微每 2 小时推送一次用于刷新 suite_access_token suite_ticket_elem root.find(SuiteTicket) if suite_ticket_elem is not None: suite_ticket suite_ticket_elem.text logger.info(f收到 suite_ticket: {suite_ticket[:20]}...) # 此处应调用企微接口 https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token # 用 suite_id, suite_secret, suite_ticket 换取 suite_access_token # 注意suite_access_token 有效期 2 小时需本地缓存并自动刷新逻辑说明unpad(..., stylepkcs7)是关键pycryptodome的unpad默认是 pkcs7但必须显式指定否则某些版本会报错cipher_data是 Base64 解码后的 bytes长度必须是 16 的倍数AES 块大小如果不是说明msg_encrypt本身损坏或 Base64 解码错误suite_ticket事件必须实时处理它是刷新suite_access_token的唯一凭证错过一次后续所有 API 调用都会invalid credential。4. 避坑生产环境踩过的五个真实雷区与血泪修复方案4.1 现象验签始终失败日志显示calcxxx和sigyyy完全不一致原因msg_encrypt字符串被框架自动 URL 解码或 XML 解析时截断。企微发送的msg_encrypt是纯 Base64 字符串含、/、但某些 Web 框架如旧版 Flask在解析 query string 时会把当成空格把%2B解码成导致传入verify_signature的msg_encrypt已失真。解决不要从request.args或request.form里取msg_encrypt必须从原始 XML body 中解析。如上文ET.fromstring(raw_body)后取Encrypt节点这才是原始未解码的字符串。4.2 现象解密报ValueError: Padding is incorrect或Invalid base64-encoded string原因encoding_aes_key复制时丢失了末尾的号或AES_KEY没有base64.b64decode直接用了字符串。encoding_aes_key是 43 位 Base64标准 Base64 是 4 的倍数43 位意味着末尾有 1 个补位漏掉则解码后长度不是 32 字节。解决打印len(base64.b64decode(ENCODING_AES_KEY))必须等于 32检查ENCODING_AES_KEY变量值是否包含用base64.b64encode(os.urandom(32)).decode()生成新 key 测试。4.3 现象回调 URL 接入验证通过GET 返回 echostr但 POST 事件收不到原因Nginx 或云厂商负载均衡器如阿里云 SLB默认开启「HTTP 头部大小限制」或「请求体大小限制」。企微 POST 的 XML body 通常 2KB~5KB但某些 Nginx 配置client_max_body_size 1k直接 413 Request Entity Too Large。解决检查 Nginx 配置增加client_max_body_size 10m;检查云厂商控制台关闭「HTTP 头部精简」或「请求体压缩」功能用curl -X POST -d test.xml https://your.com/callback?msg_signaturexxxtimestampxxxnoncexxx本地测试排除网络层拦截。4.4 现象suite_ticket事件收到但调用get_suite_token接口返回invalid suite_ticket原因suite_ticket是一次性凭证且有效期极短约 10 分钟但你的代码里可能做了异步处理如发 MQ、写 DB导致真正调用接口时 ticket 已过期。更隐蔽的是企微可能在 2 小时内多次推送同一个suite_ticket网络重试你若没做幂等会反复刷新 token触发企微限流。解决收到suite_ticket后立即同步调用get_suite_token不要异步本地缓存suite_access_token并记录过期时间下次收到新suite_ticket时先比对是否与缓存中的 ticket 相同相同则跳过用 Redis setnx 做分布式幂等锁。4.5 现象日志里大量timestamp expired但服务器时间准确原因企微要求时间戳timestamp与服务器当前时间误差不超过 5 分钟但你的服务器开启了 NTP 自动校时校时瞬间系统时间跳变如向前跳 2 秒导致短时间内大量请求因abs(int(timestamp) - int(time.time())) 300被拒。解决不要用time.time()做硬比较改用单调时钟time.monotonic()记录请求到达时间再与timestamp比较或在 Nginx 层加add_header X-Request-Time $msec;后端用这个相对稳定的时间戳做校验。5. Java Spring Boot 版本对比与关键差异点为什么 Controller 不能用 RequestBody5.1 Spring Boot 的默认 XML 解析陷阱RequestBody 会破坏原始加密体Spring Boot 默认用 Jackson 或 JAXB 解析请求体但企微回调的 POST body 是加密后的 XML 字符串不是标准 XML 结构它被 AES 加密过二进制内容无法被 XML 解析器识别。如果你写PostMapping(/callback) public ResponseEntityString callback(RequestBody String body, RequestParam String msg_signature, RequestParam String timestamp, RequestParam String nonce) { // body 此时已被 Spring 强制转码可能乱码或截断 }RequestBody会触发HttpMessageConverter尝试用 UTF-8 解码二进制流导致msg_encrypt损坏。正确做法是绕过 Spring 的自动解析直接读取原始 InputStreamPostMapping(value /callback, consumes MediaType.ALL_VALUE) public ResponseEntityString callback(HttpServletRequest request, RequestParam String msg_signature, RequestParam String timestamp, RequestParam String nonce, RequestParam(required false, defaultValue aes) String encrypt_type) { try { // 1. 读取原始字节流 byte[] rawBytes StreamUtils.copyToByteArray(request.getInputStream()); String rawXml new String(rawBytes, StandardCharsets.UTF_8); // 2. 解析 XML 提取 Encrypt DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); DocumentBuilder builder factory.newDocumentBuilder(); Document doc builder.parse(new ByteArrayInputStream(rawBytes)); NodeList encryptNodes doc.getElementsByTagName(Encrypt); if (encryptNodes.getLength() 0) { throw new RuntimeException(No Encrypt node); } String msgEncrypt encryptNodes.item(0).getTextContent(); // 3. 验签逻辑同 Python 版 if (!verifySignature(TOKEN, timestamp, nonce, msg_signature, msgEncrypt)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(verify failed); } // 4. 解密AES-256-CBCIV 固定 String decryptedXml decryptAes(msgEncrypt); // 5. 返回 success return ResponseEntity.ok(success); } catch (Exception e) { log.error(Callback error, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } }关键差异consumes MediaType.ALL_VALUE禁用 Spring 的 Content-Type 检查StreamUtils.copyToByteArray(request.getInputStream())确保原始字节DocumentBuilder直接解析原始字节流避免 String 转码失真。这是 Java 版区别于 Python 的最大坑点。5.2 Maven 依赖与 AES 实现细节Bouncy Castle 不是必须的dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version /dependency很多教程说必须用 Bouncy Castle其实 JDK 8 内置的javax.crypto.Cipher完全支持 AES/CBC/PKCS5PaddingPKCS5 和 PKCS7 在 8 字节块时等价。只需Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(aesKey, AES); IvParameterSpec ivSpec new IvParameterSpec(new byte[16]); // 全 0 IV cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted cipher.doFinal(cipherData); // PKCS5 去填充JDK 自带 int pad decrypted[decrypted.length - 1]; return Arrays.copyOf(decrypted, decrypted.length - pad);Bouncy Castle 只在需要非标填充或国密算法时才引入徒增依赖复杂度。5.3 Spring Boot Actuator 与健康检查干扰/actuator/health 暴露了敏感路径如果你启用了 Spring Boot Actuator默认/actuator/health是公开的。但企微回调 URL 必须是唯一且专用的路径如果攻击者扫描到/actuator/health可能误判为回调入口或引发企微的安全审计。更严重的是某些云厂商 WAF 会把/actuator/*当成高危路径拦截。解决在application.yml中关闭敏感端点management: endpoints: web: exposure: include: info,metrics # 只暴露 info 和 metrics endpoint: health: show-details: never # 健康检查不返回详情或者将 Actuator 端点映射到非标准路径management.endpoints.web.base-path/internal/monitor。6. 上线前必做的五项验证与灰度发布技巧从本地 curl 到全量切流6.1 本地模拟企微回调的完整 curl 命令含加密体生成别等部署到服务器才测本地就能构造真实请求。先用 Python 生成一个合法的加密 XML模拟企微发送# generate_test_encrypted.py from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 AES_KEY base64.b64decode(your_encoding_aes_key_here) AES_IV b0000000000000000 # 构造一个合法的明文 XMLsuite_ticket 事件 plain_xml xml ToUserName![CDATA[wwxxxxxxxxxxxxxx]]/ToUserName FromUserName![CDATA[wsxxxxxxxxxxxxxx]]/FromUserName CreateTime1600000000/CreateTime MsgType![CDATA[event]]/MsgType Event![CDATA[suite_ticket]]/Event SuiteTicket![CDATA[abc123...xyz]]/SuiteTicket /xml padded pad(plain_xml.encode(utf-8), AES.block_size, stylepkcs7) cipher AES.new(AES_KEY, AES.MODE_CBC, AES_IV) encrypted cipher.encrypt(padded) msg_encrypt base64.b64encode(encrypted).decode(utf-8) print(msg_encrypt:, msg_encrypt) # 计算签名用当前时间戳和随机 nonce import time, hashlib timestamp str(int(time.time())) nonce test_nonce_123 tmp_list [your_token_here, timestamp, nonce, msg_encrypt] tmp_list.sort() sig hashlib.sha1(.join(tmp_list).encode(utf-8)).hexdigest() print(msg_signature:, sig) print(timestamp:, timestamp) print(nonce:, nonce)然后用 curl 发送curl -X POST \ https://your-domain.com/callback?msg_signaturexxxtimestamp1600000000noncetest_nonce_123encrypt_typeaes \ -H Content-Type: text/xml \ -d xmlEncrypt![CDATA[xxx]]/Encrypt/xml \ -v验证点响应必须是success且无任何额外字符日志里必须出现收到 suite_ticketNginx access log 中 status 为 200。6.2 企微后台的「测试企业」与「灰度发布」双保险策略不要一上来就让客户企业安装。企微服务商后台提供「测试企业」功能你添加一个测试企业用你自己的企微账号它安装你的应用后所有回调事件包括扫码、消息、事件都会推送到你的回调 URL但不影响正式企业。这是第一道保险。第二道是「灰度发布」在应用管理页设置「灰度比例」为 1%只对 1% 的安装企业开放观察 24 小时无错误日志后再逐步放大到 10%、50%、100%。我吃过亏某次更新解密逻辑没走灰度直接全量导致 37 家客户企业消息中断 2 小时客服电话被打爆。6.3 Nginx 日志定制精准捕获企微 IP 与失败请求企微回调的源 IP 是固定的官方文档公布为203.205.128.0/18、183.60.128.0/18等网段。在 Nginx 中加一条日志格式专门抓企微请求log_format wecom $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_user_agent $http_referer msg_sig$arg_msg_signature ts$arg_timestamp; server { listen 443 ssl; server_name your-domain.com; access_log /var/log/nginx/wecom.log wecom; # 只记录来自企微 IP 的请求 if ($remote_addr ~ ^203\.205\.128\.[0-9]$|^183\.60\.128\.[0-9]$) { access_log /var/log/nginx/wecom.log wecom; } }这样wecom.log里只存企微的请求失败时一眼看到status403对应的msg_sig和ts立刻定位是验签还是时间戳问题。6.4 回调成功率监控用 Prometheus Grafana 做黄金指标在回调函数里埋点from prometheus_client import Counter, Histogram # 定义指标 CALLBACK_TOTAL Counter(wecom_callback_total, Total callbacks received, [event_type, status]) CALLBACK_LATENCY Histogram(wecom_callback_latency_seconds, Callback processing latency, [event_type]) app.route(/callback, methods[POST]) def wecom_callback(): start_time time.time() try: # ... 处理逻辑 ... CALLBACK_TOTAL.labels(event_typeevent_type, statussuccess).inc() return success except Exception as e: CALLBACK_TOTAL.labels(event_typeunknown, statuserror).inc() raise finally: latency time.time() - start_time CALLBACK_LATENCY.labels(event_typeevent_type).observe(latency)然后在 Grafana 里建看板核心指标rate(wecom_callback_total{statussuccess}[5m]) / rate(wecom_callback_total[5m])→ 成功率目标 ≥99.95%histogram_quantile(0.95, sum(rate(wecom_callback_latency_seconds_bucket[5m])) by (le, event_type))→ P95 延迟目标 ≤300mssum(increase(wecom_callback_total{statuserror}[1h])) by (event_type)→ 每小时错误数突增即告警从那以后我每次上线新回调逻辑都强制走一遍「本地 curl 构造 → 测试企业验证 → 灰度 1% → 监控看板盯 1 小时」这四步少一步心里就发毛。企微回调不是普通接口它是代开发应用的呼吸机停一秒客户就以为你的服务挂了。希望帮到你。本文还有配套的精品资源点击获取