Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wechat Work External Contact Bundle Laravel Package

tourze/wechat-work-external-contact-bundle

Symfony bundle from tourze that integrates WeChat Work (企业微信) external contact features into PHP applications. Use it to wire external contact management into an existing Symfony or tourze-based project with minimal configuration and boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

This bundle wraps WeCom (企业微信) External Contact APIs — the "客户联系" surface used for customer lists, customer tags, group chats, and contact-change callbacks. The 0.0.2 release ships no documented changes beyond the compare link, so treat 0.0.1 and 0.0.2 as functionally identical until you read the actual diff (composer diff-style: git log 0.0.1..0.0.2 --stat on the GitHub compare page).

Minimal path:

  1. composer require tourze/wechat-work-external-contact-bundle — then immediately pin the version in composer.json ("0.0.2", not ^0.0). At 0.0.x, patch bumps can carry breaking signature changes.
  2. Publish config if a service provider exists: php artisan vendor:publish --tag=wechat-work-external-contact. In the Tourze ecosystem some bundles register config purely via Symfony-style extension classes — if the publish command finds nothing, check src/DependencyInjection/ and src/Resources/config/ and register the service provider manually in bootstrap/providers.php.
  3. Populate env: WECHAT_WORK_CORP_ID, WECHAT_WORK_AGENT_ID, and critically the external contact secret (客户联系 Secret from the admin console) — not the app secret. They are different secrets with different scopes and using the wrong one yields 60011 no privilege.
  4. Callback config: WECHAT_WORK_CALLBACK_TOKEN, WECHAT_WORK_CALLBACK_AES_KEY, WECHAT_WORK_CORP_ID — the AES key is 43 chars, and the callback URL must be reachable over plain GET for the initial verification.

Where to look first: the API client class (grep for getAccessToken / HttpClient / Guzzle) and the DTOs/response models. That single file tells you whether the bundle caches tokens itself or expects you to. Everything else is plumbing around it.

First use case: backfill your local customer table. Resolve the client from the container, call the "get external contact list" endpoint with a staff userid, then "get external contact detail" per external_userid, and upsert into a wecom_customers table keyed on external_userid. That single job proves credentials, permissions, pagination, and rate limiting all at once — do it before touching callbacks.

Implementation Patterns

Bind the client, don't new it. Resolve through the container so you can swap it in tests:

$client = app(\Tourze\WechatWorkExternalContactBundle\Client\ExternalContactClient::class);

If no facade is provided, wrap it in your own thin WecomExternalContact service and bind that — you want one seam to mock later.

Paginated sync as a queued job, cursor-driven. Every list endpoint returns next_cursor; loop until empty, but never loop unbounded inside a request. Shape it as: a SyncExternalContacts job that processes one page, dispatches itself with the next cursor, and stops on empty cursor. Guard against cursor stalls (log and bail if the same cursor returns twice).

Change callbacks are the real integration surface. WeCom pushes change_external_contact events with ChangeType values: add_external_contact, del_external_contact, add_half_external_contact, edit_external_contact, del_follow_user, transfer_fail. Pattern:

// routes/web.php — no auth middleware, no CSRF
Route::post('/wecom/callback', WecomCallbackController::class);

Controller: verify signature → decrypt → verify ToUserName equals your CorpID → dispatch a job with the payload → return 200 with an empty body or the decrypted echostr for GET verification. Do the actual work (fetching contact detail, writing rows) in the queued job, because the API call is slow and WeCom retries aggressively on timeout.

Mirror tables, not live API reads. Persist external_userid, userid (the staff member), name, type (1=WeChat user, 2=WeCom user), gender, avatar, corp_name, follow_user[], and your own synced_at. Detail calls are rate-limited and slow; your UI should never block on them.

Tagging is the cheapest automation lever. mark_tag / add_tag are idempotent-ish and let you drive segmentation from Laravel (e.g. order_placed → add tag). Keep a wecom_tags table mapping your internal slugs to WeCom tag_ids so renames in the admin console don't break code.

Multi-tenant / multi-corp: if you operate more than one CorpID, the client must be resolved per-corp, not as a singleton. Access tokens are per-corp-per-secret and will silently overwrite each other in a shared cache key.

Testing: Http::fake() if the bundle uses Laravel's HTTP client; otherwise mock the client class in the container. Snapshot the raw JSON from a real sandbox response and fixture it — WeCom's error envelopes are inconsistent enough that hand-written fakes miss fields.

Gotchas and Tips

  • access_token is the #1 source of 40001/42001 errors. It lives 7200s and fetching a new one invalidates the old one for some secret types. Cache it in Redis with a ~7000s TTL, use a lock around the fetch (Cache::lock), and never fetch per-request. If the bundle doesn't cache, write the decorator yourself before anything else.
  • Wrong secret is the #2. External contact endpoints require the 客户联系 secret; app-level endpoints require the app secret. Cross-use gives 60011 or an empty external_userid list with code 0 — the silent empty result is the nastier failure mode. Log the raw response body on every sync, not just on exceptions.
  • Callbacks must return within 5 seconds and WeCom retries up to 3 times. Idempotency is mandatory: upsert on external_userid + ChangeType + EventTime, and keep a seen-events table or you'll get duplicate audit rows and duplicate side effects (emails, tags).
  • Signature verification failures look like decryption failures. Both msg_signature and the AES payload must match; a wrong AES key (wrong length, trailing newline from .env) produces the same opaque error. Log both computed and provided signatures during setup, then remove the log.
  • Rate limits are real and per-corp: roughly 600 req/min on most external contact endpoints, plus hard caps like 20,000 customers per staff member and ~500 per batch call. For backfills, throttle with ->delay() between job dispatches rather than a sleep() in a worker.
  • follow_user can hold multiple staff for one customer (shared customers). Don't key your table on (userid, external_userid) unless you want duplicates; key on external_userid and store the staff array.
  • The bundle is 0.0.x — treat it as unstable. No CHANGELOG entries for 0.0.2 means you cannot audit the change from the release notes alone. Pin exact versions, and before upgrading run git diff 0.0.1 0.0.2 -- src/ and look specifically at method signatures on the client and any renamed DTO properties. Budget for the possibility that the package is a thin generated wrapper (Tourze bundles often are), in which case consider calling the HTTP API directly through a small, well-tested service of your own and dropping the dependency.
  • Extension points: if the bundle dispatches Symfony-style events, bridge them into Laravel events in your service provider — that gives you a stable internal API even if the package's event names churn. Similarly, decorate rather than fork: bind your own class in front of theirs and delegate.
  • No official Laravel integration assumed. Tourze packages frequently register via Symfony Bundle classes with no service provider. If app(...) throws Target [X] is not instantiable, you're constructing it via new with a Guzzle client and a token provider — write the binding yourself in AppServiceProvider and move on.
  • Debugging recipe for any opaque failure: re-run the exact HTTP call in curl outside the app with a freshly minted token. If curl works and the app fails, it's token caching or secret selection. If curl also fails, it's IP allowlist (configure it in the admin console) or the corp not having 客户联系 enabled at all.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource