feat(sync): sync class schedules to DingTalk students

Group active schedules by class, resolve enrolled students through DingTalk mappings, reuse shifts and attendance groups, and expose class-based sync status in the admin UI.
This commit is contained in:
2026-07-10 14:11:39 +08:00
parent bc6b8b0095
commit 55881863c1
5 changed files with 246 additions and 192 deletions

View File

@@ -71,7 +71,7 @@ interface ScheduleSyncResult {
groupCount: number;
syncedItems: number;
skippedNoMapping: number;
groups: Array<{ deptName: string; groupId: number; itemCount: number }>;
groups: Array<{ className: string; groupId: number; itemCount: number }>;
}
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
@@ -112,12 +112,12 @@ const SchedulesPage: React.FC = () => {
const [syncModalOpen, setSyncModalOpen] = useState(false);
const [syncing, setSyncing] = useState(false);
const [syncStatus, setSyncStatus] = useState<{
activeSchedules: number; mappedTeachers: number; totalTeachers: number;
activeSchedules: number; mappedClasses: number; totalClasses: number;
} | null>(null);
const [syncResult, setSyncResult] = useState<{
scheduleCount: number; shiftCount: number; groupCount: number;
syncedItems: number; skippedNoMapping: number;
groups: Array<{ deptName: string; groupId: number; itemCount: number }>;
groups: Array<{ className: string; groupId: number; itemCount: number }>;
} | null>(null);
const [syncDateFrom, setSyncDateFrom] = useState<Dayjs>(dayjs);
const [syncDays, setSyncDays] = useState(30);
@@ -128,7 +128,7 @@ const SchedulesPage: React.FC = () => {
setSyncResult(null);
try {
const res = await api.get<{
success: boolean; data: { activeSchedules: number; mappedTeachers: number; totalTeachers: number };
success: boolean; data: { activeSchedules: number; mappedClasses: number; totalClasses: number };
}>('/sync/schedule/status');
setSyncStatus(res.data);
} catch {
@@ -986,17 +986,17 @@ const SchedulesPage: React.FC = () => {
{syncResult.skippedNoMapping > 0 && (
<Alert
type="warning"
message={`${syncResult.skippedNoMapping} 条排课因教师未绑定钉钉而跳过`}
message={`${syncResult.skippedNoMapping} 条排课因班级无钉钉绑定学生而跳过`}
style={{ marginBottom: 16 }}
showIcon
/>
)}
{syncResult.groups.length > 0 && (
<div>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
{syncResult.groups.map((g) => (
<Tag key={g.groupId} color="blue" style={{ marginBottom: 4 }}>
{g.deptName}{g.itemCount}
{g.className}{g.itemCount}
</Tag>
))}
</div>
@@ -1011,20 +1011,20 @@ const SchedulesPage: React.FC = () => {
</Col>
<Col span={8}>
<Statistic
title="已绑定教师"
value={syncStatus.mappedTeachers}
suffix={`/ ${syncStatus.totalTeachers}`}
valueStyle={{ color: syncStatus.mappedTeachers < syncStatus.totalTeachers ? '#faad14' : '#3f8600' }}
title="已就绪班级"
value={syncStatus.mappedClasses}
suffix={`/ ${syncStatus.totalClasses}`}
valueStyle={{ color: syncStatus.mappedClasses < syncStatus.totalClasses ? '#faad14' : '#3f8600' }}
/>
</Col>
<Col span={8}>
<Statistic title="绑定" value={syncStatus.totalTeachers - syncStatus.mappedTeachers} suffix="" />
<Statistic title="绑定学生班级" value={syncStatus.totalClasses - syncStatus.mappedClasses} suffix="" />
</Col>
</Row>
{syncStatus.mappedTeachers < syncStatus.totalTeachers && (
{syncStatus.mappedClasses < syncStatus.totalClasses && (
<Alert
type="warning"
message={`${syncStatus.totalTeachers - syncStatus.mappedTeachers} 位教师未绑定钉钉,其排课将被跳过。请先执行组织架构同步`}
message={`${syncStatus.totalClasses - syncStatus.mappedClasses} 个班级没有已绑定钉钉的学生,其排课将被跳过。请先在钉钉集成页导入并绑定学生`}
style={{ marginBottom: 16 }}
showIcon
/>