From f1959f0d2a9df41062295d060cd92d1b4b7c28f3 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Wed, 8 Jul 2026 14:49:22 +0800 Subject: [PATCH] fix: switch DingTalk attendance API from getcolumnval to listRecord --- .../src/integration/dingtalk.service.ts | 37 +++++++++------ apps/server/src/sync/sync.module.ts | 6 ++- apps/server/src/sync/sync.service.ts | 46 +++++++++++++++++-- 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index 02c149b..3f98df9 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -392,18 +392,17 @@ export class DingTalkService { if (!this.configured) throw new Error('DingTalk not configured'); const token = await this.getAccessToken(); - const columnIdList = ['1','2','3','4','5','6','8','9']; + const dateFrom = params.startDate.includes(' ') ? params.startDate : `${params.startDate} 00:00:00`; + const dateTo = params.endDate.includes(' ') ? params.endDate : `${params.endDate} 23:59:59`; + const body: Record = { - column_id_list: columnIdList.join(','), - from_date: params.startDate, - to_date: params.endDate, - offset: params.offset ?? 0, - limit: params.limit ?? 50, + checkDateFrom: dateFrom, + checkDateTo: dateTo, }; - if (params.userIds?.length) body.userid_list = params.userIds.join(','); + if (params.userIds?.length) body.userIds = params.userIds; const res = await fetch( - `https://oapi.dingtalk.com/topapi/attendance/getcolumnval?access_token=${token}`, + `https://oapi.dingtalk.com/attendance/listRecord?access_token=${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -412,14 +411,26 @@ export class DingTalkService { ); const data = await res.json() as { errcode: number; errmsg: string; - result: { hasMore: boolean; column_vals?: Array<{ column_vals: string[] }> }; + recordresult?: Array<{ + id: number; userId: string; workDate: number; + userCheckTime: number; sourceType: string; + checkType?: string; timeResult?: string; + locationResult?: string; locationMethod?: string; + userAddress?: string; userLongitude?: number; userLatitude?: number; + }>; }; if (data.errcode !== 0) throw new Error(`钉钉考勤获取失败: ${data.errmsg}`); - return (data.result?.column_vals ?? []).map((col) => { - const v = col.column_vals ?? []; - return { userId: v[0]??'', workDate: v[1]??'', timeResult: v[2]??'', locationResult: v[3]??'', planCheckTime: v[4]??'', actualCheckTime: v[5]??'', checkId: v[7]??'', checkType: v[8]??'' }; - }); + return (data.recordresult ?? []).map((r) => ({ + userId: r.userId, + workDate: new Date(r.workDate).toISOString().slice(0, 10), + timeResult: r.timeResult ?? r.sourceType ?? '', + locationResult: r.locationResult ?? r.locationMethod ?? r.userAddress ?? '', + planCheckTime: '', + actualCheckTime: new Date(r.userCheckTime).toISOString(), + checkId: String(r.id), + checkType: r.checkType ?? r.sourceType ?? '', + })); } private delay(requestIndex: number): Promise { diff --git a/apps/server/src/sync/sync.module.ts b/apps/server/src/sync/sync.module.ts index 58173ce..ac7c2ea 100644 --- a/apps/server/src/sync/sync.module.ts +++ b/apps/server/src/sync/sync.module.ts @@ -2,15 +2,17 @@ import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IntegrationModule } from '../integration/integration.module'; -import { SyncLog, SyncState } from '../entities'; +import { AttendanceModule } from '../attendance/attendance.module'; +import { SyncLog, SyncState, UserDingMapping } from '../entities'; import { SyncService } from './sync.service'; import { SyncController } from './sync.controller'; @Module({ imports: [ ScheduleModule.forRoot(), - TypeOrmModule.forFeature([SyncLog, SyncState]), + TypeOrmModule.forFeature([SyncLog, SyncState, UserDingMapping]), IntegrationModule, + AttendanceModule, ], controllers: [SyncController], providers: [SyncService], diff --git a/apps/server/src/sync/sync.service.ts b/apps/server/src/sync/sync.service.ts index 9424190..6ef8cef 100644 --- a/apps/server/src/sync/sync.service.ts +++ b/apps/server/src/sync/sync.service.ts @@ -2,22 +2,25 @@ import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { SyncLog, SyncState } from '../entities'; +import { SyncLog, SyncState, UserDingMapping } from '../entities'; import type { SyncPlatform, SyncType, SyncStatus } from '../entities/sync-log.entity'; import { DingTalkService } from '../integration/dingtalk.service'; import { WeComService } from '../integration/wecom.service'; +import { AttendanceImportService } from '../attendance/attendance-import.service'; @Injectable() export class SyncService { private readonly logger = new Logger(SyncService.name); - constructor( @InjectRepository(SyncLog) private readonly syncLogRepo: Repository, @InjectRepository(SyncState) private readonly syncStateRepo: Repository, + @InjectRepository(UserDingMapping) + private readonly mappingRepo: Repository, private readonly dingTalkService: DingTalkService, private readonly weComService: WeComService, + private readonly attendanceImportService: AttendanceImportService, ) {} // ── Scheduled cron: daily at 2 AM ── @@ -151,9 +154,44 @@ export class SyncService { await this.syncLogRepo.save(log); } - private async performDingTalkSync(_lastSyncAt: Date | null): Promise { + private async performDingTalkSync(lastSyncAt: Date | null): Promise { + // Stage 1: Sync departments and users const result = await this.dingTalkService.syncAll(); - return result.deptCount + result.userCount; + let total = result.deptCount + result.userCount; + + // Stage 2: Import attendance data (last 7 days or since last sync) + try { + const endDate = new Date(); + const startDate = new Date(); + // If never synced, import last 7 days; otherwise import since last sync + if (lastSyncAt) { + startDate.setTime(lastSyncAt.getTime()); + } else { + startDate.setDate(startDate.getDate() - 7); + } + + const start = startDate.toISOString().slice(0, 10); + const end = endDate.toISOString().slice(0, 10); + + this.logger.log(`Importing DingTalk attendance: ${start} ~ ${end}`); + const mappings = await this.mappingRepo.find(); + const userIds = mappings.map((m) => m.dingUserId).filter(Boolean); + const importResult = await this.attendanceImportService.importFromDingTalk({ + startDate: start, + endDate: end, + userIds: userIds.length > 0 ? userIds : undefined, + autoMatch: true, + }); + + total += importResult.imported; + this.logger.log(`DingTalk attendance import: ${importResult.imported} imported, ${importResult.skipped} skipped`); + } catch (err: unknown) { + // Attendance import failure should not block the sync + const msg = err instanceof Error ? err.message : String(err); + this.logger.warn(`DingTalk attendance import failed (non-fatal): ${msg}`); + } + + return total; } private async performWeComSync(_lastSyncAt: Date | null): Promise {