For most teams, WeCom development is not about building a new system from scratch. They face a more common and more realistic scenario: the business system already exists and has been running for years β the attendance module went live long ago, and the approval workflow is built on Activiti with complex processes such as countersign (all must approve), or-sign (any one approves), and organization-chart-based multi-level approval, and the approval process queries and interacts with attendance data along the way. The requirement now is: bring this system into WeCom, so employees can open it from the WeCom Workbench and use it immediately β no username or password needed; they land straight on their own attendance data and to-do tasks.
Under these conditions, the H5 app model is often a better fit than a Mini Program: the existing system is already an Angular + SpringBoot web application, so H5 can reuse the frontend pages and backend APIs directly, and with WeCom OAuth2 web authorization (snsapi_base) it achieves a fully silent automatic login (SSO). Deployment takes effect immediately with no review or release, and iteration cost is minimal when approval forms change frequently.
Set against the backdrop of βan existing attendance management system + a complex Activiti approval workflow,β and taking the H5 model as the main thread, this article explains systematically how to complete the WeCom-side integration without rewriting the business system. It focuses on the full chain of OAuth2 silent automatic login, the binding and mapping between WeCom accounts and system accounts, JS-SDK device capability invocation, and the WeCom-side implementation of Activiti countersign / or-sign / organization-chart approval interacting with attendance (to-do push, one-tap approval cards, organization-chart synchronization).
1. Scenario Analysis and Model Selection
1.1 Assumptions About the Existing System
This article assumes the business system looks as follows (a typical shape for internal systems in most mid-to-large enterprises):
- Attendance management: complete check-in, check-in records, make-up check-in requests, and attendance statistics already exist, with REST APIs provided by the backend
- Approval workflow engine: built on Activiti (6.x/7.x); the process definitions include:
- Countersign (all must approve): one node requires approval from multiple people (e.g., a make-up check-in must be approved by both the direct manager and HR)
- Or-sign (any one approves): at one node, any one of several people can approve (e.g., a department duty-approval group)
- Organization-chart-based approval: approvers are determined dynamically according to the applicantβs department (department head β executive in charge β HRBP)
- Attendance data interaction: the approval process reads/writes attendance data (e.g., after a make-up check-in is approved, the check-in record is corrected automatically; after annual leave is approved, the leave balance is deducted)
- Account system: the system has its own user table and role/permission model (e.g., Spring Security + JWT/Session)
- Frontend: an existing web client, an Angular single-page application (TypeScript)
There are only two core problems to solve:
- Identity: who is the person entering from WeCom? How do they map to a system account for automatic login?
- Entry and reach: how do users enter the app from the WeCom Workbench? How are approval to-do tasks proactively pushed to employeesβ WeCom?
None of the business logic (check-in rules, approval transitions) needs to be moved into WeCom at all. WeCom only plays three roles: βentry point + identity provider (IdP) + message channel.β
1.2 Why H5 Is the First Choice for This Scenario
| Dimension | H5 app (the approach in this article) | WeCom Mini Program |
|---|---|---|
| Reuse of existing web frontend | Existing Angular pages reused directly | All pages must be rewritten in WXML/WXSS |
| Reuse of existing backend APIs | Reused directly; only one OAuth login endpoint added | Reused too, but the entire frontend is redone |
| Automatic login | OAuth2 snsapi_base silent authorization, completely seamless |
wx.qyLogin silent, also seamless |
| Release and iteration | Takes effect on deployment; approval forms change anytime | Requires review and release; emergency fixes are slow |
| Complex forms / workflow pages | Web tech is flexible, well suited to form-heavy approval pages | Form-engine-style pages are expensive to build |
| Device capabilities | JS-SDK: geolocation/camera/scan (signature required) | Native API calls, marginally better experience |
| Approval business: βlow frequency, form-heavy, fast iterationβ | Excellent fit | Overweight |
Conclusion: check-in itself is high-frequency and device-heavy, and the Mini Program experience is indeed better; but under the premise of βintegrating an existing system, with complex and frequently changing approval flows, where the primary goals are low-cost launch and automatic login,β H5βs overall benefits far outweigh the small gap in experience. H5 can also invoke geolocation, camera, and scan via JS-SDK, fully covering the attendance scenario. Later in the article we provide the complete JS-SDK signature scheme and iOS/Android pitfall handling.
If check-in experience requirements increase later, a hybrid model is possible: the same self-built app is configured with both an H5 home page (approval, records, statistics) and a Mini Program (check-in); message cards route by business type, and the backend account system is fully shared.
1.3 Overall Architecture
1 | βββββββββββββββββββββββββββββββββ |
Key design principle: the WeCom userid is merely an external identity field on the systemβs user table. Attendance and Activiti candidates/assignees still use the internal system userId (or are unified with the userid β see the discussion in section 4.6). Thus WeCom is just one more login method, without intruding on the existing permission and workflow models.
The diagram places the H5 frontend, the WeCom adaptation logic, and the business backend on the same side to illustrate the call relationships. If your attendance system is deployed in an intranet isolation zone and is not directly reachable from WeCom or external phones, you need to deploy a separate public relay gateway in the DMZ (put the WeCom adaptation logic on the gateway, keep the business in the intranet, and let the two sides communicate in a controlled manner via mTLS + internal tokens). See Chapter 9, βPublic Relay Gateway Under Network Isolation.β
2. Setting Up the Development Environment
2.1 Create a Self-Built App and Obtain the Three Credentials
- Go to the WeCom Admin Console and log in with an administrator account
- App Management β Self-built β Create App; fill in the app name (e.g., βMobile Attendance & Approvalβ), logo, and visibility scope
- After creation, record three key parameters:
| Parameter | Description | Where to get it |
|---|---|---|
corpid |
Unique enterprise ID | My Enterprise β Enterprise Info β Enterprise ID |
agentid |
Unique app ID | App Management β Self-built App β AgentId |
secret |
App secret | App Management β Self-built App β Secret |
β οΈ The
secretis the most sensitive credential. Store it only on the server side; it must never appear in H5 frontend code, Git repositories, or browser requests.
2.2 Configure the App Home Page (the H5 Entry)
On the app details page, configure the H5 home URL under App Home Page:
1 | App Management β Self-built App β App Home Page β Configure Web Page |
When an employee taps the app icon in the WeCom Workbench, this URL opens inside WeComβs built-in browser. We recommend using a dedicated path for the mobile H5 app (such as /mobile/), separate from the PC admin client, to make routing split and independent layouts easier.
2.3 Configure the Trusted Domain (the Most Critical Backend Setting for H5)
In H5 mode, both the OAuth web-authorization callback domain and JS-SDK rely on the βtrusted domainβ:
1 | App Management β Self-built App β Developer Interfaces β Web Authorization & JS-SDK |
Domain requirements:
- Must be HTTPS (mandatory for OAuth authorization and JS-SDK)
- ICP filing completed (for servers in mainland China)
- The ownership verification file is served directly by the frontend static-resource service or Nginx
- One app can have multiple trusted domains (the domain registrant must be consistent), and the callback URL must live under one of these domains
Also configure the Enterprise Trusted IPs: the egress IP of the server calling the server-side APIs must be whitelisted, otherwise endpoints such as gettoken return 60020 not allow to access from your ip.
2.4 Configure Message Receiving (Callback, for Card-Button Approval)
To support βtap Approve/Reject directly on the message cardβ (without opening a page), configure a callback:
1 | App Management β Self-built App β Receive Messages β Set API Receiving |
If you only need to-do jumps without in-card interaction, this can wait, but we recommend configuring it from the start (it is used in Chapter 7).
2.5 Local Development Environment
The core difficulty of local H5 development: the OAuth callback and JS-SDK require a trusted domain + HTTPS, while locally you have http://localhost. Two common approaches exist.
Option 1: Intranet penetration (recommended; closest to the real environment)
1 | # Use frp or ngrok to map local 8080 / frontend port to a sub-path of the filed domain |
Add the penetration domain to the admin consoleβs trusted domains (during development) and place the verification file in your local static directory to pass verification.
Option 2: hosts + mkcert (no public network needed; good for pure page joint debugging)
1 | mkcert -install |
Note: the hosts approach only fools the browserβs certificate check. WeComβs OAuth authorization still goes to the real WeCom servers and then redirects back, and during real-device debugging the phone cannot use your computerβs hosts. So real-device debugging must use an intranet-penetration domain.
Starting the backend locally:
1 | cd ~/work/code/attendance-backend |
3. Integrating the H5 Frontend Project
3.1 Directory Structure (Reuse the Existing Angular Project; Add a Mobile Module)
No new project is needed. In the existing Angular + TypeScript project, add a lazy-loaded mobile module (feature module / routes) and a WeCom adaptation layer:
1 | attendance-web/ |
3.2 Import the WeCom JS-SDK
WeCom H5 uses the jweixin module (it shares its origin with the WeChat Official Account JSSDK; WeCom extends it with wx.agentConfig and enterprise-specific APIs):
1 | npm install weixin-js-sdk --save |
1 | // src/app/wecom/env.service.ts |
3.3 Routing and the Silent-Login Guard
All mobile business routes sit behind one CanActivate guard: without a system token it initiates the OAuth silent login, then returns to the original page after success. This is the master switch for βopen the app and youβre logged in automaticallyβ; Chapter 4 covers it in detail.
1 | // src/app/mobile/mobile.routes.ts |
1 | // src/app/mobile/guards/wecom-auth.guard.ts |
Lazily load the whole mobile module under the mobile path in the root routes:
1 | // src/app/app.routes.ts |
4. The Complete OAuth2 Silent Automatic Login (SSO) Chain
This is the heart of the whole integration. Target behavior: an employee taps the app icon in WeCom (or taps an approval message card), and while the page opens there is no login page and no confirmation button at all; after a second or two they land directly on the business page, and the backend already knows βwhich person in the system this is.β
4.1 Choosing the Authorization Mode: snsapi_base
WeCom web authorization supports two scopes:
| scope | Confirmation prompt | What you get | When to use |
|---|---|---|---|
snsapi_base |
Silent, no popup whatsoever | Only the memberβs userid (exchanged by the backend) | Automatic login for internal enterprise apps β used in this article |
snsapi_privateinfo |
Requires manual user confirmation | userid + sensitive info (phone/email, etc., requiring member authorization) | The rare scenario where extra privacy fields must be collected |
For an internal self-built app whose visibility scope already covers the users, snsapi_base is completely silent inside the WeCom client β this is precisely the basis for automatic login. We donβt need phone numbers or emails at this step (they can be queried by userid through the server-side Contacts API), so we always use snsapi_base.
4.2 End-to-End Sequence
1 | WeCom client H5 frontend (WebView) Business backend WeCom server |
Two key points:
- The code is exchanged only on the backend: the frontend never calls WeCom APIs directly (that would expose the secret). The frontend is only responsible for βguiding the redirectβ and βhanding the code on the redirect URL to the backend.β
- The authorization URL can be built on either the frontend or the backend, but the
state-based CSRF protection and the βreturn to the original page after loginβ logic must be handled by you.
4.3 Step 1: Build the Authorization URL and Redirect
Authorization URL format:
1 | https://open.weixin.qq.com/connect/oauth2/authorize |
| Parameter | Description |
|---|---|
appid |
The enterprise corpid (note: although it is called appid, you fill in the corpid) |
redirect_uri |
The redirect address after authorization; URL-encoded; must be under a trusted domain |
response_type |
Fixed to code |
scope |
snsapi_base |
agentid |
The self-built appβs agentid (required; on some versions the app identity cannot be obtained without it) |
state |
Custom parameter, returned by WeCom unchanged; used for CSRF protection + carrying the post-login target path |
#wechat_redirect |
Fixed suffix; must end the URL as a hash |
The frontend wraps this in an injectable WecomOAuthService (src/app/wecom/oauth.service.ts):
1 | import { Injectable, inject } from '@angular/core'; |
The route guard only needs to call hasToken() to check, and redirectToWecomAuth() if not logged in (see WecomAuthGuard in 3.3).
corpid and agentid are βpublic identifiersβ (the authorization URL appears in plaintext in the browser anyway), so keeping them in the frontend is fine. The only real secret is
secret, which always stays on the server.
4.4 Step 2: The Callback Landing Page Exchanges the Code for a Token
After redirecting back to /mobile/oauth/callback?code=xxx&state=yyy, the callback page does three things: verify state β send the code to the backend β jump back to the original target page after receiving the JWT.
1 | // src/app/mobile/pages/oauth/oauth-callback.component.ts |
1 | // src/app/core/services/auth.service.ts |
4.5 Step 3: The Backend Exchanges the Code for a userid (Core of Authentication)
After receiving the code, the backend must first obtain an access_token, then call two endpoints:
auth/getuserinfo: code β userid (internal enterprise member) or openid (non-member / external contact)- After obtaining the userid, if needed use
user/get(Contacts) to fill in name, department, and mobile number
Endpoint 1: obtain the access credential
1 | GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=CORPID&corpsecret=SECRET |
Returns an access_token (valid for 7200 seconds). The access_token must be centrally managed (Redis cache + distributed lock; see Chapter 8); neither the frontend nor other services fetch it themselves.
Endpoint 2: exchange code for userid
1 | GET https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=TOKEN&code=CODE |
For an internal enterprise member it returns:
1 | { |
If it returns an
openidwithout auserid, the current user is not within the enterprise appβs visibility scope (possibly an external contact). You should reject the login and prompt them to contact an administrator for access, rather than auto-creating an account.
Login Controller:
1 | /** |
1 | /** |
4.6 Step 4: Bind WeCom Accounts to System Accounts (The Most Critical Design for an Existing System)
This is the biggest difference between βan existing business systemβ and βbuilding a system from scratchβ: the system already has a set of accounts (users may log in with employee number, email, or domain account), and everything that arrives from WeCom is a single userid. You cannot simply βcreate a new user from the useridβ β otherwise the same person becomes two accounts, and attendance records and Activiti to-dos all fail to line up.
Three binding strategies are recommended; choose according to the enterpriseβs reality:
Strategy A: Employee number/account is consistent β automatic binding (most recommended, zero ops)
The βaccountβ field in the WeCom Contacts is usually the enterpriseβs unified employee number, and the WeCom userid often uses the employee number too. Agree that userid = system username (or employee number), and associate directly by account at login:
1 | /** |
Strategy B: Self-service binding (when account systems are not unified)
If automatic matching fails on first login, have the user enter their system account password once to complete binding; afterward the mapping between wecom_user_id and user_id is persisted and login is permanently silent:
1 | First WeCom login β backend finds no mapping β returns NEED_BIND state |
The binding is established only once; credentials are discarded immediately after verification, and no plaintext password is stored.
Strategy C: Admin pre-binding / Contacts sync
Use the Contacts API (user/list) to batch-sync by department, aligning WeCom userids with system accounts by employee number (the sync approach is in Chapter 8). Suitable for a one-time initialization before launch.
User table change (add a column to the existing user table without touching its existing structure):
1 | ALTER TABLE sys_user ADD COLUMN wecom_user_id VARCHAR(64); |
Design point: the internal userId stays unchanged. Attendance-record foreign keys, Activitiβs
ACT_RU_TASK.ASSIGNEE_, and candidate groups all continue to use the internal system userId (username). The WeCom userid is used only for βrecognizing the person at loginβ and βaddressing them at push time,β decoupled through thesys_user.wecom_user_idmapping layer. This neither pollutes workflow definitions nor removes the ability to coexist with PC username/password and other SSO login methods.
4.7 Step 5: Seamlessly Connect JWT with the Existing Authentication System
After silent login obtains the userid, subsequent requests work exactly as on PC: they all go through the systemβs existing JWT/Session authentication. Thus attendance and approval APIs need zero changes.
The frontend uses an Angular HttpInterceptor to inject the token uniformly and re-run silent login on 401:
1 | // src/app/core/interceptors/auth.interceptor.ts |
Register it in app.config.ts (functional interceptors, Angular 15+):
1 | // src/app/app.config.ts |
The silent-login endpoint
/api/auth/wecom/logincarries no token itself; the interceptor passes requests through unchanged when there is no token in local storage, so no special check is needed β re-login is triggered only on 401.
The backend keeps the existing Spring Security configuration (SecurityFilterChain Bean style) and only permits the WeCom login endpoint and callback endpoint:
1 | /** |
If your project still uses Spring Security 5.xβs
WebSecurityConfigurerAdapter, the equivalent is to overrideconfigure(HttpSecurity), callpermitAll()on the same two paths andcsrf().disable(); the JWT issued by silent login is verified uniformly by the existing JWT filter, fully shared with username/password login.
At this point the chain βopen the app β automatic login β see your own attendance and to-dos directlyβ is fully connected, and none of the existing attendance and Activiti APIs, permissions, or data changed by a single line.
5. JS-SDK: Geolocation, Camera, and Scan in H5
Attendance cannot do without geolocation, camera, and scan. Unlike a Mini Program, H5 cannot call native APIs directly; it must go through the WeCom JS-SDK after signature-based authorization. This chapter gives a signature scheme that can be put into practice directly, focusing on the iOS/Android signature-URL difference that trips people up most often.
5.1 wx.config and wx.agentConfig
The WeCom JS-SDK has two layers of configuration, which beginners most often confuse:
| Configuration | Purpose | Signature ticket |
|---|---|---|
wx.config |
Inject basic configuration; invoke common capabilities (sharing, getLocation, scanQRCode, choose image, and most other interfaces) |
Signed with jsapi_ticket |
wx.agentConfig |
Inject the current self-built app identity; invoke WeCom-specific interfaces (e.g., selectEnterpriseContact contact picker, some approval-related interfaces) |
Signed with get_jsapi_ticket (enterprise app ticket) |
For check-in geolocation/camera/scan, passing wx.config is enough; only enterprise-specific capabilities such as the βselect approver/CC recipient by organization chartβ contact picker require agentConfig in addition.
5.2 Backend: jsapi_ticket Management and Signing
The jsapi_ticket is exchanged with an access_token, is valid for 7200 seconds, and likewise needs centralized caching:
1 | GET https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=TOKEN |
The ticket endpoint for enterprise-app agentConfig is ticket/get?type=agent_config.
Signature algorithm (as specified by WeCom):
1 | string1 = jsapi_ticket={ticket}&noncestr={nonce}×tamp={timestamp}&url={current page URL} |
1 | /** |
1 |
|
5.3 Frontend: Signature Initialization (Handling the iOS Entry-Page Problem)
The most classic JS-SDK pitfall: Android signs with the current page URL, while iOS (WKWebView) signs with the URL of the entry page when the app was first opened. In an SPA, client-side route changes never truly refresh the page; on iOS, if you sign with βthe current routeβs href,β then as long as it isnβt the first landing page, wx.config will inevitably report invalid signature.
The unified solution: record the first URL on the entry page and use it for all subsequent signatures (iOS); Android always uses the current URL.
1 | // src/app/wecom/jssdk.service.ts |
At app startup (before the first route navigation), record the iOS entry URL as early as possible; APP_INITIALIZER works well:
1 | // Register startup initialization in src/app/app.config.ts |
The key point is that on iOS the entry URL must be captured from
location.hrefbefore any client-side route navigation happens. Putting it inAPP_INITIALIZER(which runs before Angular routing starts) is the most reliable approach; even if you donβt pre-warm the signature, at minimum write the entry URL to sessionStorage in this hook.
Routing-mode recommendation: to reduce the mental burden of hashes and signatures, the mobile H5 app can use history mode; if you use hash mode, be sure to truncate at
#viasignableUrlabove, ensuring the URLs signed by frontend and backend match exactly β both usingencodeURIComponent, or neither, consistently.
5.4 Geolocation Check-in
1 | // src/app/wecom/device.service.ts |
Called by the check-in component (checkin.component.ts); use the teamβs existing UI library for prompts (e.g. NG-ZORROβs NzMessageService):
1 | // src/app/mobile/pages/checkin/checkin.component.ts (excerpt) |
The backend check-in API is identical to the systemβs existing implementation (secondary distance validation, duplicate check-in prevention, persistence, push notifications). This logic already exists; H5 is just a new caller. The backend must re-validate the distance and must not trust the latitude/longitude sent by the frontend (frontend coordinates can be tampered with via packet capture).
5.5 Photo Check-in and QR Code Check-in
Photo and QR code scanning are already wrapped in the WecomDeviceService from 5.4 (takePhoto() returns a localId, scanQRCode() returns the QR code content); the component can simply await them. The localId image from takePhoto needs an additional upload:
wx.uploadImagefirst uploads the image to WeCom to obtain aserverId; the backend then calls WeComβs media APImedia/getto pull it back into the intranet β suitable when you donβt want to upload files directly from H5;- Or draw the localId onto a canvas to convert it to a Blob, then POST it directly to the existing file service using Angularβs
FormData+HttpClient, reusing the systemβs existing attachment storage.
With either approach, the backend reuses the existing photo storage and watermark (time + location + device info) logic. For QR code check-in, the backend validates the QR code tokenβs validity and expiry time, and layers location validation on top as a double check β again reusing the existing API.
6. Implementing Complex Activiti Approval Flows on the WeCom Client
Attendance-related approvals (make-up check-in, leave, off-site work, overtime appeal, etc.) are already defined and running in Activiti. The WeCom client does not need to re-implement the processes β it only needs to do three things: bring the to-do tasks out, wire the approval actions in, and actively push to-do tasks to WeCom. This chapter explains how to reuse the engine with three typical node types: countersign, or-sign, and organization-chart-based approval.
6.1 Unify the Assignee Identity First
Activiti identifies a task assignee (ACT_RU_TASK.ASSIGNEE_) or candidate users/groups (ACT_RU_IDENTITYLINK) with a string. Make sure that: the assignee identifier hard-coded in the process definition or computed at runtime matches sys_user.username (the internal account, i.e. the unique key bound to wecom_user_id).
We recommend using the employee number / username (e.g. zhangsan) as the unique person identifier across the whole system:
- Activiti assignee / candidateUser =
sys_user.username - WeCom mapping =
sys_user.wecom_user_id(in many companies this is also the employee number, so the two may be identical, but they are logically separate) - When pushing WeCom messages:
username β look up sys_user β take wecom_user_idastouser
This way Activiti process definitions, UEL expressions, and candidate queries need no changes whatsoever for WeCom.
6.2 Expressing the Three Typical Node Types in Process Definitions
Using the βmake-up check-in applicationβ process as an example, here is how countersign, or-sign, and organization-chart-based approval are written in BPMN.
Countersign (passes only when all members approve) β use a multi-instance node (multiInstanceLoopCharacteristics) plus a completion condition:
1 | <userTask id="countersignLeaderHr" name="Direct leader and HR countersign"> |
isSequential="false": parallel countersign β a task is generated for every person at the same timenrOfInstances: total number of countersigners;approveResultList: a process variable collecting each personβs decision- Completion condition: proceed only when everyone has handled it and there is no REJECT
Or-sign (any one of several people can handle it) β also a multi-instance node, but with the completion condition changed to βfinish as soon as 1 is handled.β The more common approach is candidate users (candidateUsers): one task visible to multiple people, and whoever claims it handles it:
1 | <userTask id="orSignDuty" name="Duty team or-sign" activiti:candidateUsers="${dutyGroupUsers}"> |
Or use multi-instance + nrOfCompletedInstances >= 1 to give each person a separate to-do task, with the rest automatically canceled after one person handles it.
Dynamic approval by organization chart β the assignee is not hard-coded; it is computed in real time from the org chart by a process expression (applicant β direct department head β managing director):
1 | <userTask id="deptLeaderApprove" name="Department head approval" |
orgService is a Spring Bean registered in the Activiti expression context; internally it walks up the department tree to find the person in charge. After a department head transfers positions, new process instances are automatically routed according to the latest org chart β no process definition changes needed.
This BPMN already runs on the PC client. The WeCom client merely adds a new βhandling entry pointβ; underneath, the approval action still calls the same
taskService.complete(), so countersign counting, or-sign claiming, organization routing, and gateway conditions are all guaranteed consistent by the engine. There is no problem of βthe process on PC differs from the process on mobile.β
6.3 To-do List and Details on the WeCom Client
To-do list β directly use Activitiβs TaskQuery to query to-do tasks by the current logged-in userβs username (with countersign each person has their own task; or-sign candidate tasks are queried with taskCandidateUser):
1 | /** |
Approval details β display the form, countersign progress (who has approved, who is pending), and the approval-comment timeline:
1 | /** Countersign progress: aggregate each handler's status from historic tasks + current tasks */ |
The frontend ApprovalDetailComponent renders based on nodeType: countersign shows a multi-avatar progress bar (handled / pending); or-sign shows βAny member of the duty team can approve; tap to claim and handle.β
6.4 Claiming (Or-sign) and Approval Actions
An or-sign candidate task must first be claimed so the user becomes the assignee before it can be handled; countersign tasks are directly assigned and skip the claim step.
1 |
|
The rejection strategy can follow company rules: reject back to the initiator (resubmit), reject to the previous node, or end the process outright. The make-up check-in scenario commonly uses βany rejection terminates + notify the initiatorβ, which is exactly the semantics of !contains('REJECT') in the countersign completion condition.
6.5 Linking Approval with Attendance Data (Reusing Existing Capabilities)
When the process ends, attendance data is written back according to the business type. This logic already exists in the system, and WeCom-side approvals trigger the same taskService.complete(), so the linkage works out of the box. Typical handling for make-up check-in:
1 | public void afterProcessFinished(String processInstanceId) { |
Listening for the Activiti process-completed event is more robust than manually calling this from every approval API (completion from any entry point β PC, WeCom, or scheduled task β will reach it):
1 | import org.activiti.engine.delegate.event.ActivitiEntityEvent; |
6.6 Actively Pushing To-do Tasks to WeCom
An H5 to-do list alone is not enough β employees will not open it proactively to refresh. When process flow generates a new to-do task, the backend should actively push an βapproval cardβ to the next handlerβs WeCom. Tapping the card opens the corresponding H5 approval detail page directly, and thanks to the silent login from chapter 4, the user is already logged in when it opens.
Trigger the push in a task-creation listener (Activiti event listener):
1 | import org.activiti.engine.delegate.event.ActivitiEntityEvent; |
A text card message (tap to jump straight to the H5 approval page):
1 | { |
Key point: the card URL points directly to the approval detail page. The employee taps it β no token β chapter 4βs OAuth silent login β after the callback, the redirect-back path carried in state (see redirectPath in 4.3) returns to this approval detail. To achieve this, simply include the login parameters WeCom expects in the message card link, or have the frontend guard enforce login for all /mobile/** routes β no special handling is required.
Template card button callbacks (advanced: approve/reject without opening the page)
If you want approvers to tap βApprove/Rejectβ directly in the message notification, use template_card (button_interaction) plus the callback reception from chapter 6. After the backend receives the button event, it calls mobileApprovalService.approve() directly and then updates the card status. This suits nodes where the approval action is extremely simple (one-tap approve); for cases involving comments or viewing countersign details, jumping to H5 is still recommended. Both approaches call the exact same approval method underneath.
6.7 Organization Sync: Ensuring Dynamic Approvers Can Be Reached
The handler dynamically computed by βorganization-chart-based approvalβ is a username; when pushing, its wecom_user_id must be findable. There are two ways to guarantee this:
- Incremental contact-book callback sync (recommended, real-time): subscribe to
change_contactevents (member create/update/delete, department changes) and updatesys_userβs wecom_user_id and department membership in real time. - Scheduled full sync: call the contact-book department/member APIs for a full alignment once every night as a safety net.
1 | GET /cgi-bin/department/list?id=0 # department tree |
Align by employee number (username) during sync, write the WeCom userid back to sys_user.wecom_user_id, and sync department relationships for use by orgService.findLeader() org routing and push addressing. The contact-book read APIs have daily call limits (see 9.4), so be sure to use βincremental callbacks as primary + one daily full sync as fallbackβ β do not poll at high frequency.
7. Message Push and Event Callbacks
7.1 access_token and Sending Messages
App messages are sent uniformly by the server, via:
1 | POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=TOKEN |
Common message types:
text: plain text such as attendance reminderstextcard: title + description + button, tap to jump to H5 (first choice for approval to-dos)template_card: interactive buttons allowing direct action within the notification (paired with callbacks)markdown: rich text such as approval summaries (supported inside WeCom)
Push service wrapper (duplicate_check_interval prevents duplicate pushes within a short window):
1 |
|
The
invaliduser/invalidpartyfields in the response body must be logged: they indicate that someone in the push target has not bound the app or is outside the visible scope, and are the first clue when troubleshooting βwhy doesnβt someone receive to-do notificationsβ.
7.2 Callback Signature Verification and Encryption/Decryption
After βreceive messagesβ is configured, WeCom sends two kinds of requests to the callback URL:
- GET: URL validity verification when saving the configuration; decrypt the
echostrand return it verbatim - POST: formal event pushes (template card buttons, contact-book changes) as encrypted XML; verify the signature and AES-decrypt
1 |
|
Do not implement the crypto yourself β use the official aes-256 sample code package (WeCom officially provides the Java WXBizMsgCrypt), which encapsulates: SHA1 signature verification, AES-256-CBC decryption, corpId verification, and XML assembly. The three parameters Token, EncodingAESKey, and corpid come from the backend callback configuration.
7.3 Handling Template Card Buttons and Contact-Book Events
1 |
|
Event handling must be idempotent: WeCom may re-push the same event on timeout. approve internally checks βtask already ended / already handledβ (6.4 queries for an active task), so duplicate pushes never produce a second approval. Time-consuming operations (e.g. sending multiple messages, writing to multiple tables) go to an async thread or message queue to guarantee the callback returns success within seconds.
8. Server-Side Infrastructure
8.1 Centralized Management of access_token / jsapi_ticket
Both tickets are valid for 7200 seconds and unique per corp per application (repeated acquisition invalidates the old one), so they must be cached centrally on the server. In multi-instance deployments, use a distributed lock to ensure only one instance refreshes:
1 |
|
Cache jsapi_ticket and the agent_config ticket independently using the exact same pattern (with separate cache keys).
8.2 Separating Sensitive Configuration
corpid/agentid may be public, but the secret, callback Token, and EncodingAESKey must be injected via environment variables or a config center and must never enter Git:
1 | # application-prod.yml |
8.3 API Security
- Let the OAuth login endpoint and WeCom callback endpoint through; everything else goes through the existing JWT authentication
- One-time random
statestring + sessionStorage validation to prevent CSRF - A code can only be used once and is valid for 5 minutes; the backend exchanges it immediately on receipt and never caches it
- The backend re-validates the distance from check-in coordinates and does not trust the frontend; photos get watermarks; QR scanning layers on location
- Callback endpoints verify the signature + AES-decrypt + verify corpId, rejecting forged events
- Rate-limit critical APIs (Redis sliding window) against abuse
8.4 Contact-Book Sync Service
1 |
|
9. Public Relay Gateway Under Network Isolation
The previous chapters assume the backend services can be accessed directly by the WeCom cloud and by usersβ phones. But many companies deploy their attendance system in an intranet isolation zone: no public IP, no inbound access allowed, and sometimes the servers themselves cannot directly reach the public internet. WeComβs servers are on the public internet, and mobile clients are outside the corporate intranet (off-site 4G/5G), so neither can reach the system directly. In this situation you deploy a dedicated public relay gateway in the DMZ (demilitarized zone): reachable by WeCom and mobile phones on one side, and able to reach the intranet attendance system through a controlled channel on the other.
9.1 Current Network State and Goals
Typical current state:
- The intranet attendance system (SpringBoot + PostgreSQL + Redis + Activiti) is open only to the office network, at an address like
http://10.10.20.30:8080 - The perimeter firewall denies all public inbound traffic by default
- When phones connect to WeCom (especially off-site), traffic travels over the public internet and cannot be routed to
10.xintranet addresses - WeComβs OAuth callback, JS-SDK trusted domain, and event callback all require a publicly reachable, ICP-filed HTTPS domain
Goals:
- H5 pages on usersβ phones load normally, complete silent login, and call attendance and approval APIs
- The WeCom cloud can deliver OAuth authorization results and message card event callbacks
- The intranet system is not directly exposed to the internet; the database, Activiti, and business logic stay safely inside the intranet
- If the public-facing side is compromised, the blast radius is limited to the gateway; attackers cannot directly reach business databases or move laterally inside the intranet
9.2 Two Connectivity Models
| Model | Connection direction | Prerequisites | Characteristics |
|---|---|---|---|
| Model 1: DMZ gateway + firewall whitelist reverse proxy | DMZ gateway β intranet (perimeter firewall opens specified ports) | The firewall supports a restricted βDMZ β intranetβ access policy | Most common, short path, good performance, clear auditing; the main recommendation of this article |
| Model 2: intranet-initiated reverse tunnel | Intranet β DMZ/public actively establishes a tunnel (frp/WireGuard) | The intranet allows no inbound traffic at all, only outbound | No inbound policy needed at the perimeter, strong penetration; operations and auditing are more complex |
The vast majority of companies choose Model 1: place a gateway server in the DMZ, and have the perimeter firewall open only one whitelist rule, βgateway IP β intranet attendance service IP:portβ. If security policy is so strict that even the DMZ cannot actively connect to the intranet, use Model 2, where the intranet dials out actively (see 9.7).
9.3 Recommended Architecture: the Gateway as a βWeCom Adapter Layerβ
Key design principle: the public gateway is not just an Nginx forwarder β it is a WeCom-facing adapter layer (BFF). All communication with the WeCom cloud converges at the gateway, and the intranet system is completely unaware of the WeCom protocol.
1 | WeCom cloud WeCom mobile app (public/4G) |
Responsibility split (very important):
| Capability | Public gateway | Intranet system |
|---|---|---|
| Holds corpid/secret/EncodingAESKey | Yes | No |
| Calls WeCom cloud (gettoken, getuserinfo, get_jsapi_ticket, message/send) | Yes | No |
| OAuth callback landing, callback message verification/decryption | Yes | No |
| H5 static resource hosting (or CDN/OSS) | Yes | No |
| Account binding mapping (wecom_user_id <-> internal account) | No | Yes |
| Issue/validate business JWT, attendance, Activiti, org chart | No | Yes |
| PostgreSQL/Redis business data | No | Yes |
| Ordinary business APIs (/api/β¦) passthrough | reverse proxy only | handles them |
This way, even if the gateway is compromised, the attacker cannot obtain business database data, and the gateway holds no long-lived intranet credentials (internal tokens are short-lived and revocable).
9.4 Silent Login Flow Under Network Isolation (Differences from Chapter 4)
Chapter 4 assumes same-origin frontend/backend and that the backend can call WeCom directly. With the gateway added, βcode for useridβ happens at the gateway, while βuserid for internal account, sign JWTβ happens on the intranet β adding one extra hop of internal trusted invocation in between:
1 | Mobile H5 DMZ gateway Intranet system WeCom cloud |
Key points:
- The secret lives only on the gateway; the intranet system does not need and should not configure WeCom credentials
- The intranet adds only one internal trusted endpoint
/internal/wecom/assert: input is a userid, output is the systemβs own JWT. It is not exposed to the internet and accepts only calls from the gateway carrying an internal token / mTLS - For business APIs
/api/**, the gateway only reverse-proxies and passes through the JWT; authentication still happens in the intranetβs Spring Security (see 4.7), and the gateway does not parse business payloads
Gateway side: exchange code for userid, then for internal JWT
1 | /** |
Do not keep the internal JWT in the URL long-term (it would enter gateway/Nginx logs and browser history). The one-time
/oauth-bridge.htmlabove works like this: the page script reads the token from the hash (the hash is never sent to the server and leaves no server logs), writes it to localStorage, immediately clears it withhistory.replaceState, and then jumps to the target page.statestill performs CSRF validation and original-path restoration as in 4.3.
Intranet side: an internal trusted endpoint callable only by the gateway
1 | /** |
Configure /internal/** separately in Spring Security: allow only requests from the gateway IP (or carrying an mTLS client certificate), and never include it in the public Nginx reverse-proxy locations, ensuring at both the network and application layers that it cannot be called directly from outside.
9.5 Internal Trust Between Gateway and Intranet (the Security Core)
The DMZ-to-intranet hop is the highest-security part of the whole solution; it must provide βencrypted channel + authentication + least privilegeβ:
- Network-layer whitelist: the perimeter firewall opens only
gateway IP:random source port β intranet attendance IP:8080/tcp; all other intranet ports and hosts are unreachable. Gateway access to the database (5432) and Redis (6379) is never opened - Transport encryption mTLS: HTTPS mutual certificate authentication between gateway and intranet; the intranet trusts only the gatewayβs client certificate. Even sniffing on the same network segment cannot forge or replay traffic
- Application-layer internal token: in addition to mTLS, add a short-lived
X-Internal-Token(injected by the gateway, verified by the intranet) as a double safeguard; keep the token in environment variables and rotate it regularly - Minimal endpoints: the intranet exposes only a very small number of endpoints such as
/internal/wecom/assert(exchange JWT),/internal/message/send(proxy-send to-dos), and/internal/callback/event(deliver callback events), with strict whitelist validation of inputs - Anti-replay: internal requests carry a timestamp + nonce; the intranet validates the time window (e.g. Β±5 minutes) and nonce uniqueness
- The gateway stores no business data: it does not connect to the business PostgreSQL; access_token and such use the gatewayβs own Redis or local cache; logs are masked and never record JWT plaintext
Internal token verification example:
1 |
|
9.6 Cross-Zone Handling of Event Callbacks and Message Push
Under network isolation, WeCom event callbacks (card buttons, contact-book changes) can only hit the public gateway first, and the gateway then delivers them to the intranet; approval to-do notifications generated on the intranet go in reverse, proxied through the gateway.
Inbound: WeCom event callback β gateway verifies and decrypts β deliver to intranet
1 | /** |
The intranet receives an already-verified plaintext event and directly reuses the WecomCallbackService dispatch logic from chapter 7 (card button β approvalService.approve(), contact-book change β incremental sync). Note that intranet handling must be idempotent, because gateway forwarding may retry.
Outbound: intranet to-do β gateway proxy-sends the WeCom message
The intranet does not hold the secret and may not directly reach the public internet, so Activitiβs to-do push listener (see 6.6) no longer calls WeCom directly; instead it submits βwhom to send to, what cardβ to the gateway, which proxy-sends it:
1 | /** |
1 | /** |
This creates a clean one-way division of responsibility: all WeCom-related secrets and cloud calls converge on the gateway; the intranet only produces and handles business, sending and receiving messages through a narrow interface.
9.7 Alternative Model: Intranet-Initiated Reverse Tunnel
If security policy does not allow the DMZ to actively connect to the intranet (any DMZβintranet inbound is forbidden), you can instead have the intranet actively establish a long-lived tunnel to the DMZ/public gateway β the intranet dials out, reusing the already-permitted outbound policy:
- WireGuard / IPsec tunnel: establish an encrypted point-to-point network between the gateway and a tunnel machine on the intranet; the intranet actively dials in. To the gateway, the intranet service appears as the tunnel peer address, and application-layer authentication from 9.3 still applies. Mature operations and good performance β preferred
- Reverse proxies such as frp / rathole: the intranet client
frpcactively connects to the publicfrps, mapping intranet8080to a local port on the gateway. Fast to set up, but strictly limit exposed ports and protocols and add mTLS/tokens so the tunnel does not become a βpublic internet straight into the intranetβ backdoor - Message queue / polling relay: when security requirements are extremely high, the intranet only actively consumes a queue on the gateway side (e.g. pull pending callbacks, push back proxy-send results) β all traffic is intranet outbound with no reverse inbound. Latency is slightly higher, but the attack surface is minimal
Selection principle: if βDMZ + firewall whitelistβ works, donβt use a tunnel; when a tunnel is necessary, prefer a network-layer solution like WireGuard plus application-layer authentication; never use bare frp to map intranet management ports directly to the public internet.
9.8 Key Points of the Nginx Gateway Reverse-Proxy Configuration
The gatewayβs Nginx handles TLS termination, H5 static resources, and reverse-proxying /api/** to the intranet (via the tunnel peer address or a firewall-reachable address). Note that /internal/** must never be exposed here:
1 | server { |
When building the frontend, point the API base at the public gatewayβs same-origin path (e.g.
/api) and let Nginx forward it to the intranet; both the JS-SDK signature domain and the OAuth callback domain use the gatewayβs public ICP-filed domain. The intranet system needs no public domain or certificate at all.
9.9 Hardening the Public Gateway Itself
A DMZ host is the exposed surface and must be hardened by the least-privilege principle:
- Open only 443 (plus necessary SSH restricted to source IPs + key login), close all other ports; put a cloud WAF / security group in front
- Run the gateway process as non-root with minimal privileges; for container deployments use a read-only root filesystem and drop capabilities
- The gateway persists no business data and connects to no business database; forward logs centrally and do not keep sensitive information on disk long-term
- Secrets, internal tokens, and mTLS private keys all go through environment variables/KMS, never into images or Git (see 8.2)
- Add the gatewayβs outbound IP to WeComβs βenterprise trusted IPβ whitelist (see the rate-limit and whitelist section in 10.4)
- Apply rate limiting, anti-replay, and request-body size limits uniformly at the gateway layer; alert on abnormal calls
- Audit calls to internal endpoints between gateway and intranet (who, when, which internal endpoint, what userid)
10. Pitfall Guide
10.1 OAuth Silent Login
- The app home page / callback domain must be under the βtrusted domainβ, otherwise the authorization page reports
redirect_uri parameter error. - The authorization link must include
agentid, otherwise under some WeCom versionsgetuserinfocannot obtain the application identity. appidis the corpid, not the agentid β a common beginner mistake is swapping them.- Returns openid instead of userid: the user is outside the appβs visible scope. Check whether the appβs βvisible scopeβ includes the memberβs department; do not silently create accounts in code.
- Opening the link in a PC browser does not trigger silent authorization:
snsapi_baseis seamless only inside the WeCom client. The frontend must check the UA first; non-WeCom environments go through the systemβs username/password login. - The code can be used only once and expires in 5 minutes: refreshing the redirect-back page causes a code-reuse error. After successful login, the app should use
router.replaceto clear the code from the URL, preventing refresh replay.
10.2 JS-SDK Signatures
- Sign with the entry-page URL on iOS and the current-page URL on Android (see 5.3); under SPA this is the number-one cause of
invalid signature. Record the entry URL before the first route jump. - The signed URL must match
location.hrefcharacter by character: protocol, domain, port, and query must all be included; handle the hash consistently per the rules (history mode is recommended to avoid it). - If the frontend encodes, the backend encodes; if neither encodes, neither does; the signature string concatenation order must be
jsapi_ticket&noncestr×tamp&url. - Calling WeCom-proprietary APIs requires
beta: trueinwx.config, plus anotherwx.agentConfig. - Real-device local debugging must use an https domain via intranet penetration; the hosts approach does not work on phones.
10.3 Activiti and Account Mapping
- Unify the assignee identifier to the internal username; do not write wecom_user_id directly into the BPMN assignee, otherwise switching identity sources later (DingTalk/Feishu in the future) would require changing every process definition.
- Do not create duplicate accounts by wecom_user_id: the number-one principle of an existing system is binding/mapping (4.6), otherwise attendance and historical to-dos split into two people.
- Or-sign candidate tasks must be claimed before handling; completing without claiming reports that the task does not belong to the current user.
- A countersign rejection must end remaining instances early: use a completionCondition containing a REJECT check + delete remaining tasks in a listener, otherwise others still receive to-dos after a rejection.
- Put attendance linkage in a process-completion listener, not in a specific approval-button API, so it takes effect from any entry point (PC, H5, card callback), and attendance is not mistakenly modified unless approval genuinely passes.
10.4 WeCom API Rate Limits and Others
| API | Limit (for reference; official docs prevail) |
|---|---|
| gettoken | Calls per corp within 5 minutes are limited; must be cached |
| Send messages | Per-app per-minute cap; batch and dedupe touser where possible |
| Contact-book reads | Daily total call cap; rely mainly on incremental callbacks |
| Message card updates | Subject to API rate limits; avoid cyclic updates |
Other common issues:
- The serverβs outbound IP must be added to the βenterprise trusted IPβ whitelist, otherwise error
60020is returned. - HTTPS + ICP filing are mandatory (mainland China servers); an expired certificate makes the entire app fail to open with no obvious prompt β include it in monitoring.
- Callbacks must return
successwithin seconds, with business handling asynchronous; otherwise WeCom re-pushes and causes duplicate approvals (covered by idempotency as a safety net). - The textcard URL should land directly on the detail page, combined with silent login + state redirect-back, to achieve βtap notification straight to approvalβ.
- If the secret leaks, reset it in the admin console immediately and restart services; in code review, list βsecret appearing in frontend/logsβ as a red line.
10.5 Network Isolation and Relay Gateway
- Never expose the intranet system directly to the internet: place only the gateway in the DMZ; the perimeter firewall opens only the single whitelist βgateway IP β intranet attendance service IP:portβ; gateway access to database/Redis ports is never opened.
- Place the secret and the business database on separate sides: the WeCom secret and EncodingAESKey live only on the gateway; account binding, JWT, and business data live only on the intranet. Neither side should both hold the keys and connect to the business database.
- Internal endpoints
/internal/**must be doubly protected: mTLS client certificate + internal token (short-lived, rotatable, constant-time comparison), they must not appear in the public Nginx reverse-proxy locations, and timestamp/nonce anti-replay must be added. - Do not keep the internal JWT in the URL query long-term: it enters Nginx/gateway logs and browser history. Use a one-time relay page that reads the hash (the
#part never lands in server logs), writes localStorage, and clears it immediately. - The callback gateway returns success first; the intranet processes asynchronously and idempotently: after verifying and decrypting, the gateway forwards to the intranet and returns in seconds itself; the intranet is idempotent by event id and tolerates gateway retries.
- Do not expose management ports bare through reverse tunnels: only intranet-initiated WireGuard/mTLS tunnels carrying narrow interfaces are allowed; bare frp mapping intranet 8080/admin console directly to the internet is forbidden.
- Verify certificates and reachability separately: the WeCom trusted domain/HTTPS certificate is configured on the gatewayβs public domain; the intranet can use self-signed or internal-CA certificates for mTLS β no public certificate needed. Be sure to regression-test silent login and callbacks on real off-site networks (4G/5G).
11. Go-Live Checklist
WeCom admin console
- The self-built appβs visible scope covers all user departments
- The app home page is configured as the H5 mobile address (https)
- The trusted domain is configured and the ownership-verification file is accessible
- The enterprise trusted IP (server outbound IP) is whitelisted
- The receive-message URL/Token/EncodingAESKey is configured and the GET verification passes
Accounts and identity
-
sys_user.wecom_user_idis initialized via contact-book sync, with correct employee-number mapping - Unmatched accounts get clear βcontact admin / self-service bindingβ guidance; no silent account creation
-
snsapi_basesilent login verified on real devices (iOS + Android) - Re-login after token expiry is seamless and redirects back correctly (including approval-detail deep links)
Functionality
- JS-SDK
wx.configpasses on both iOS and Android (focus on verifying the signature URL) - Location/photo/QR scanning work on real devices; backend secondary distance validation is effective
- Countersign: each person gets an independent to-do; any rejection terminates and notifies the initiator
- Or-sign: all candidates receive it; after one person claims and handles it, othersβ to-dos disappear
- Organization-chart approval: routes correctly to the head/managing director based on the applicantβs department
- After approval, attendance linkage (make-up correction / leave deduction) is persisted correctly
- To-do card push is delivered, taps go straight through and are already logged in; card button callbacks are idempotent
Security and operations
- secret/Token/AESKey come from environment variables, never in Git or logs
- access_token/jsapi_ticket caching + distributed lock verified (multi-instance)
- HTTPS certificate-expiry monitoring, API rate limiting, and key-operation audit logs
- Incremental contact-book callbacks + daily full-sync safety-net job enabled
Network isolation / public relay gateway (chapter 9, mandatory for isolated networks)
- The DMZ gateway is the only public exposure; the intranet attendance system has no public inbound rules
- The perimeter firewall opens only βgateway IP β intranet attendance IP:8080β; PG/Redis ports are not opened
- The WeCom secret / EncodingAESKey exist only on the gateway, not on the intranet; the gateway connects to no business database
-
/internal/**uses mTLS + internal token + timestamp/nonce anti-replay and is not reverse-proxied by public Nginx - Cross-zone silent login verified on real devices: gateway exchanges userid β intranet assert exchanges JWT β business APIs pass through authentication
- Callbacks: gateway verifies/decrypts and returns success in seconds, intranet is async and idempotent; to-dos are delivered via gateway proxy-send
- Off-site 4G/5G real-device regression: H5 loading, silent login, location check-in, approval to-dos and card callbacks
- If using a reverse tunnel: intranet-initiated, WireGuard/mTLS, only narrow interfaces exposed, no bare frp management ports
Summary
Given βan existing attendance system + complex Activiti approvalsβ, the correct approach to WeCom integration is not to rewrite a new system, but to treat WeCom as an entry point, identity provider, and message channel:
- Technology choice: when you already have a web system, complex approval forms, and require fast iteration and review-free releases, H5 fits better than a Mini Program; OAuth2
snsapi_basesilent authorization is enough for automatic login when the app is opened, and the JS-SDK covers location, photos, and QR scanning. - Automatic login flow: the frontend route guard finds no token β 302 to WeCom authorization (with state) β silent redirect back with code β backend gettoken +
auth/getuserinfoto obtain userid β map to an existing system account by employee number (not create a new one) β issue the systemβs existing JWT; thereafter all attendance and approval APIs are reused with zero changes. - Account decoupling: Activiti assignees/candidates continue to use internal usernames; the WeCom userid is only an external-identity field on
sys_user, converted when identifying a person at login or addressing pushes, preserving the ability to coexist with multiple login methods. - Approval reuse: countersign (multi-instance + completion condition), or-sign (candidateUsers + claim), and organization-chart approval (UEL expression dynamically resolving the leader) all reuse the existing BPMN; H5 only adds to-do list/detail/handling entry points, all going through the same
taskService.complete()underneath. - Linkage and reach: attendance linkage lives in a process-completion listener to guarantee consistency across entry points; new to-dos are pushed via textcard with a link straight to the approval detail, reusing silent login; one-tap in-card approval goes through callbacks and must be idempotent.
- Key pitfalls: trusted domain and enterprise trusted IP, iOS/Android signature-URL differences, one-time code and state CSRF protection, never creating duplicate accounts, or-sign claiming, callbacks returning success in seconds, and centralized ticket caching.
- Network isolation implementation: when the intranet attendance system cannot be reached by WeCom, deploy a public relay gateway in the DMZ as the only exposed surface β WeCom credentials and cloud calls (gettoken/getuserinfo/signatures/proxy message send/callback verification) converge on the gateway, while account binding, JWT, Activiti, and business data all stay on the intranet; the two sides communicate in a controlled manner over narrow mTLS + internal-token interfaces (
/internal/**); when even DMZβintranet inbound is forbidden, fall back to an intranet-initiated WireGuard/mTLS reverse tunnel. This opens the WeCom entry point without exposing the intranet system directly to the internet.
Official docs: WeCom Developer Center
The essence of this solution is βintegrationβ rather than βrebuildingβ: with minimal new code (one OAuth login endpoint, one account-mapping layer, one JS-SDK signature service, a set of to-do push listeners), years of accumulated attendance and Activiti approval capabilities appear smoothly in employeesβ WeCom, with seamless automatic login. If check-in experience needs further improvement later, a Mini Program check-in entry can be layered on, sharing the same backend accounts and workflows with H5 approval for smooth evolution.