We built an attendance summary for a client: every night, pull the previous day's check-in records from WeCom, write them to MySQL, and export an Excel for HR. Three days of local testing were flawless. On launch morning, HR sent one line: everyone's clock-in times are 8 hours late.
1. The Timestamp Got Converted Twice
The API returns checkin_time as a Unix timestamp — no timezone attached. Our script formatted it with PHP's date(), the server timezone set to Asia/Shanghai; that step was fine. The problem was an earlier "unified time handling" layer a colleague had written: it treated the timestamp as a string and parsed it again as UTC, adding one extra lap and shifting everything overall.
The fix was unglamorous: treat the value as a raw timestamp across the entire pipeline; format only at the final export step, with no conversions in between. We also added a raw_ts column to the database to keep the original value — when something breaks, you can always look back.
2. access_token Fetched on Every Request
Version one took the lazy path: every request called the token endpoint. Syncing 200-plus people meant dozens of requests, and one day we tripped the rate limit and the whole batch failed. We switched to Redis with a 7,000-second expiry — the official value is 7,200; leave margin — refreshing just before it lapses.
We'd integrated DingTalk's attendance API six months earlier. Different pits, much the same rate limits.
3. The Pagination Cursor Used Wrong
The check-in API paginates by cursor. Version one passed the last record's timestamp from the previous page as the cursor, so the boundary record got pulled twice and a few duplicates appeared in the timesheet. We switched to honestly using the next_cursor from the response, and added a unique index on (user_id, checkin_time) at write time, ignoring duplicates outright.
4. Inserting 2,000 Rows One at a Time
With 2,000-plus rows, the first pass looped single INSERTs and took over 20 seconds. Batching them into grouped INSERTs of 500 brought it down to just over 1 second. No cleverness involved — just don't forget.
5. We Ignored Makeup Punches
Besides normal check-ins, the API returns makeup-punch records — same fields, different source. Version one didn't distinguish them and counted a makeup punch as a second check-in that day. We later added a source field, and the export lists it in its own column per HR's rules.
Wrapping Up
That morning we corrected the 8-hour shift, re-ran the full sync, and HR re-exported. Before sending the sheet out, we hand-computed three people's times from their raw timestamps and compared — no more drift.
The first line of that script's comments now reads: time is converted once, at the end.

