feat: DingTalk attendance import + integration config + expense types + UI polish

Server:
- Add DingTalk attendance import service with SSE progress streaming
- Add IntegrationConfig entity & module for multi-tenant DingTalk setup
- Add ExpenseType entity & ExpenseTypesModule
- Add SeedModule for DB initialization
- Add UserDingMapping entity for DingTalk user linkage
- Attendance service: import flow with dedup & student auto-mapping
- Rooms service: time-range overlap queries
- Sync controller/service: DingTalk integration wiring
- Permission guard: refactor to pure re-export
- Campus scope middleware: tenant-aware filtering

Admin UI:
- Attendance page: import UI with progress & result summary
- All pages: tableStyle/tablePagination standardization
- Login page: responsive styling
- Sensitive data: useViewSensitive hook for masked viewing
- Vite config: path aliases, build optimization
- Test infra: vitest config, test utilities

Docs: PRD DingTalk batch 1 & 2 design docs
This commit is contained in:
2026-07-09 09:11:56 +08:00
parent f1959f0d2a
commit 42d3f0e27f
71 changed files with 5331 additions and 609 deletions

View File

@@ -0,0 +1,46 @@
import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RequirePermission } from '../../auth/decorators/permission.decorator';
import { IntegrationConfigService } from './integration-config.service';
import type { SaveConfigRequest } from './dto/config.dto';
@Controller('integration/config')
@UseGuards(JwtAuthGuard)
export class IntegrationConfigController {
constructor(private readonly service: IntegrationConfigService) {}
/** 获取全部配置(脱敏) */
@Get()
@RequirePermission('integration:read')
async getConfigs() {
const data = await this.service.getThirdConfig();
return { success: true, data };
}
/** 按类型获取单个配置 */
@Get(':type')
@RequirePermission('integration:read')
async getConfig(@Param('type') type: string) {
const data = await this.service.getConfigByType(type.toUpperCase());
if (!data) {
return { success: false, message: `未找到 ${type} 的配置` };
}
return { success: true, data };
}
/** 保存配置 */
@Post()
@RequirePermission('integration:read')
async saveConfig(@Body() body: SaveConfigRequest) {
await this.service.saveConfig(body);
return { success: true, message: '配置已保存' };
}
/** 测试连接 */
@Post('test')
@RequirePermission('integration:read')
async testConnection(@Body() body: SaveConfigRequest) {
const success = await this.service.testConnection(body.type, body.config);
return { success, message: success ? '连接成功' : '连接失败,请检查配置信息' };
}
}