Files
gongxue-base/apps/server/src/sync/sync.controller.ts
wangziqi cc4f4dae4e fix: audit remediation — SSE user scoping, FK transactional safety, UI error handling
- H4: scoped SSE import progress to exact userId match; non-HTTP events excluded from all subscribers
- H2: moved PRAGMA foreign_key_check inside SQLite transaction before COMMIT; violations rollback preserving old tables
- M1: removed dead axios-style error branch from extractErrorMessage (interceptor already unwraps)
- M2: split handleSave try/catch — save errors vs reload errors shown distinctly
- M3: added provider field validation before AI config test request
- Added SSE scoping regression tests (import service + controller)
- Added FK check failure rollback test (database-migrations.spec)
- Updated controller spec expectations for userId parameter

Co-authored-by: Code Review <branch-review>
2026-07-12 22:59:03 +08:00

114 lines
3.8 KiB
TypeScript

import { BadRequestException, Controller, Get, Logger, Post, Query, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RequirePermission } from '../auth/decorators/permission.decorator';
import { SyncService } from './sync.service';
import type { SyncPlatform } from '../entities/sync-log.entity';
@UseGuards(JwtAuthGuard)
@Controller('sync')
export class SyncController {
private readonly logger = new Logger(SyncController.name);
constructor(private readonly syncService: SyncService) {}
@Post('trigger')
@RequirePermission('sync:trigger')
async triggerSync(
@Query('platform') platform?: SyncPlatform,
@Query('rootDeptId') rootDeptId?: string,
) {
const rootId = rootDeptId ? this.parseRootDeptId(rootDeptId) : 1;
const logs = await this.syncService.triggerSync(platform, rootId);
return { synced: logs.length, logs };
}
@Get('status')
@RequirePermission('sync:read')
async getStatus() {
const lastDingTalk = await this.syncService.getLastSync('dingtalk');
const lastWeCom = await this.syncService.getLastSync('wecom');
return {
dingTalk: lastDingTalk ? { lastSyncAt: lastDingTalk.finishedAt, status: lastDingTalk.status } : null,
weCom: lastWeCom ? { lastSyncAt: lastWeCom.finishedAt, status: lastWeCom.status } : null,
};
}
/** 获取钉钉组织部门树,供前端选择同步起点 */
@Get('dingtalk/org-tree')
@RequirePermission('sync:read')
async getDingTalkOrgTree(@Query('rootDeptId') rootDeptId?: string) {
const rootId = rootDeptId ? this.parseRootDeptId(rootDeptId) : 1;
const tree = await this.syncService.getDingTalkOrgTree(rootId);
return { success: true, data: tree };
}
/** 获取钉钉组织部门树(含用户),供同步用户选择器使用 */
@Get('dingtalk/org-tree-with-users')
@RequirePermission('sync:read')
async getDingTalkOrgTreeWithUsers(@Query('rootDeptId') rootDeptId?: string) {
const rootId = rootDeptId ? this.parseRootDeptId(rootDeptId) : 1;
const tree = await this.syncService.getDingTalkOrgTreeWithUsers(rootId);
return { success: true, data: tree };
}
@Get('dingtalk/attendance-groups')
@RequirePermission('sync:read')
async getDingTalkAttendanceGroups() {
return { success: true, data: await this.syncService.getDingTalkAttendanceGroups() };
}
@Post('dingtalk/attendance-groups/delete-all')
@RequirePermission('sync:trigger')
async deleteAllDingTalkAttendanceGroups() {
return {
success: true,
data: await this.syncService.deleteAllDingTalkAttendanceGroups(),
};
}
@Get('logs')
@RequirePermission('sync:read')
async getLogs(
@Query('platform') platform?: SyncPlatform,
@Query('limit') limit?: number,
) {
return this.syncService.getLogs(platform, limit ? Number(limit) : 50);
}
// ── 排班同步 ──
/** 触发排班同步到钉钉考勤排班 */
@Post('schedule/sync')
@RequirePermission('sync:trigger')
async syncSchedule(
@Query('dateFrom') dateFrom?: string,
@Query('days') days?: string,
@Query('attendanceMachineOnly') attendanceMachineOnly?: string,
) {
const result = await this.syncService.syncScheduleToDingTalk(
dateFrom,
days ? parseInt(days, 10) : 30,
attendanceMachineOnly === 'true',
);
return { success: true, data: result };
}
/** 查询钉钉排班状态(核对本地 vs 钉钉) */
@Get('schedule/status')
@RequirePermission('sync:read')
async getScheduleStatus(
@Query('date') date?: string,
) {
const status = await this.syncService.getScheduleSyncStatus(date);
return { success: true, data: status };
}
private parseRootDeptId(rootDeptId: string): number {
const parsed = parseInt(rootDeptId, 10);
if (isNaN(parsed)) {
throw new BadRequestException('rootDeptId must be a valid integer');
}
return parsed;
}
}