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,78 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
/**
* 第三方集成主配置表。全局单例(本项目无多组织)。
* type 目前固定为 'THIRD'。
*/
@Entity('integration_config')
export class IntegrationConfig {
@PrimaryGeneratedColumn()
id: number;
/** 配置类型,目前固定 'THIRD' */
@Column({ length: 50 })
type: string;
/** 最近一次同步的来源: 'WECOM' | 'DINGTALK' | null */
@Column({ name: 'sync_resource', length: 50, nullable: true })
syncResource: string;
/** 是否已同步过 */
@Column({ name: 'is_sync', default: false })
isSync: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
/**
* 第三方集成明细配置表。一个主配置对应多条明细(钉钉/企微各一条)。
* content 存 JSON 字符串,含加密/明文的 corpId、appSecret 等。
*/
@Entity('integration_config_detail')
@Index(['configId'])
export class IntegrationConfigDetail {
@PrimaryGeneratedColumn()
id: number;
/** 关联主配置表 IntegrationConfig.id */
@Column({ name: 'config_id', type: 'integer' })
configId: number;
@ManyToOne(() => IntegrationConfig)
@JoinColumn({ name: 'config_id' })
config: IntegrationConfig;
@Column({ length: 100, nullable: true })
name: string;
/** 明细类型: 'DINGTALK_SYNC' | 'WECOM_SYNC' */
@Column({ length: 50 })
type: string;
/** 配置 JSON 字符串: { type, verify, config: {...} } */
@Column({ type: 'text', nullable: true })
content: string;
/** 该明细是否验证通过(能拿到 token */
@Column({ default: false })
enable: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}