commit
build / build-push (push) Failing after 2m7s

This commit is contained in:
wanghu
2026-10-20 09:13:14 +08:00
parent e3c0226e47
commit 0e75efa023
66 changed files with 4556 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
/**
* 教室管理后端冒烟脚本(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", capacity: 50, 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: "capacity", order: "desc" },
});
ok(
"List 排序 capacity desc",
sortedDesc.code === 0 &&
sortedDesc.data.list[0].capacity >= sortedDesc.data.list[sortedDesc.data.list.length - 1].capacity,
JSON.stringify(sortedDesc.data && sortedDesc.data.list.map(r => r.capacity))
);
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);