commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
#!/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);
|
||||
})();
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 宿舍通知(DormitoryNotice)后端 P8 端到端冒烟脚本。
|
||||
*
|
||||
* 背景:/api/* 受基础平台 CAS/SSO 过滤器保护,无登录态会返回 302→SSO,脚本无法自证身份。
|
||||
* 因此用「浏览器登录后复制 Cookie」的方式携带会话:
|
||||
* 1) 起后端:先跑 sql/dormnotice_init.sql 建库+字典,再 `java -jar target/dormnotice.jar`
|
||||
* (或 IDEA 运行 DormNoticeApplication,端口 8105)。
|
||||
* 2) 浏览器用教师账号 lijing 走 SSO 登录前端(http://localhost:8078 或后端直连),
|
||||
* 打开 DevTools→Network→任一 /api 请求,复制其「Cookie」请求头整串值。
|
||||
* 3) 同样用学生账号(kegemo / csxs1)登录,复制一份学生 Cookie。
|
||||
* 4) 运行:
|
||||
* TEACHER_COOKIE='<粘贴教师Cookie>' STUDENT_COOKIE='<粘贴学生Cookie>' \
|
||||
* node smoke/smoke.mjs
|
||||
* 可选:BASE(默认 http://localhost:8105)。
|
||||
*
|
||||
* 只读为主;Create 冒烟会创建一条草稿再删除,不留脏数据。退出码非 0 表示有必过项失败。
|
||||
*/
|
||||
|
||||
const BASE = (process.env.BASE || "http://localhost:8105").replace(/\/+$/, "");
|
||||
const TEACHER = process.env.TEACHER_COOKIE || "";
|
||||
const STU = process.env.STUDENT_COOKIE || "";
|
||||
// 写链路默认执行(用一次性数据并自清理);设 WRITE=0 跳过所有增删改,仅跑只读冒烟。
|
||||
const WRITE = (process.env.WRITE || "1") !== "0";
|
||||
|
||||
const results = [];
|
||||
let failed = 0;
|
||||
|
||||
function hdr(cookie, extra = {}) {
|
||||
const h = { Accept: "application/json", ...extra };
|
||||
if (cookie) h.Cookie = cookie;
|
||||
return h;
|
||||
}
|
||||
|
||||
async function req(label, method, path, { cookie = TEACHER, body, form, json = true, must = true } = {}) {
|
||||
const url = BASE + path;
|
||||
const headers = hdr(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(); }
|
||||
let status, data, raw;
|
||||
try {
|
||||
const resp = await fetch(url, { method, headers, body: payload, redirect: "manual" });
|
||||
status = resp.status;
|
||||
raw = await resp.text();
|
||||
if (json) { try { data = JSON.parse(raw); } catch { data = null; } }
|
||||
} catch (e) {
|
||||
push(label, false, `网络错误 ${e.message}`, must);
|
||||
return null;
|
||||
}
|
||||
if (status === 302 || status === 301) {
|
||||
push(label, false, `${status}→SSO 重定向:Cookie 无效/未登录`, must);
|
||||
return null;
|
||||
}
|
||||
push(label, status === 200 && (!json || (data && data.code === 0)), `HTTP ${status} code=${data && data.code} msg=${data && data.msg}`, must);
|
||||
return data ? data.data : raw;
|
||||
}
|
||||
|
||||
function push(name, ok, detail, must) {
|
||||
results.push({ name, ok, detail, must });
|
||||
if (!ok && must) failed++;
|
||||
console.log(`${ok ? "PASS" : "FAIL"} ${name} — ${detail}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`\n=== DormNotice 冒烟 @ ${BASE} ===`);
|
||||
if (!TEACHER) { console.log("⚠ 未提供 TEACHER_COOKIE,鉴权类接口会 302。请先按脚本头注释获取。"); }
|
||||
|
||||
// 0. 免鉴权健康(actuator 返回 {status:UP},非 {code} 信封,只验 HTTP 200)
|
||||
await req("actuator/health", "GET", "/actuator/health", { must: false, json: false });
|
||||
|
||||
// 1. 平台代理
|
||||
await req("base/GetUserInfo", "GET", "/api/base/GetUserInfo");
|
||||
await req("base/GetUserMod", "GET", "/api/base/GetUserMod");
|
||||
await req("base/GetDormTree", "GET", "/api/base/GetDormTree", { must: false });
|
||||
await req("base/SetRole(parent)", "POST", "/api/base/SetRole", { cookie: STU || TEACHER, body: { as: "parent" }, must: false });
|
||||
|
||||
// 2. 字典(B2:type 参数化 + 无“文字重复”)
|
||||
const types = await req("dict/List?noticeType", "GET", "/api/dict/List?type=noticeType");
|
||||
const depts = await req("dict/ManageList?dept", "GET", "/api/dict/ManageList?type=dept");
|
||||
await req("dict/Sync", "GET", "/api/dict/Sync", { must: false });
|
||||
// 断言无逗号重复名
|
||||
if (Array.isArray(types)) {
|
||||
const dup = types.some(t => String(t.v || t.k || "").includes(","));
|
||||
push("字典名无“X,X”重复", !dup, dup ? "存在含逗号字典名,检查 B1 是否真后端未装" : "OK", true);
|
||||
}
|
||||
|
||||
// 3. 宿舍域
|
||||
const buildings = await req("dorm/BuildingList", "GET", "/api/dorm/BuildingList", { must: false });
|
||||
await req("dorm/RoomList", "GET", "/api/dorm/RoomList", { must: false });
|
||||
await req("dorm/PendingStudents", "GET", "/api/dorm/PendingStudents", { must: false });
|
||||
|
||||
// 4. 通知:FormTemplates / SendList / Create(草稿)→Detail→Delete
|
||||
await req("notice/FormTemplates", "GET", "/api/notice/FormTemplates");
|
||||
await req("notice/SendList", "GET", "/api/notice/SendList?pageIndex=1&pageSize=10");
|
||||
await req("notice/Statis", "GET", "/api/notice/Statis", { must: false });
|
||||
|
||||
const deptCode = Array.isArray(depts) && depts[0] ? depts[0].CodeID : "D002";
|
||||
const receiveList = Array.isArray(buildings) && buildings[0] ? String(buildings[0].buildingId * 10000) : "ALL";
|
||||
// 4b. 通知写链路:Create(草稿) → Edit 改标题 → GetDetail 校验 → Delete 自清理
|
||||
if (WRITE && TEACHER) {
|
||||
const nid = await req("notice/Create(draft)", "POST", "/api/notice/Create", {
|
||||
body: { title: "【冒烟】通知A", content: "<p>smoke</p>", noticeType: "其他",
|
||||
level: "normal", receiptMode: "none", deptId: deptCode, receiveType: "room",
|
||||
receiveList, draft: true },
|
||||
});
|
||||
if (nid) {
|
||||
await req("notice/Edit(改标题)", "POST", "/api/notice/Edit", { body: { id: nid, title: "【冒烟】通知B", receiptMode: "none" } });
|
||||
const d = await req("notice/GetDetail(校验)", "GET", `/api/notice/GetDetail?serid=${nid}`);
|
||||
push("notice/Edit 生效(标题→B)", !!(d && /通知B/.test(String(d.title || ""))), `title=${d && d.title}`, false);
|
||||
await req("notice/Delete(cleanup)", "POST", "/api/notice/Delete", { body: [nid] });
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 家长端(学生 Cookie)
|
||||
await req("notice/List(parent)", "GET", "/api/notice/List?pageIndex=1&pageSize=20", { cookie: STU, must: false });
|
||||
await req("notice/MyNotices(parent)", "GET", "/api/notice/MyNotices", { cookie: STU, must: false });
|
||||
|
||||
// 6. 写链路自清理(用一次性数据,跑完即删;设 WRITE=0 跳过)
|
||||
if (WRITE && TEACHER) {
|
||||
// 6.1 字典:Add(noticeType) → ManageList 查得 → Delete
|
||||
const tn = "冒烟类型" + Date.now();
|
||||
await req("dict/Add(noticeType)", "POST", "/api/dict/Add", { form: { name: tn, type: "noticeType" } });
|
||||
const ml = await req("dict/ManageList(noticeType)", "GET", "/api/dict/ManageList?type=noticeType", { must: false });
|
||||
const dictItem = Array.isArray(ml) ? ml.find(x => x.Name === tn) : null;
|
||||
push("dict/Add→ManageList 可查得", !!dictItem, dictItem ? "SerID=" + dictItem.SerID : "未找到新增项", false);
|
||||
if (dictItem) await req("dict/Delete(cleanup)", "POST", "/api/dict/Delete", { form: { serid: dictItem.SerID, type: "noticeType" } });
|
||||
|
||||
// 6.2 联系组:AddGroup → GetGroup 查得 → DelGroup
|
||||
const gn = "冒烟组" + Date.now();
|
||||
await req("contacts/AddGroup", "POST", "/api/contacts/AddGroup", { form: { name: gn } });
|
||||
const gp = await req("contacts/GetGroup", "GET", "/api/contacts/GetGroup?page=1&count=200", { must: false });
|
||||
const grp = gp && Array.isArray(gp.list) ? gp.list.find(x => x.GroupName === gn) : null;
|
||||
push("contacts/AddGroup→GetGroup 可查得", !!grp, grp ? "serid=" + grp.serid : "未找到", false);
|
||||
if (grp) await req("contacts/DelGroup(cleanup)", "POST", "/api/contacts/DelGroup", { form: { ids: grp.serid } });
|
||||
|
||||
// 6.3 宿舍:AddBuilding → BuildingList 查得 → AddRoom → RoomList 查得 → DelRoom → DelBuilding
|
||||
const bn = "冒烟楼" + Date.now();
|
||||
await req("dorm/AddBuilding", "POST", "/api/dorm/AddBuilding", { form: { buildingName: bn, gender: "混合", floorCount: 1, roomsPerFloor: 1, bedsPerRoom: 2 } });
|
||||
const bl = await req("dorm/BuildingList", "GET", "/api/dorm/BuildingList", { must: false });
|
||||
const b = Array.isArray(bl) ? bl.find(x => x.buildingName === bn) : null;
|
||||
push("dorm/AddBuilding→BuildingList 可查得", !!b, b ? "buildingId=" + b.buildingId : "未找到", false);
|
||||
if (b) {
|
||||
await req("dorm/AddRoom", "POST", "/api/dorm/AddRoom", { form: { buildingId: b.buildingId, floor: 1, roomNo: "901", bedCount: 2 } });
|
||||
const rl = await req("dorm/RoomList", "GET", "/api/dorm/RoomList?buildingId=" + b.buildingId, { must: false });
|
||||
const room = Array.isArray(rl) ? rl.find(x => x.roomNo === "901") : null;
|
||||
push("dorm/AddRoom→RoomList 可查得", !!room, room ? "roomId=" + room.roomId : "未找到", false);
|
||||
if (room) await req("dorm/DelRoom(cleanup)", "POST", "/api/dorm/DelRoom", { form: { roomId: room.roomId } });
|
||||
await req("dorm/DelBuilding(cleanup)", "POST", "/api/dorm/DelBuilding", { form: { buildingId: b.buildingId } });
|
||||
}
|
||||
} else {
|
||||
console.log("SKIP 写链路(WRITE=0 或缺 TEACHER_COOKIE)");
|
||||
}
|
||||
|
||||
console.log("\n=== 汇总 ===");
|
||||
const pass = results.filter(r => r.ok).length;
|
||||
console.log(`${pass}/${results.length} 通过;必过失败 ${failed} 项`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error("脚本异常:", e); process.exit(2); });
|
||||
Reference in New Issue
Block a user