# DingTalk 导入链路修复 — 设计文档

> 日期：2026-07-09  
> 状态：已确认  
> 关联：PRD 钉钉集成批次 1/2

---

## 1. 背景

DingTalk 用户导入 → 班级标记 → 排课 → 考勤 的链路已基本搭建完成，但存在两个阻塞问题：

1. **前端：** "标为班级"按钮出现在所有部门节点上，包括有子部门的父节点。应该在叶子节点（无子部门、有用户）才显示。
2. **后端：** `CampusScope` 按 `departmentId` 做数据隔离，但钉钉同步的老师没有 `UserDepartment` 关联，导致非超管老师看不到学生和考勤数据。

本设计的核心原则：**教学域（班级、学生、考勤、排课）不走部门过滤，权限边界是"老师教的班"，不是"老师归哪个部门"。**

---

## 2. 改动清单

### 2.1 前端：叶子节点判断

**文件：** `apps/admin/src/pages/IntegrationConfig/index.tsx`

**位置：** `buildTreeData` 回调函数，第 371–431 行

**改动：** 部门节点的标题渲染逻辑改为：

```
if (node.children.length === 0 && node.users.length > 0) {
  // 叶子节点：显示"标为班级"按钮 或 "班级: xxx [已标记]"
} else {
  // 非叶子节点：只显示部门名称
}
```

非叶子节点（有子部门）不显示任何班级标记入口。

### 2.2 后端：移除教学域 CampusScope

**原则：** CampusScope 保留给管理域（宿舍 `OccupanciesService`、房间 `RoomsService`、账单 `BillsService`、费用 `ExpensesService`）——这些按校区/部门隔离数据是合理的。教学域不走部门过滤。

#### 2.2.1 StudentsService

**文件：** `apps/server/src/students/students.service.ts`

**改动：**
- 移除 `CampusScope` 注入（构造函数参数 `private readonly scope: CampusScope`）
- 移除 `findAll` 方法中的 `await this.scope.filter(where)` 调用
- `findAll` 直接使用原始 `where` 条件查询

**影响：** 有 `student:view` 权限的用户可以看到所有学生，不再按部门隔离。

#### 2.2.2 AttendanceService

**文件：** `apps/server/src/attendance/attendance.service.ts`

**改动：**
- 移除 `CampusScope` 注入
- 移除所有 `scope.filter()` / `scope.getScopeDepartmentIds()` 调用
- 考勤记录查询不再按部门过滤

**影响：** 有 `attendance:view` 权限的用户可以看到所有考勤数据。

---

## 3. 不改的

| 模块 | 原因 |
|---|---|
| `ClassesService` | 已不使用 CampusScope |
| `SchedulesService` | 已不使用 CampusScope |
| `OccupanciesService` | 宿舍管理需要部门隔离，保留 |
| `RoomsService` | 房间管理需要部门隔离，保留 |
| `BillsService` | 财务管理需要部门隔离，保留 |
| `ExpensesService` | 费用管理需要部门隔离，保留 |
| `DingTalkService.syncAll` | 同步逻辑不变 |
| `SyncService.importDingTalkUsers` | 导入逻辑不变 |
| RBAC 权限码 | 不动 |

---

## 4. 验收标准

1. 钉钉组织架构树中，父部门节点不显示"标为班级"按钮，叶子节点正常显示
2. 非超管老师登录后，学生列表正常显示所有学生
3. 非超管老师登录后，考勤页面正常显示所有考勤记录
4. 宿舍、财务等管理域功能不受影响（部门过滤仍然生效）
5. `tsc --noEmit` 编译通过
