|
@@ -0,0 +1,423 @@
|
|
|
|
|
+# 批量学生换班级 Implementation Plan
|
|
|
|
|
+
|
|
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
|
+
|
|
|
|
|
+**Goal:** 学生信息页「换班级」支持勾选多人,共用同一目标班级,前端串行调用现有单条接口写入 N 条 `status=0` 记录。
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** 仅改前端 `studentMsg/index.vue`。弹窗改为「学生列表 + 共用目标表单」;提交时对每人 `POST /studentMsg/studentClassChange`;已在目标班的学生跳过计入失败明细;后端与后台脚本不动。
|
|
|
|
|
+
|
|
|
|
|
+**Tech Stack:** Vue 3 + TypeScript + Element Plus、现有 `service.post`、`xg_student_class_change` 既有接口
|
|
|
|
|
+
|
|
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-15-batch-student-class-change-design.md`
|
|
|
|
|
+
|
|
|
|
|
+## Global Constraints
|
|
|
|
|
+
|
|
|
|
|
+- 不改后端接口 / 表结构 / `process_student_class_change_fixed.sh`
|
|
|
|
|
+- 不立即更新学生当前班级(仍由后台脚本处理)
|
|
|
|
|
+- 多人共用同一套新学院/专业/班级/生效时间/原因
|
|
|
|
|
+- 原班级不限制;部分成功不回滚;有失败则保留弹窗
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## File map
|
|
|
|
|
+
|
|
|
|
|
+| File | Responsibility |
|
|
|
|
|
+|------|----------------|
|
|
|
|
|
+| `web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue` | 按钮、弹窗、打开逻辑、批量提交 |
|
|
|
|
|
+| `docs/开发文档/API/学生学籍异动与换班级API.md` | 补充批量换班级(前端循环)说明 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: 数据结构与打开弹窗(支持多人)
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue`(约 1911–1932、3339–3392)
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Produces: `changeClassStudents: Ref<ChangeClassStudentItem[]>`;`changeClassSubmitting` / `changeClassProgress`;`changeClassFailList`
|
|
|
|
|
+- Produces: `openChangeClassModal()` 在 `ids.length >= 1` 时打开弹窗并填充列表
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 在换班级相关状态旁增加类型与列表状态**
|
|
|
|
|
+
|
|
|
|
|
+在 `changeClassDialog` / `changeClassForm` 附近加入(保留 `changeClassForm` 中目标班级字段;单人专用的 studentId/original* 可逐步不再依赖,由列表驱动):
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+interface ChangeClassStudentItem {
|
|
|
|
|
+ studentId: string | number;
|
|
|
|
|
+ studentNo: string;
|
|
|
|
|
+ studentName: string;
|
|
|
|
|
+ gender?: string;
|
|
|
|
|
+ originalCollegeId: string | number;
|
|
|
|
|
+ originalCollegeName: string;
|
|
|
|
|
+ originalMajorId: string | number;
|
|
|
|
|
+ originalMajorName: string;
|
|
|
|
|
+ originalClassId: string | number;
|
|
|
|
|
+ originalClassName: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const changeClassStudents = ref<ChangeClassStudentItem[]>([]);
|
|
|
|
|
+const changeClassSubmitting = ref(false);
|
|
|
|
|
+const changeClassProgress = ref({ current: 0, total: 0 });
|
|
|
|
|
+const changeClassFailList = ref<{ studentNo: string; studentName: string; reason: string }[]>([]);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 重写 `openChangeClassModal`**
|
|
|
|
|
+
|
|
|
|
|
+替换现有「必须 length === 1 + 拉详情」逻辑:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const openChangeClassModal = async () => {
|
|
|
|
|
+ if (ids.value.length === 0) {
|
|
|
|
|
+ ElMessage.warning('请选择要换班级的学生');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const selectedStudents = studentList.value.filter((s) => ids.value.includes(s.id));
|
|
|
|
|
+ if (selectedStudents.length === 0) {
|
|
|
|
|
+ ElMessage.error('未找到选中的学生信息');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ changeClassStudents.value = selectedStudents.map((s) => ({
|
|
|
|
|
+ studentId: s.id,
|
|
|
|
|
+ studentNo: s.studentNo,
|
|
|
|
|
+ studentName: s.name,
|
|
|
|
|
+ gender: s.gender,
|
|
|
|
|
+ originalCollegeId: s.collegeId,
|
|
|
|
|
+ originalCollegeName: s.collegeName,
|
|
|
|
|
+ originalMajorId: s.majorId,
|
|
|
|
|
+ originalMajorName: s.majorName,
|
|
|
|
|
+ originalClassId: s.classId,
|
|
|
|
|
+ originalClassName: s.className
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ changeClassForm.newCollegeId = '';
|
|
|
|
|
+ changeClassForm.newMajorId = '';
|
|
|
|
|
+ changeClassForm.newClassId = '';
|
|
|
|
|
+ changeClassForm.effectiveTime = '';
|
|
|
|
|
+ changeClassForm.changeReason = '';
|
|
|
|
|
+ majorOptions.value = [];
|
|
|
|
|
+ classOptions.value = [];
|
|
|
|
|
+ changeClassFailList.value = [];
|
|
|
|
|
+ changeClassProgress.value = { current: 0, total: 0 };
|
|
|
|
|
+
|
|
|
|
|
+ changeClassDialog.title =
|
|
|
|
|
+ changeClassStudents.value.length === 1
|
|
|
|
|
+ ? '换班级申请'
|
|
|
|
|
+ : `批量换班级(${changeClassStudents.value.length} 人)`;
|
|
|
|
|
+
|
|
|
|
|
+ await loadCollegeOptions();
|
|
|
|
|
+ changeClassDialog.visible = true;
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+说明:列表行已含学院/专业/班级字段,无需再逐个请求详情(减少等待)。若某行字段缺失,提交前该人记失败「学生信息不完整」。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 浏览器手测打开弹窗**
|
|
|
|
|
+
|
|
|
|
|
+Run: 打开 `http://localhost/student/studentmsg/studentMsg`,勾选 1 人与多人,分别点「换班级」
|
|
|
|
|
+
|
|
|
|
|
+Expected:
|
|
|
|
|
+- 未勾选:按钮仍禁用(现有 `:disabled="multiple"` 即可,`multiple = !selection.length`)
|
|
|
|
|
+- 勾选 1 / 多人:均可打开;标题分别为「换班级申请」/「批量换班级(N 人)」
|
|
|
|
|
+- 控制台无报错
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+feat(studentMsg): 换班级弹窗支持加载多名学生
|
|
|
|
|
+
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: 弹窗 UI — 学生列表 + 共用目标表单
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue`(约 926–1035 换班级对话框模板)
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Consumes: `changeClassStudents`、`changeClassDialog`、`changeClassForm`、`changeClassFailList`、`changeClassSubmitting`、`changeClassProgress`
|
|
|
|
|
+- Produces: 可见的学生表 + 目标表单;提交按钮绑定进度态
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 替换弹窗上半区「单人信息卡片」为表格**
|
|
|
|
|
+
|
|
|
|
|
+将「学生信息」`el-card` 内 `student-info-grid` 替换为:
|
|
|
|
|
+
|
|
|
|
|
+```vue
|
|
|
|
|
+<el-card shadow="never" class="mb-4">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="text-lg font-medium">
|
|
|
|
|
+ 学生信息(共 {{ changeClassStudents.length }} 人)
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <el-table :data="changeClassStudents" max-height="240" border size="small">
|
|
|
|
|
+ <el-table-column label="学号" prop="studentNo" min-width="110" />
|
|
|
|
|
+ <el-table-column label="姓名" prop="studentName" min-width="90" />
|
|
|
|
|
+ <el-table-column label="原学院" prop="originalCollegeName" min-width="120" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="原专业" prop="originalMajorName" min-width="120" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="原班级" prop="originalClassName" min-width="120" show-overflow-tooltip />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+</el-card>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+保留下半区「新班级信息」表单不变(`newCollegeId` / `newMajorId` / `newClassId` / `effectiveTime` / `changeReason` 及原有 `@change` 处理器)。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 失败明细与提交态 footer**
|
|
|
|
|
+
|
|
|
|
|
+在表单下方、`</el-form>` 之后增加失败列表;footer 按钮增加 loading/progress:
|
|
|
|
|
+
|
|
|
|
|
+```vue
|
|
|
|
|
+<div v-if="changeClassFailList.length" class="mt-3">
|
|
|
|
|
+ <el-alert type="error" :closable="false" title="部分学生提交失败">
|
|
|
|
|
+ <ul class="pl-4 m-0">
|
|
|
|
|
+ <li v-for="(item, idx) in changeClassFailList" :key="idx">
|
|
|
|
|
+ {{ item.studentNo }} {{ item.studentName }}:{{ item.reason }}
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </el-alert>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+<template #footer>
|
|
|
|
|
+ <div class="dialog-footer">
|
|
|
|
|
+ <el-button :disabled="changeClassSubmitting" @click="changeClassDialog.visible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="changeClassSubmitting" @click="submitChangeClass">
|
|
|
|
|
+ <template v-if="changeClassSubmitting">
|
|
|
|
|
+ 提交中 {{ changeClassProgress.current }}/{{ changeClassProgress.total }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-else>确定</template>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: 手测 UI**
|
|
|
|
|
+
|
|
|
|
|
+Expected: 多人打开时表格展示各人原班级;目标下拉联动仍可用;失败区初始为空。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+feat(studentMsg): 换班级弹窗改为学生列表+共用目标表单
|
|
|
|
|
+
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: 批量提交逻辑
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue`(`submitChangeClass`,约 3469–3521)
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:**
|
|
|
|
|
+- Consumes: `changeClassStudents`、`changeClassForm`、学院/专业/班级 options
|
|
|
|
|
+- Produces: 串行写入 N 条记录;更新 `changeClassFailList` / progress;全成功关窗刷新
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 重写 `submitChangeClass`**
|
|
|
|
|
+
|
|
|
|
|
+完整替换为:
|
|
|
|
|
+
|
|
|
|
|
+```ts
|
|
|
|
|
+const submitChangeClass = async () => {
|
|
|
|
|
+ const formRef = changeClassFormRef.value;
|
|
|
|
|
+ if (!formRef) return;
|
|
|
|
|
+
|
|
|
|
|
+ await formRef.validate(async (valid: boolean) => {
|
|
|
|
|
+ if (!valid) return;
|
|
|
|
|
+ if (!changeClassStudents.value.length) {
|
|
|
|
|
+ ElMessage.warning('没有待换班级的学生');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const newCollegeName =
|
|
|
|
|
+ collegeOptions.value.find((c) => c.id === changeClassForm.newCollegeId)?.label || '';
|
|
|
|
|
+ const newMajorName =
|
|
|
|
|
+ majorOptions.value.find((m) => m.id === changeClassForm.newMajorId)?.label || '';
|
|
|
|
|
+ const newClassName =
|
|
|
|
|
+ classOptions.value.find((c) => c.id === changeClassForm.newClassId)?.label || '';
|
|
|
|
|
+ const changeTime = proxy?.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}');
|
|
|
|
|
+
|
|
|
|
|
+ changeClassSubmitting.value = true;
|
|
|
|
|
+ changeClassFailList.value = [];
|
|
|
|
|
+ changeClassProgress.value = { current: 0, total: changeClassStudents.value.length };
|
|
|
|
|
+
|
|
|
|
|
+ let successCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (const student of changeClassStudents.value) {
|
|
|
|
|
+ changeClassProgress.value.current += 1;
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ student.originalClassId === undefined ||
|
|
|
|
|
+ student.originalClassId === null ||
|
|
|
|
|
+ student.originalClassId === ''
|
|
|
|
|
+ ) {
|
|
|
|
|
+ changeClassFailList.value.push({
|
|
|
|
|
+ studentNo: student.studentNo,
|
|
|
|
|
+ studentName: student.studentName,
|
|
|
|
|
+ reason: '学生信息不完整'
|
|
|
|
|
+ });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (String(student.originalClassId) === String(changeClassForm.newClassId)) {
|
|
|
|
|
+ changeClassFailList.value.push({
|
|
|
|
|
+ studentNo: student.studentNo,
|
|
|
|
|
+ studentName: student.studentName,
|
|
|
|
|
+ reason: '已在目标班级'
|
|
|
|
|
+ });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const submitData = {
|
|
|
|
|
+ studentId: student.studentId,
|
|
|
|
|
+ studentNo: student.studentNo,
|
|
|
|
|
+ studentName: student.studentName,
|
|
|
|
|
+ originalCollegeId: student.originalCollegeId,
|
|
|
|
|
+ originalCollegeName: student.originalCollegeName,
|
|
|
|
|
+ originalMajorId: student.originalMajorId,
|
|
|
|
|
+ originalMajorName: student.originalMajorName,
|
|
|
|
|
+ originalClassId: student.originalClassId,
|
|
|
|
|
+ originalClassCode: '',
|
|
|
|
|
+ originalClassName: student.originalClassName,
|
|
|
|
|
+ newCollegeId: changeClassForm.newCollegeId,
|
|
|
|
|
+ newCollegeName,
|
|
|
|
|
+ newMajorId: changeClassForm.newMajorId,
|
|
|
|
|
+ newMajorName,
|
|
|
|
|
+ newClassId: changeClassForm.newClassId,
|
|
|
|
|
+ newClassName,
|
|
|
|
|
+ changeReason: changeClassForm.changeReason,
|
|
|
|
|
+ changeType: '1',
|
|
|
|
|
+ changeTime,
|
|
|
|
|
+ effectiveTime: changeClassForm.effectiveTime,
|
|
|
|
|
+ status: '0'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await service.post('/studentMsg/studentClassChange', submitData);
|
|
|
|
|
+ if (res && res.code === 200) {
|
|
|
|
|
+ successCount += 1;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ changeClassFailList.value.push({
|
|
|
|
|
+ studentNo: student.studentNo,
|
|
|
|
|
+ studentName: student.studentName,
|
|
|
|
|
+ reason: res?.msg || '提交失败'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error('提交换班级申请失败:', error);
|
|
|
|
|
+ changeClassFailList.value.push({
|
|
|
|
|
+ studentNo: student.studentNo,
|
|
|
|
|
+ studentName: student.studentName,
|
|
|
|
|
+ reason: error?.message || '提交失败,请重试'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (changeClassFailList.value.length === 0) {
|
|
|
|
|
+ ElMessage.success(
|
|
|
|
|
+ changeClassStudents.value.length === 1
|
|
|
|
|
+ ? '换班级申请提交成功'
|
|
|
|
|
+ : `批量换班级成功(${successCount} 人)`
|
|
|
|
|
+ );
|
|
|
|
|
+ changeClassDialog.visible = false;
|
|
|
|
|
+ await getList();
|
|
|
|
|
+ } else if (successCount > 0) {
|
|
|
|
|
+ ElMessage.warning(`成功 ${successCount} 人,失败 ${changeClassFailList.value.length} 人`);
|
|
|
|
|
+ await getList();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error('全部提交失败,请查看失败明细');
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ changeClassSubmitting.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: 手测提交场景**
|
|
|
|
|
+
|
|
|
|
|
+| 场景 | Expected |
|
|
|
|
|
+|------|----------|
|
|
|
|
|
+| 选 1 人,正常目标班 | 1 条 `status=0`;弹窗关闭;提示成功 |
|
|
|
|
|
+| 选 3 人,不同原班,同一目标 | 3 条记录,目标字段相同 |
|
|
|
|
|
+| 含 1 人已在目标班 | 该人失败明细「已在目标班级」;其他人成功;弹窗保留 |
|
|
|
|
|
+| 全部已在目标班 | 全部失败;弹窗保留;不假装成功 |
|
|
|
|
|
+
|
|
|
|
|
+可用换班级记录页 `/` 下对应菜单或接口 `GET /studentMsg/studentClassChange/list` 核对。
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+feat(studentMsg): 换班级支持批量串行提交并汇总失败
|
|
|
|
|
+
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### Task 4: 文档同步
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `docs/开发文档/API/学生学籍异动与换班级API.md`
|
|
|
|
|
+- Modify: `docs/superpowers/specs/2026-09-15-batch-student-class-change-design.md`(状态改为已实现)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 1: 在 API 文档「模块说明」后增加章节**
|
|
|
|
|
+
|
|
|
|
|
+```markdown
|
|
|
|
|
+## 3. 学生信息页批量换班级(前端)
|
|
|
|
|
+
|
|
|
|
|
+- 入口:学生信息页「换班级」按钮(权限 `studentMsg:class:change`)
|
|
|
|
|
+- 行为:勾选 1~N 名学生,共用同一目标学院/专业/班级/生效时间/原因
|
|
|
|
|
+- 实现:前端串行多次调用 `POST /studentMsg/studentClassChange`,每人一条记录,`status=0`
|
|
|
|
|
+- 后端:无新接口;班级实际变更仍由后台脚本 `学生换班级/process_student_class_change_fixed.sh` 处理
|
|
|
|
|
+- 边界:已在目标班级的学生跳过并计入失败明细;部分失败不回滚已成功记录
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+并在文末更新记录表增加一行:`2026-09-15 | 补充学生信息页批量换班级说明 | Cursor`
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 2: Spec 状态改为「已实现」**
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add docs/开发文档/API/学生学籍异动与换班级API.md \
|
|
|
|
|
+ docs/superpowers/specs/2026-09-15-batch-student-class-change-design.md
|
|
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
|
|
+docs: 同步批量换班级 API 说明与设计状态
|
|
|
|
|
+
|
|
|
|
|
+EOF
|
|
|
|
|
+)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Spec coverage (self-review)
|
|
|
|
|
+
|
|
|
|
|
+| Spec 要求 | Task |
|
|
|
|
|
+|-----------|------|
|
|
|
|
|
+| 多人同一目标班级 | Task 2 + 3 |
|
|
|
|
|
+| 原班级不限制 | Task 1(不校验同源) |
|
|
|
|
|
+| 只写 status=0,脚本处理 | Task 3 payload |
|
|
|
|
|
+| 前端循环现有接口 | Task 3 |
|
|
|
|
|
+| 按钮支持多人 | Task 1(打开逻辑)+ 既有 disabled |
|
|
|
|
|
+| 学生列表 + 共用表单 | Task 2 |
|
|
|
|
|
+| 进度与失败保留弹窗 | Task 2 + 3 |
|
|
|
|
|
+| 已在目标班跳过 | Task 3 |
|
|
|
|
|
+| 不改后端/脚本 | 全任务遵守 |
|
|
|
|
|
+| 文档同步 | Task 4 |
|
|
|
|
|
+| 单人行为兼容 | Task 3 成功文案与 1 人路径 |
|
|
|
|
|
+
|
|
|
|
|
+**Placeholder scan:** 无 TBD /「类似 Task N」含糊步骤。
|
|
|
|
|
+**类型一致性:** `ChangeClassStudentItem` / `changeClassFailList` 在 Task 1–3 命名一致。
|