281 lines
18 KiB
JavaScript
281 lines
18 KiB
JavaScript
/**
|
||
* 教室管理后端冒烟脚本(Node 18+,无第三方依赖)。
|
||
*
|
||
* 用法:
|
||
* node smoke/smoke.mjs # 无认证模式(本地 YH_SSO_URLPATTERN=/none/* + ACACLASSROOM_DEVCONTEXT_ENABLED=true)
|
||
* TEACHER_COOKIE="CASTGC=..." node smoke/smoke.mjs # 真实 SSO 会话(浏览器复制 Cookie)
|
||
*
|
||
* 覆盖:健康检查 → 基础/校区/类型/楼栋/教室 全链路(自建数据 → 断言 → 逆序清理)。
|
||
*/
|
||
const BASE = process.env.SMOKE_BASE || "http://localhost:8106";
|
||
const COOKIE = process.env.TEACHER_COOKIE || "";
|
||
const headers = { ...(COOKIE ? { Cookie: COOKIE } : {}) };
|
||
|
||
let passed = 0, failed = 0;
|
||
const ok = (name, cond, detail = "") => {
|
||
if (cond) { passed++; console.log(` ✓ ${name}`); }
|
||
else { failed++; console.error(` ✗ ${name} ${detail}`); }
|
||
};
|
||
|
||
async function api(method, path, { form, json, params } = {}) {
|
||
const url = new URL(BASE + path);
|
||
if (params) Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, v));
|
||
const opt = { method, headers: { ...headers } };
|
||
if (json != null) {
|
||
opt.headers["Content-Type"] = "application/json;charset=UTF-8";
|
||
opt.body = JSON.stringify(json);
|
||
} else if (form != null) {
|
||
opt.headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
|
||
opt.body = new URLSearchParams(form).toString();
|
||
}
|
||
const res = await fetch(url, opt);
|
||
const text = await res.text();
|
||
try { return JSON.parse(text); } catch { return { code: -1, msg: text.substring(0, 120) }; }
|
||
}
|
||
|
||
console.log(`[smoke] target=${BASE} auth=${COOKIE ? "cookie" : "none(dev-context)"}`);
|
||
|
||
// 1. 健康
|
||
const health = await fetch(`${BASE}/actuator/health`).then(r => r.json()).catch(() => null);
|
||
ok("actuator/health UP", health && health.status === "UP");
|
||
|
||
// 2. 基础
|
||
const info = await api("GET", "/api/base/GetUserInfo");
|
||
ok("GetUserInfo code=0", info.code === 0, JSON.stringify(info).substring(0, 80));
|
||
const mods = await api("GET", "/api/base/GetUserMod");
|
||
ok("GetUserMod code=0", mods.code === 0);
|
||
|
||
// 3. 校区
|
||
const stamp = Date.now().toString(36);
|
||
const campusName = `冒烟校区${stamp}`;
|
||
const cAdd = await api("POST", "/api/campus/Add", { form: { campusName, sortOrder: 99 } });
|
||
ok("campus Add", cAdd.code === 0 && cAdd.data > 0, cAdd.msg);
|
||
const campusId = cAdd.data;
|
||
const cDup = await api("POST", "/api/campus/Add", { form: { campusName } });
|
||
ok("campus Add 重名拒绝", cDup.code !== 0);
|
||
const cList = await api("GET", "/api/campus/List");
|
||
ok("campus List 含新校区", cList.code === 0 && cList.data.some(c => c.serID === campusId));
|
||
|
||
// 4. 教室类型
|
||
const typeName = `冒烟类型${stamp}`;
|
||
const tAdd = await api("POST", "/api/classtype/Add", { form: { typeName } });
|
||
ok("classtype Add", tAdd.code === 0, tAdd.msg);
|
||
const typeId = tAdd.data;
|
||
|
||
// 5. 楼栋
|
||
const bAdd = await api("POST", "/api/building/Add", { form: { campusId, buildingName: `冒烟楼栋${stamp}`, buildingCode: `S${stamp.slice(-3)}`, floorCount: 2 } });
|
||
ok("building Add", bAdd.code === 0, bAdd.msg);
|
||
const buildingId = bAdd.data;
|
||
|
||
// 6. 教室批量添加(带设备)+ 列表
|
||
const eqSeed = await api("POST", "/api/equipment/Add", { form: { equipmentName: `冒烟设备${stamp}` } });
|
||
ok("equipment Add(独立台账)", eqSeed.code === 0, eqSeed.msg);
|
||
const eqSeedId = eqSeed.data;
|
||
const eqSeedDup = await api("POST", "/api/equipment/Add", { form: { equipmentName: `冒烟设备${stamp}` } });
|
||
ok("equipment Add 校内重名拒绝", eqSeedDup.code !== 0);
|
||
|
||
const batch = await api("POST", "/api/classroom/BatchAdd", { json: { buildingId, floorFrom: 1, floorTo: 2, countPerFloor: 3, typeId, equipments: [{ equipmentId: eqSeedId, quantity: 2 }] } });
|
||
ok("classroom BatchAdd 6 间(带设备,每间数量 2)", batch.code === 0 && batch.data.created === 6, batch.msg);
|
||
const batchBad = await api("POST", "/api/classroom/BatchAdd", { json: { buildingId, floorFrom: 1, floorTo: 9, countPerFloor: 1, typeId } });
|
||
ok("BatchAdd 楼层超限拒绝", batchBad.code !== 0);
|
||
const rooms = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||
ok("classroom List 6 间", rooms.code === 0 && rooms.data.total === 6, `total=${rooms.data && rooms.data.total}`);
|
||
const room1 = rooms.data.list[0].serID;
|
||
ok("教室行含设备关联(数量 2,名称×2)", rooms.data.list.every(r => Array.isArray(r.equipmentIds) && r.equipmentIds.includes(eqSeedId) && r.equipmentQtys && r.equipmentQtys[eqSeedId] === 2 && /冒烟设备.*×2$/.test(r.equipmentNames)), JSON.stringify(rooms.data.list[0] && rooms.data.list[0].equipmentNames));
|
||
const floors = await api("GET", "/api/classroom/Floors", { params: { buildingId } });
|
||
ok("classroom Floors [1,2]", floors.code === 0 && JSON.stringify(floors.data) === "[1,2]");
|
||
const bStats = await api("GET", "/api/building/Stats", { params: { campusId } });
|
||
ok("building Stats total=6", bStats.code === 0 && bStats.data.some(b => b.serID === buildingId && b.total === 6));
|
||
|
||
// 7. 教室编辑(含设备全量替换)+ 编号守卫
|
||
const edit = await api("POST", "/api/classroom/Edit", { json: { serID: room1, roomName: `冒烟教室${stamp}`, status: "closed", seatCount: 45, equipmentIds: [] } });
|
||
ok("classroom Edit(清空设备)", edit.code === 0, edit.msg);
|
||
const roomsAfterEdit = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||
const room1After = roomsAfterEdit.data.list.find(r => r.serID === room1);
|
||
ok("Edit 后教室设备已清空", room1After && room1After.equipmentIds.length === 0);
|
||
const editQty = await api("POST", "/api/classroom/Edit", { json: { serID: room1, equipments: [{ equipmentId: eqSeedId, quantity: 3 }] } });
|
||
ok("classroom Edit equipments 按数量替换", editQty.code === 0, editQty.msg);
|
||
const roomsAfterQty = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||
const room1Qty = roomsAfterQty.data.list.find(r => r.serID === room1);
|
||
ok("Edit 后设备数量为 3(名称×3)", room1Qty && room1Qty.equipmentQtys && room1Qty.equipmentQtys[eqSeedId] === 3 && /冒烟设备.*×3$/.test(room1Qty.equipmentNames), JSON.stringify(room1Qty && room1Qty.equipmentQtys));
|
||
const editClear = await api("POST", "/api/classroom/Edit", { json: { serID: room1, equipmentIds: [] } });
|
||
ok("classroom Edit 旧字段 equipmentIds 清空设备(兼容)", editClear.code === 0, editClear.msg);
|
||
const room2code = roomsAfterEdit.data.list[1].roomCode;
|
||
const editDup = await api("POST", "/api/classroom/Edit", { json: { serID: roomsAfterEdit.data.list[1].serID, roomCode: roomsAfterEdit.data.list[2].roomCode } });
|
||
ok("Edit 楼栋内编号重复拒绝", editDup.code !== 0);
|
||
|
||
// 8. 设备编辑 + 引用删除守卫(设备被教室引用时不可删)
|
||
const eqList = await api("GET", "/api/equipment/List");
|
||
ok("equipment List 含引用数与统计数量(5 间 × 每间 2 = 10)", eqList.code === 0 && eqList.data.list.some(e => e.serID === eqSeedId && e.classroomCount === 5 && e.quantity === 10), JSON.stringify(eqList.data && eqList.data.list.find(e => e.serID === eqSeedId)));
|
||
const eqEdit = await api("POST", "/api/equipment/Edit", { form: { serID: eqSeedId, equipmentName: `冒烟设备${stamp}` } });
|
||
ok("equipment Edit", eqEdit.code === 0, eqEdit.msg);
|
||
const eqDelGuarded = await api("POST", "/api/equipment/Delete", { form: { serid: eqSeedId } });
|
||
ok("equipment Delete 被引用守卫拒绝", eqDelGuarded.code !== 0);
|
||
|
||
// 9. 新增接口:列表排序 / 楼栋楼层分布 / 教室批量编辑
|
||
const sortedDesc = await api("GET", "/api/classroom/List", {
|
||
params: { buildingId, page: 1, count: 20, sortField: "seatCount", order: "desc" },
|
||
});
|
||
ok(
|
||
"List 排序 seatCount desc",
|
||
sortedDesc.code === 0 &&
|
||
sortedDesc.data.list[0].seatCount >= sortedDesc.data.list[sortedDesc.data.list.length - 1].seatCount,
|
||
JSON.stringify(sortedDesc.data && sortedDesc.data.list.map(r => r.seatCount))
|
||
);
|
||
|
||
const sortedFloorDesc = await api("GET", "/api/classroom/List", {
|
||
params: { buildingId, page: 1, count: 20, sortField: "floor", order: "desc" },
|
||
});
|
||
ok("List 排序 floor desc 首行为 2 层", sortedFloorDesc.code === 0 && sortedFloorDesc.data.list[0].floor === 2);
|
||
|
||
const defaultOrder = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20 } });
|
||
ok(
|
||
"List 不传排序参数保持默认序(楼层升序 + 编号升序)",
|
||
defaultOrder.code === 0 &&
|
||
defaultOrder.data.list[0].floor === 1 &&
|
||
defaultOrder.data.list[0].roomCode <= defaultOrder.data.list[1].roomCode
|
||
);
|
||
|
||
const bStatsFloors = await api("GET", "/api/building/Stats", { params: { campusId } });
|
||
const mineBuilding = bStatsFloors.data && bStatsFloors.data.find(b => b.serID === buildingId);
|
||
ok(
|
||
"building Stats 新增 floors 楼层分布(2 层,每层 3 间)",
|
||
mineBuilding &&
|
||
Array.isArray(mineBuilding.floors) &&
|
||
mineBuilding.floors.length === 2 &&
|
||
mineBuilding.floors[0].floor === 1 &&
|
||
mineBuilding.floors[0].total === 3 &&
|
||
mineBuilding.floors[1].floor === 2,
|
||
JSON.stringify(mineBuilding && mineBuilding.floors)
|
||
);
|
||
|
||
// 9.5 空间作用域查询(教室管理三栏布局):按校区 List + Tree 扁平数据
|
||
const byCampus = await api("GET", "/api/classroom/List", { params: { campusId, page: 1, count: 20 } });
|
||
ok(
|
||
"List 按校区作用域命中 6 间",
|
||
byCampus.code === 0 && byCampus.data.total === 6,
|
||
`total=${byCampus.data && byCampus.data.total}`
|
||
);
|
||
ok(
|
||
"List 行含校区/楼栋名称",
|
||
byCampus.data.list.every(r => r.campusName === campusName && r.buildingName === `冒烟楼栋${stamp}`),
|
||
JSON.stringify(byCampus.data.list[0])
|
||
);
|
||
const byCampusFloor = await api("GET", "/api/classroom/List", { params: { campusId, floor: 1, page: 1, count: 20 } });
|
||
ok("List 校区+楼层作用域命中 3 间", byCampusFloor.code === 0 && byCampusFloor.data.total === 3);
|
||
ok("List 校区/楼栋均缺拒绝", (await api("GET", "/api/classroom/List", { params: { page: 1, count: 20 } })).code !== 0);
|
||
const treeRows = await api("GET", "/api/classroom/Tree");
|
||
const myTreeRows = treeRows.data ? treeRows.data.filter(r => r.buildingId === buildingId) : [];
|
||
ok(
|
||
"Tree 扁平数据含本楼栋 6 间(轻量字段)",
|
||
treeRows.code === 0 && myTreeRows.length === 6 && myTreeRows.every(r => r.campusId === campusId && r.roomCode && r.status),
|
||
JSON.stringify(myTreeRows[0])
|
||
);
|
||
|
||
// 9.7 绑定班级:class 档案 → BindClass/UnbindClass/BatchUnbind → 年级 Tab 过滤
|
||
const clsNameA = `冒烟高一班${stamp}`;
|
||
const clsNameB = `冒烟高二班${stamp}`;
|
||
const clsAddA = await api("POST", "/api/class/Add", { form: { className: clsNameA, grade: 1, studentCount: 45, headTeacher: "王老师" } });
|
||
ok("class Add(高一)", clsAddA.code === 0 && clsAddA.data > 0, clsAddA.msg);
|
||
const clsAddB = await api("POST", "/api/class/Add", { form: { className: clsNameB, grade: 2, studentCount: 44, headTeacher: "陈老师" } });
|
||
ok("class Add(高二)", clsAddB.code === 0 && clsAddB.data > 0, clsAddB.msg);
|
||
ok("class Add 校内重名拒绝", (await api("POST", "/api/class/Add", { form: { className: clsNameA, grade: 1 } })).code !== 0);
|
||
ok("class Add 非法年级拒绝", (await api("POST", "/api/class/Add", { form: { className: `冒烟班X${stamp}`, grade: 9 } })).code !== 0);
|
||
const clsListG1 = await api("GET", "/api/class/List", { params: { grade: 1 } });
|
||
const clsA = clsListG1.data ? clsListG1.data.find(c => c.serID === clsAddA.data) : null;
|
||
ok("class List grade=1 含新班级(未绑定 classroomId=null)", clsListG1.code === 0 && clsA && clsA.classroomId == null && clsA.headTeacher === "王老师", JSON.stringify(clsA));
|
||
|
||
const bind = await api("POST", "/api/classroom/BindClass", { json: { serID: room1, classId: clsAddA.data } });
|
||
ok("classroom BindClass 返回班级名", bind.code === 0 && bind.data && bind.data.className === clsNameA, bind.msg);
|
||
const bindDup = await api("POST", "/api/classroom/BindClass", { json: { serID: rooms.data.list[1].serID, classId: clsAddA.data } });
|
||
ok("BindClass 班级已被其他教室占用拒绝", bindDup.code !== 0);
|
||
ok("BindClass 缺参拒绝", (await api("POST", "/api/classroom/BindClass", { json: { serID: room1 } })).code !== 0);
|
||
|
||
const roomsG1 = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 1, page: 1, count: 20 } });
|
||
const boundRow = roomsG1.data ? roomsG1.data.list.find(r => r.serID === room1) : null;
|
||
ok(
|
||
"List grade=1 含已绑教室且行含班级信息(名/人数/班主任)",
|
||
roomsG1.code === 0 && roomsG1.data.total === 6 && boundRow && boundRow.classId === clsAddA.data &&
|
||
boundRow.className === clsNameA && boundRow.studentCount === 45 && boundRow.headTeacher === "王老师",
|
||
JSON.stringify(boundRow)
|
||
);
|
||
const roomsG2 = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 2, page: 1, count: 20 } });
|
||
ok(
|
||
"List grade=2 排除高一已绑定教室(6-1=5,其余未绑定)",
|
||
roomsG2.code === 0 && roomsG2.data.total === 5 && roomsG2.data.list.every(r => r.serID !== room1 && r.classId == null),
|
||
`total=${roomsG2.data && roomsG2.data.total}`
|
||
);
|
||
const roomsBoundOnly = await api("GET", "/api/classroom/List", { params: { buildingId, grade: 1, bindStatus: "bound", page: 1, count: 20 } });
|
||
ok("List bindStatus=bound 只含已绑 1 间", roomsBoundOnly.code === 0 && roomsBoundOnly.data.total === 1);
|
||
|
||
ok("class Delete 绑定中拒绝", (await api("POST", "/api/class/Delete", { form: { serid: clsAddA.data } })).code !== 0);
|
||
|
||
ok("classroom UnbindClass", (await api("POST", "/api/classroom/UnbindClass", { form: { serid: room1 } })).code === 0);
|
||
ok("UnbindClass 未绑定教室静默成功", (await api("POST", "/api/classroom/UnbindClass", { form: { serid: room1 } })).code === 0);
|
||
|
||
// 批量绑定两间 → 批量解绑 3 间(含 1 间未绑定 → skipped)
|
||
await api("POST", "/api/classroom/BindClass", { json: { serID: room1, classId: clsAddA.data } });
|
||
await api("POST", "/api/classroom/BindClass", { json: { serID: rooms.data.list[1].serID, classId: clsAddB.data } });
|
||
const bUnbind = await api("POST", "/api/classroom/BatchUnbind", { json: [room1, rooms.data.list[1].serID, rooms.data.list[2].serID] });
|
||
ok(
|
||
"BatchUnbind 3 间(updated=2, skipped=1)",
|
||
bUnbind.code === 0 && bUnbind.data.updated === 2 && bUnbind.data.skipped === 1,
|
||
JSON.stringify(bUnbind.data)
|
||
);
|
||
ok("BatchUnbind 空集合拒绝", (await api("POST", "/api/classroom/BatchUnbind", { json: [] })).code !== 0);
|
||
ok("class Delete 解绑后可删 A", (await api("POST", "/api/class/Delete", { form: { serid: clsAddA.data } })).code === 0);
|
||
ok("class Delete 解绑后可删 B", (await api("POST", "/api/class/Delete", { form: { serid: clsAddB.data } })).code === 0);
|
||
|
||
// 菜单兜底(仅本地免 SSO 模式):绑定班级在第二位
|
||
if (!COOKIE) {
|
||
const modsTree = await api("GET", "/api/base/GetUserMod");
|
||
const names = (modsTree.data || []).map(m => m.name);
|
||
ok(
|
||
"GetUserMod 菜单序 教室管理→绑定班级→基础设置",
|
||
modsTree.code === 0 && JSON.stringify(names) === JSON.stringify(["教室管理", "绑定班级", "基础设置"]),
|
||
JSON.stringify(names)
|
||
);
|
||
}
|
||
|
||
const allIds = roomsAfterEdit.data.list.map(r => r.serID);
|
||
const bEditStatus = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: allIds, status: "closed" } });
|
||
ok("classroom BatchEdit 批量关闭 6 间", bEditStatus.code === 0 && bEditStatus.data.updated === 6, bEditStatus.msg);
|
||
|
||
const afterBatch = await api("GET", "/api/classroom/List", { params: { buildingId, page: 1, count: 20, status: "closed" } });
|
||
ok("BatchEdit 后按 closed 过滤命中 6 间", afterBatch.code === 0 && afterBatch.data.total === 6);
|
||
|
||
const bEditType = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: allIds.slice(0, 2), typeId } });
|
||
ok("classroom BatchEdit 批量改类型 2 间", bEditType.code === 0 && bEditType.data.updated === 2, bEditType.msg);
|
||
|
||
ok("BatchEdit 空集合拒绝", (await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [] } })).code !== 0);
|
||
ok("BatchEdit 未指定修改项拒绝", (await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1] } })).code !== 0);
|
||
ok(
|
||
"BatchEdit 非法状态拒绝",
|
||
(await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1], status: "xxx" } })).code !== 0
|
||
);
|
||
const bEditSkip = await api("POST", "/api/classroom/BatchEdit", { json: { serIDs: [room1, 99999999], status: "open" } });
|
||
ok(
|
||
"BatchEdit 不存在 id 计入 skipped(不整批失败)",
|
||
bEditSkip.code === 0 && bEditSkip.data.updated === 1 && bEditSkip.data.skipped === 1,
|
||
JSON.stringify(bEditSkip.data)
|
||
);
|
||
|
||
// 10. 其余删除守卫(楼栋有教室 → 类型被引用 → 校区有楼栋)
|
||
const delBuilding = await api("POST", "/api/building/Delete", { form: { serid: buildingId } });
|
||
ok("building Delete 守卫拒绝", delBuilding.code !== 0);
|
||
const delType = await api("POST", "/api/classtype/Delete", { form: { serid: typeId } });
|
||
ok("classtype Delete 守卫拒绝", delType.code !== 0);
|
||
const delCampus = await api("POST", "/api/campus/Delete", { form: { serid: campusId } });
|
||
ok("campus Delete 守卫拒绝", delCampus.code !== 0);
|
||
|
||
// 11. 逆序清理:教室(关联随删)→ 楼栋 → 类型 → 设备 → 校区
|
||
const delRooms = await api("POST", "/api/classroom/Delete", { json: allIds });
|
||
ok("classroom Delete 全部(关联自动清理)", delRooms.code === 0, delRooms.msg);
|
||
ok("equipment Delete(解除引用后可删)", (await api("POST", "/api/equipment/Delete", { form: { serid: eqSeedId } })).code === 0);
|
||
ok("building Delete", (await api("POST", "/api/building/Delete", { form: { serid: buildingId } })).code === 0);
|
||
ok("classtype Delete", (await api("POST", "/api/classtype/Delete", { form: { serid: typeId } })).code === 0);
|
||
ok("campus Delete", (await api("POST", "/api/campus/Delete", { form: { serid: campusId } })).code === 0);
|
||
|
||
console.log(`\n[smoke] 通过 ${passed} / ${passed + failed}`);
|
||
process.exit(failed ? 1 : 0);
|