Files
2026-09-20 09:03:44 +08:00

125 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
/**
* 宿舍通知 家长端闭环自清理测试(P8 深测)。
* 教师建临时楼/房 → 把登录学生分配到该房 → 发布 confirm + fill 两条通知到该房 →
* 学生 MyNotice(自动确认)/SubmitReceipt(填报) → 教师 Receipts/Statis 校验 →
* finally 逆序彻底清理:删通知(级联删回执) → 学生退宿 → 删房间 → 删楼栋。
*
* 用法:TEACHER_COOKIE='…' STUDENT_COOKIE='…' [STUDENT_ID=10069] node smoke/closed-loop.mjs
*/
const BASE = (process.env.BASE || "http://localhost:8105").replace(/\/+$/, "");
const T = process.env.TEACHER_COOKIE || "";
const S = process.env.STUDENT_COOKIE || "";
const SID = Number(process.env.STUDENT_ID || 10069);
if (!T || !S) { console.error("需 TEACHER_COOKIE 与 STUDENT_COOKIE"); process.exit(2); }
const stamp = Date.now();
const BLD = "闭环测试楼" + stamp;
let buildingId = null, roomId = null, noticeConfirm = null, noticeFill = null;
let assigned = false;
const log = (ok, name, detail = "") => console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
async function call(cookie, method, path, { body, form } = {}) {
const headers = { Accept: "application/json", Cookie: cookie };
let payload;
if (body !== undefined) { headers["Content-Type"] = "application/json;charset=UTF-8"; payload = JSON.stringify(body); }
else if (form !== undefined) { headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"; payload = new URLSearchParams(form).toString(); }
const r = await fetch(BASE + path, { method, headers, body: payload, redirect: "manual" });
const txt = await r.text();
let j = null; try { j = JSON.parse(txt); } catch { /* non-json */ }
if (r.status !== 200 || !j || j.code !== 0) throw new Error(`${method} ${path} → HTTP ${r.status} ${txt.slice(0, 160)}`);
return j.data;
}
async function setup() {
// 楼栋
await call(T, "POST", "/api/dorm/AddBuilding", { form: { buildingName: BLD, gender: "混合", floorCount: 1, roomsPerFloor: 1, bedsPerRoom: 2 } });
const bl = await call(T, "GET", "/api/dorm/BuildingList");
buildingId = (bl.find(x => x.buildingName === BLD) || {}).buildingId;
if (!buildingId) throw new Error("楼栋未创建成功");
log(true, "dorm/AddBuilding", "buildingId=" + buildingId);
// 房间
await call(T, "POST", "/api/dorm/AddRoom", { form: { buildingId, floor: 1, roomNo: "998", bedCount: 2 } });
const rl = await call(T, "GET", "/api/dorm/RoomList?buildingId=" + buildingId);
roomId = (rl.find(x => x.roomNo === "998") || {}).roomId;
if (!roomId) throw new Error("房间未创建成功");
log(true, "dorm/AddRoom", "roomId=" + roomId);
// 分配学生(需在 dorm_student 池内)
await call(T, "POST", "/api/dorm/AssignStudent", { form: { roomId, studentIds: String(SID) } });
assigned = true;
const rs = await call(T, "GET", "/api/dorm/RoomStudents?roomId=" + roomId);
log(rs.some(s => s.studentId === SID), "dorm/AssignStudent", "房内=" + JSON.stringify(rs.map(s => s.studentId)));
}
async function run() {
// confirm 通知
noticeConfirm = await call(T, "POST", "/api/notice/Create", {
body: { title: "【闭环】需确认通知" + stamp, content: "<p>confirm</p>", noticeType: "安全排查", level: "important",
receiptMode: "confirm", deptId: "D001", receiveType: "room", receiveList: String(roomId), needSupervisor: 1, draft: false } });
log(!!noticeConfirm, "notice/Create(confirm)", "id=" + noticeConfirm);
// fill 通知
noticeFill = await call(T, "POST", "/api/notice/Create", {
body: { title: "【闭环】需填报通知" + stamp, content: "<p>fill</p>", noticeType: "留宿申请", level: "normal",
receiptMode: "fill", formType: "stay", deptId: "D001", receiveType: "room", receiveList: String(roomId), needSupervisor: 0, draft: false } });
log(!!noticeFill, "notice/Create(fill/stay)", "id=" + noticeFill);
// 学生收件箱应见两条
const mn = await call(S, "GET", "/api/notice/MyNotices");
const ids = (mn.list || []).map(x => x.id);
log(ids.includes(noticeConfirm) && ids.includes(noticeFill), "notice/MyNotices(学生可见两条)", "buckets=" + JSON.stringify(mn.buckets));
// 打开 confirm 通知 → 自动确认
const mc = await call(S, "GET", "/api/notice/MyNotice?serid=" + noticeConfirm);
log(mc.my && mc.my.status === "confirmed", "notice/MyNotice(confirm 打开即自动确认)", "status=" + (mc.my && mc.my.status));
// 填报 fill 通知
const sr = await call(S, "POST", "/api/notice/SubmitReceipt", {
body: { serid: noticeFill, guardianName: "测试家长", guardianPhone: "13900000000", relation: "父亲",
fillData: { conclusion: "同意", stayStart: "2026-10-01", stayEnd: "2026-10-03", stayCount: "1", stayReason: "家远不便往返" },
remark: "闭环自测" } });
log(sr && sr.status === "filled", "notice/SubmitReceipt(fill)", "status=" + (sr && sr.status));
// 教师看回执名单
const rc = await call(T, "GET", "/api/notice/Receipts?serid=" + noticeFill + "&status=all");
log(rc.summary.should >= 1 && rc.summary.filled >= 1, "notice/Receipts(fill 汇总)", "should=" + rc.summary.should + " filled=" + rc.summary.filled + " rate=" + rc.summary.receiptRate + "%");
const rc2 = await call(T, "GET", "/api/notice/Receipts?serid=" + noticeConfirm + "&status=all");
log(rc2.summary.confirmed >= 1, "notice/Receipts(confirm 汇总)", "confirmed=" + rc2.summary.confirmed);
// 阅读情况(房间级)
const rd = await call(T, "GET", "/api/notice/ReadDetail?serid=" + noticeConfirm);
log(Array.isArray(rd) && rd.length >= 1, "notice/ReadDetail", JSON.stringify(rd).slice(0, 120));
// 统计
const st = await call(T, "GET", "/api/notice/Statis");
log(st.publishedCount >= 2, "notice/Statis", "publishedCount=" + st.publishedCount + " totalReceipts=" + st.totalReceipts);
// 导出回执 CSV
const csv = await (await fetch(BASE + "/api/notice/ExportReceipts?serid=" + noticeFill, { headers: { Cookie: T } })).text();
log(/回执状态|留宿/.test(csv), "notice/ExportReceipts", "len=" + csv.length);
}
async function cleanup() {
const errs = [];
try { if (noticeConfirm || noticeFill) await call(T, "POST", "/api/notice/Delete", { body: [noticeConfirm, noticeFill].filter(Boolean) }); } catch (e) { errs.push("delNotice:" + e.message); }
try { if (assigned && roomId) await call(T, "POST", "/api/dorm/RemoveStudent", { form: { roomId, studentId: SID } }); } catch (e) { errs.push("removeStudent:" + e.message); }
try { if (roomId) await call(T, "POST", "/api/dorm/DelRoom", { form: { roomId } }); } catch (e) { errs.push("delRoom:" + e.message); }
try { if (buildingId) await call(T, "POST", "/api/dorm/DelBuilding", { form: { buildingId } }); } catch (e) { errs.push("delBuilding:" + e.message); }
// 校验清理干净
try {
const bl = await call(T, "GET", "/api/dorm/BuildingList");
const gone = !bl.some(x => x.buildingId === buildingId);
log(gone, "cleanup 楼栋已删", gone ? "" : "残留 buildingId=" + buildingId);
} catch (e) { errs.push("verify:" + e.message); }
if (errs.length) console.log("清理告警:", errs.join(" | "));
}
(async () => {
console.log(`\n=== 闭环自清理测试 @ ${BASE} · 学生 studentId=${SID} ===`);
let failed = 0;
try { await setup(); await run(); }
catch (e) { failed++; log(false, "流程异常", e.message); }
finally { console.log("\n--- 清理 ---"); await cleanup(); }
console.log(failed ? "\n结果:有失败" : "\n结果:闭环全部通过(数据已清理)");
process.exit(failed ? 1 : 0);
})();