const http = require("http"); const fs = require("fs").promises; const path = require("path"); const crypto = require("crypto"); const rootDir = __dirname; const port = Number(process.env.PORT || 8787); const host = process.env.HOST || "0.0.0.0"; const sessions = new Map(); const customerTiers = ["A", "A+"]; function getInitialPassword(username) { const key = `KEYISSUE_${username.toUpperCase()}_PASSWORD`; if (process.env[key]) return process.env[key]; const digest = crypto.createHash("sha256").update(`${username}:${rootDir}`).digest("hex").slice(0, 12); return `keyissue-${digest}`; } function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) { const hash = crypto.pbkdf2Sync(String(password), salt, 120000, 32, "sha256").toString("hex"); return `pbkdf2:sha256:120000:${salt}:${hash}`; } function verifyPasswordHash(password, encodedHash) { const parts = String(encodedHash || "").split(":"); if (parts.length !== 5 || parts[0] !== "pbkdf2" || parts[1] !== "sha256") return false; const iterations = Number(parts[2]); const salt = parts[3]; const expected = parts[4]; if (!iterations || !salt || !expected) return false; const actual = crypto.pbkdf2Sync(String(password), salt, iterations, 32, "sha256").toString("hex"); const expectedBuffer = Buffer.from(expected, "hex"); const actualBuffer = Buffer.from(actual, "hex"); return expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer); } function setUserPassword(user, password) { user.passwordHash = hashPassword(password); delete user.password; } function verifyUserPassword(user, password) { if (!user) return false; if (user.passwordHash) return verifyPasswordHash(password, user.passwordHash); return user.password === password; } const pageCatalog = [ { key: "dashboard", name: "动态驾驶舱" }, { key: "customers", name: "A/A+客户库" }, { key: "inspections", name: "每日线上巡查" }, { key: "issues", name: "问题反馈池" }, { key: "audit", name: "监察核查" }, { key: "analysis", name: "问题分析" }, { key: "rules", name: "预警规则" }, { key: "admin", name: "系统后台" } ]; const permissionCatalog = [ { key: "issue:view", name: "查看问题详情" }, { key: "issue:create", name: "提交问题反馈" }, { key: "issue:assign", name: "指派责任人" }, { key: "issue:handle", name: "提交处置" }, { key: "inspection:view", name: "查看每日巡查" }, { key: "inspection:create", name: "提交每日巡查" }, { key: "audit:review", name: "监察核查" }, { key: "customer:submit", name: "提交客户入池申请" }, { key: "customer:approve", name: "审核客户入池" }, { key: "rules:edit", name: "保存预警规则" }, { key: "admin:manage", name: "系统后台管理" } ]; let roles = { admin: { roleName: "系统管理员", summary: "全量功能、规则配置、后台权限管理", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis", "rules", "admin"], permissions: ["*", "admin:manage"] }, executive: { roleName: "管理层", summary: "全局查看、督办、指派责任人", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"], permissions: ["issue:view", "issue:assign", "inspection:view", "customer:submit"] }, delivery: { roleName: "交付助理", summary: "提交每日巡查、问题反馈、查看客户与问题", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "inspection:view", "inspection:create", "customer:submit"] }, deliveryManager: { roleName: "交付经理", summary: "交付线全量查看、问题督办与客户入池提交", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"], permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, customerOwner: { roleName: "客管人员", summary: "仅查看本人负责客户、提交反馈与客户入池申请", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, customerManager: { roleName: "客管经理", summary: "查看本部门客管人员名下客户、督办反馈与客户入池提交", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, manager: { roleName: "项目经理", summary: "问题处置、提交巡查、提交核查、补充根因", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, designer: { roleName: "设计师", summary: "查看客户与问题、提交设计相关反馈、补充处置记录", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, designManager: { roleName: "设计经理", summary: "设计问题统筹、责任指派、处置跟进与客户入池提交", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, auditor: { roleName: "监察部", summary: "查看巡查、监察核查、通过或驳回整改", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"], permissions: ["issue:view", "audit:review", "inspection:view"] }, supplyChain: { roleName: "供应链", summary: "供应链相关岗位全量查看客户与问题反馈", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "inspection:view", "customer:submit"] } }; let users = [ { id: "U001", username: "admin", password: getInitialPassword("admin"), displayName: "肖总", role: "admin", status: "启用" }, { id: "U002", username: "executive", password: getInitialPassword("executive"), displayName: "管理层", role: "executive", status: "启用" }, { id: "U003", username: "delivery", password: getInitialPassword("delivery"), displayName: "交付助理", role: "delivery", status: "启用" }, { id: "U004", username: "manager", password: getInitialPassword("manager"), displayName: "项目经理", role: "manager", status: "启用" }, { id: "U005", username: "auditor", password: getInitialPassword("auditor"), displayName: "监察部", role: "auditor", status: "启用" }, { id: "U006", username: "designer", password: getInitialPassword("designer"), displayName: "设计师", role: "designer", status: "启用" }, { id: "U007", username: "design_manager", password: getInitialPassword("design_manager"), displayName: "设计经理", role: "designManager", status: "启用" } ]; let dictionaries = { departments: ["光宸", "澜光", "蘭堂", "焕境"], customerSources: ["转介绍", "公司特殊关系", "标杆项目", "风险关注"], issueSources: ["线下巡检", "客户群反馈", "每日线上巡查", "客户主动反馈", "管理层转办", "监察部发现"], issueTypes: ["施工质量", "设计变更", "沟通响应", "材料交付", "客户满意度"], progressStatuses: ["正常", "延期"] }; const requiredRoleDefaults = { executive: { roleName: "管理层", summary: "全局查看、督办、指派责任人", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"], permissions: ["issue:view", "issue:assign", "inspection:view", "customer:submit"] }, deliveryManager: { roleName: "交付经理", summary: "交付线全量查看、问题督办与客户入池提交", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"], permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, customerOwner: { roleName: "客管人员", summary: "仅查看本人负责客户、提交反馈与客户入池申请", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, customerManager: { roleName: "客管经理", summary: "查看本部门客管人员名下客户、督办反馈与客户入池提交", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, designer: { roleName: "设计师", summary: "查看客户与问题、提交设计相关反馈、补充处置记录", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, designManager: { roleName: "设计经理", summary: "设计问题统筹、责任指派、处置跟进与客户入池提交", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"] }, auditor: { roleName: "监察部", summary: "查看客户、巡查、监察核查、通过或驳回整改", pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"], permissions: ["issue:view", "audit:review", "inspection:view"] }, supplyChain: { roleName: "供应链", summary: "供应链相关岗位全量查看客户与问题反馈", pages: ["dashboard", "customers", "inspections", "issues", "analysis"], permissions: ["issue:view", "issue:create", "inspection:view", "customer:submit"] } }; const requiredUserDefaults = [ { username: "designer", displayName: "设计师", role: "designer" }, { username: "design_manager", displayName: "设计经理", role: "designManager" } ]; let rosterMembers = []; let customers = [ { id: "C001", dataTag: "测试", name: "肖女士", tier: "A+", source: "转介绍", keyCustomerReason: "业主具备持续转介绍潜力,当前木作节点需要重点口碑维护。", customerOwner: "客管负责人-罗敏", address: "阅花溪 9 栋 B 座 1702", department: "光宸", designer: "蔡菁", manager: "刘江新", stage: "木作安装", startDate: "2026-05-18", plannedFinishDate: "2026-08-20", progressStatus: "延期", satisfaction: 2, inspectionStatus: "异常", inspectionAt: "2026-07-04 09:30", inspector: "交付助理", inspectionSummary: "客户群反馈木作收口疑问,现场材料堆放偏乱。" }, { id: "C002", dataTag: "测试", name: "周先生", tier: "A+", source: "标杆项目", keyCustomerReason: "别墅全案标杆项目,后续样板案例展示价值高。", customerOwner: "客管负责人-周芸", address: "江山府别墅 18 号", department: "澜光", designer: "何敏", manager: "张峥", stage: "水电验收", startDate: "2026-06-02", plannedFinishDate: "2026-09-15", progressStatus: "正常", satisfaction: 3, inspectionStatus: "已巡查", inspectionAt: "2026-07-04 10:10", inspector: "设计师", inspectionSummary: "群内沟通已回复,水电验收节点正常。" }, { id: "C003", dataTag: "测试", name: "林女士", tier: "A", source: "特殊关系", keyCustomerReason: "公司特殊关系客户,主材确认阶段需要保持高频同步。", customerOwner: "客管负责人-陈希", address: "云麓大平层 2601", department: "蘭堂", designer: "邱然", manager: "顾森", stage: "主材确认", startDate: "2026-06-10", plannedFinishDate: "2026-10-01", progressStatus: "正常", satisfaction: 4, inspectionStatus: "已巡查", inspectionAt: "2026-07-04 10:45", inspector: "交付助理", inspectionSummary: "主材确认进度正常,客户暂无新增反馈。" }, { id: "C004", dataTag: "测试", name: "陈先生", tier: "A", source: "风险关注", keyCustomerReason: "油漆阶段延期风险已显现,需要纳入重点风险控制。", customerOwner: "客管负责人-何静", address: "天境花园 5 栋 902", department: "焕境", designer: "姚晨", manager: "蒋川", stage: "油漆阶段", startDate: "2026-05-25", plannedFinishDate: "2026-07-30", progressStatus: "延期", satisfaction: 3, inspectionStatus: "未巡查", inspectionAt: "-", inspector: "-", inspectionSummary: "今日尚未提交线上巡查。" }, { id: "C005", dataTag: "测试", name: "王女士", tier: "A+", source: "标杆项目", keyCustomerReason: "竣工交付案例价值高,软装点位变更需重点关注。", customerOwner: "客管负责人-罗敏", address: "中海天钻 11 栋 1801", department: "光宸", designer: "苏禧", manager: "李成", stage: "竣工交付", startDate: "2026-04-28", plannedFinishDate: "2026-07-20", progressStatus: "正常", satisfaction: 3, inspectionStatus: "异常", inspectionAt: "2026-07-04 11:20", inspector: "服务部", inspectionSummary: "客户提到软装点位变更记录需要再次确认。" } ]; let customerDrafts = [ { id: "CR-240701", dataTag: "测试", status: "待审核", submittedBy: "交付助理", submittedRole: "交付助理", submittedAt: "2026-07-04 14:20", reviewedBy: "", reviewedAt: "", reviewNote: "", customerId: "", customer: { name: "许女士", tier: "A+", source: "客户群反馈", keyCustomerReason: "客户在群内明确表达对交付节奏的担忧,同时具备转介绍潜力,建议纳入重点客户跟进。", customerOwner: "客管负责人-周芸", address: "麓湖云境 7 栋 1501", department: "澜光", designer: "何敏", manager: "张峥", stage: "泥木阶段", startDate: "2026-06-18", plannedFinishDate: "2026-09-28", progressStatus: "正常", satisfaction: 3 } } ]; let inspections = [ { id: "R-240701", dataTag: "测试", customerId: "C001", customerName: "肖女士", inspector: "交付助理", status: "异常", createdAt: "2026-07-04 09:30", siteQuality: "木作收口与效果图有差异", civility: "现场材料堆放偏乱", groupFeedback: "客户群追问整改到场时间", summary: "需设计师和项目经理共同到场说明整改口径。" }, { id: "R-240702", dataTag: "测试", customerId: "C002", customerName: "周先生", inspector: "设计师", status: "已巡查", createdAt: "2026-07-04 10:10", siteQuality: "水电验收节点正常", civility: "现场整洁", groupFeedback: "客户群暂无新增疑问", summary: "按计划推进,保持每日同步。" }, { id: "R-240703", dataTag: "测试", customerId: "C005", customerName: "王女士", inspector: "服务部", status: "异常", createdAt: "2026-07-04 11:20", siteQuality: "竣工收口基本完成", civility: "现场已清理", groupFeedback: "客户要求复核软装点位", summary: "建议补齐变更确认链路后回访客户。" } ]; let issues = [ { id: "I-240701", dataTag: "测试", customerId: "C001", type: "施工质量", source: "客户主动反馈", status: "红色预警", owner: "刘江新", score: 91, summary: "客户反馈木作收口与效果图存在明显差异,希望项目经理今天到场确认整改方案。", rootCause: "设计交底与现场深化尺寸未完全同步", action: "项目经理 24 小时内到场复核", handlerNote: "已约设计师、项目经理今日 16:00 到场", postSatisfaction: "待回访", submitter: "交付助理", auditDue: "2026-07-02", handlingRecords: [ { handler: "刘江新", handledAt: "2026-07-04 10:30", note: "已约设计师、项目经理到场复核木作收口,现场确认整改口径。", satisfaction: "待回访", feedback: "客户要求当天给出明确整改时间。" } ], timeline: ["客户反馈木作收口与效果图存在明显差异", "二次反馈触发红色预警", "建议设计师、项目经理、监察部联合到场确认整改口径"] }, { id: "I-240702", dataTag: "测试", customerId: "C002", type: "沟通响应", source: "管理层转办", status: "红色预警", owner: "张峥", score: 86, summary: "客户连续两次反馈响应慢,担心后续交付过程失控。", rootCause: "设计师与项目经理对外答复口径不一致", action: "高层陪访并明确沟通机制", handlerNote: "已指定单一客户接口,每日 18:00 同步", postSatisfaction: "一般", submitter: "客户负责人", auditDue: "2026-07-03", handlingRecords: [ { handler: "张峥", handledAt: "2026-07-03 18:00", note: "指定单一客户接口,每日固定同步进度。", satisfaction: "一般", feedback: "客户接受固定同步,但要求本周持续观察。" } ], timeline: ["客户连续两次反馈响应慢", "担心后续交付失控", "建议设定单一接口与固定反馈节奏"] }, { id: "I-240703", dataTag: "测试", customerId: "C003", type: "材料交付", source: "线下巡检", status: "橙色预警", owner: "顾森", score: 73, summary: "进口岩板到货时间不确定,客户担心影响工期。", rootCause: "供应商排产反馈滞后", action: "采购补充替代方案", handlerNote: "待采购提供同等级替代材料", postSatisfaction: "待处置", submitter: "交付助理", auditDue: "2026-07-04", handlingRecords: [], timeline: ["进口岩板到货时间不确定", "客户担心影响工期", "建议 48 小时内提供替代材料与影响测算"] }, { id: "I-240704", dataTag: "测试", customerId: "C004", type: "施工质量", source: "监察部发现", status: "待核查", owner: "蒋川", score: 68, summary: "监察部发现墙面基层处理验收不细,要求复验整改。", rootCause: "基层处理验收不细", action: "等待监察部复验", handlerNote: "墙面修补已完成,提交复验", postSatisfaction: "满意", submitter: "项目经理", auditDue: "2026-07-01", handlingRecords: [ { handler: "蒋川", handledAt: "2026-07-01 16:20", note: "完成墙面修补并提交复验。", satisfaction: "满意", feedback: "客户确认修补效果可接受。" } ], timeline: ["墙面修补已完成", "项目经理提交完成申请", "监察部复验通过后标记已处理并纳入班组质量复盘"] }, { id: "I-240705", dataTag: "测试", customerId: "C005", type: "设计变更", source: "客户主动反馈", status: "待核查", owner: "苏禧", score: 79, summary: "客户反馈软装点位与最终确认稿不一致,要求重新核对。", rootCause: "变更记录分散在微信沟通中", action: "核查变更签字与交付一致性", handlerNote: "已补齐变更确认链路", postSatisfaction: "待核查", submitter: "设计师", auditDue: "2026-07-01", handlingRecords: [ { handler: "苏禧", handledAt: "2026-07-02 11:10", note: "补齐点位变更确认记录,并提交监察核查。", satisfaction: "待核查", feedback: "客户要求交付前再核对一次。" } ], timeline: ["客户反馈软装点位与最终确认稿不一致", "变更记录未固化到项目台账", "建议形成交付前核对清单"] }, { id: "I-240706", dataTag: "测试", customerId: "C002", type: "客户满意度", source: "线下巡检", status: "已处理", owner: "何敏", score: 66, summary: "满意度低于 3 分,客户认为节点说明不够清晰。", rootCause: "客户期望与节点说明不一致", action: "已完成解释与节点确认", handlerNote: "监察部核查通过", postSatisfaction: "满意", submitter: "监察部", auditDue: "2026-06-29", handlingRecords: [ { handler: "何敏", handledAt: "2026-06-29 15:00", note: "补充节点说明并重新确认计划。", satisfaction: "满意", feedback: "客户确认接受新计划。" } ], timeline: ["满意度低于 3 分触发预警", "设计师补充节点说明", "客户确认接受新计划,监察部标记已处理"] } ]; let rulesState = { updatedAt: null, updatedBy: null }; const dataFile = process.env.DATA_FILE || ( process.platform === "win32" ? path.join(rootDir, "data", "app-state.json") : "/var/lib/keyissue-prototype/app-state.json" ); let persistQueue = Promise.resolve(); function getStateSnapshot() { return { version: 1, savedAt: new Date().toISOString(), users, roles, dictionaries, customers, customerDrafts, inspections, issues, rulesState }; } function cloneDefault(value) { return JSON.parse(JSON.stringify(value)); } function ensureRequiredRolesAndUsers() { let changed = false; Object.entries(requiredRoleDefaults).forEach(([roleKey, defaults]) => { if (!roles[roleKey]) { roles[roleKey] = cloneDefault(defaults); changed = true; return; } ["roleName", "summary", "pages", "permissions"].forEach((field) => { if (!roles[roleKey][field]) { roles[roleKey][field] = cloneDefault(defaults[field]); changed = true; return; } if (Array.isArray(defaults[field]) && Array.isArray(roles[roleKey][field])) { defaults[field].forEach((item) => { if (!roles[roleKey][field].includes(item)) { roles[roleKey][field].push(item); changed = true; } }); } }); }); requiredUserDefaults.forEach((spec) => { if (users.some((user) => user.username === spec.username)) return; const managedUser = { id: nextUserId(), username: spec.username, displayName: spec.displayName, role: spec.role, status: "启用" }; setUserPassword(managedUser, getInitialPassword(spec.username)); users.push(managedUser); changed = true; }); users.forEach((user) => { if (user.password && !user.passwordHash) { setUserPassword(user, user.password); changed = true; } if (user.registerSource !== "roster") return; const member = findRosterMemberForUser(user); if (!member) return; const nextRole = mapRosterMemberToRole(member); const nextDepartment = getRosterScopeDepartment(member); const nextPosition = member.position || ""; if (user.role !== nextRole) { user.role = nextRole; changed = true; } if ((user.department || "") !== nextDepartment) { user.department = nextDepartment; changed = true; } if ((user.position || "") !== nextPosition) { user.position = nextPosition; changed = true; } }); return changed; } function applyPersistedState(state) { if (!state || typeof state !== "object") return; if (Array.isArray(state.users)) users = state.users; if (state.roles && typeof state.roles === "object") roles = state.roles; if (state.dictionaries && typeof state.dictionaries === "object") dictionaries = state.dictionaries; if (Array.isArray(state.customers)) customers = state.customers; if (Array.isArray(state.customerDrafts)) customerDrafts = state.customerDrafts; if (Array.isArray(state.inspections)) inspections = state.inspections; if (Array.isArray(state.issues)) issues = state.issues; if (state.rulesState && typeof state.rulesState === "object") rulesState = state.rulesState; } async function loadState() { let loadedPersistedState = false; try { const raw = await fs.readFile(dataFile, "utf8"); applyPersistedState(JSON.parse(raw)); loadedPersistedState = true; console.log(`Loaded persisted data from ${dataFile}`); } catch (error) { if (error.code !== "ENOENT") throw error; } const changed = ensureRequiredRolesAndUsers(); if (loadedPersistedState && changed) await persistState(); } async function loadRoster() { const rosterFile = path.join(rootDir, "data", "roster-members.json"); try { const raw = await fs.readFile(rosterFile, "utf8"); const data = JSON.parse(raw); let activeDepartment = ""; rosterMembers = (Array.isArray(data.members) ? data.members : []).map((member) => { const department = String(member.department || "").trim(); if (department) activeDepartment = department; return { ...member, scopeDepartment: inferRosterScopeDepartment(member, activeDepartment) }; }); console.log(`Loaded roster whitelist: ${rosterMembers.length} members`); } catch (error) { if (error.code !== "ENOENT") throw error; rosterMembers = []; console.warn("Roster whitelist file not found; registration is disabled."); } } async function persistState() { persistQueue = persistQueue.catch(() => {}).then(async () => { await fs.mkdir(path.dirname(dataFile), { recursive: true }); const tempFile = `${dataFile}.${process.pid}.tmp`; await fs.writeFile(tempFile, JSON.stringify(getStateSnapshot(), null, 2), "utf8"); await fs.rename(tempFile, dataFile); }); return persistQueue; } const mimeTypes = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".md": "text/markdown; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".svg": "image/svg+xml; charset=utf-8", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }; function sendJson(res, status, body) { const payload = JSON.stringify(body); res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Content-Length": Buffer.byteLength(payload), "Cache-Control": "no-store" }); res.end(payload); } function sendError(res, status, code, message) { sendJson(res, status, { error: code, message }); } async function readBody(req) { let raw = ""; for await (const chunk of req) raw += chunk; if (!raw) return {}; try { return JSON.parse(raw); } catch { const error = new Error("请求体不是有效的 JSON。"); error.status = 400; throw error; } } function publicUser(user) { const role = roles[user.role]; return { id: user.id, loginName: user.username, userName: user.displayName, roleKey: user.role, roleName: role.roleName, status: user.status, pages: role.pages, permissions: role.permissions }; } function publicManagedUser(user) { return { id: user.id, username: user.username, displayName: user.displayName, phone: user.phone || "", department: user.department || "", position: user.position || "", registerSource: user.registerSource || "", role: user.role, roleName: roles[user.role]?.roleName || user.role, status: user.status || "启用" }; } function publicCustomerDraft(draft) { return { id: draft.id, dataTag: draft.dataTag, status: draft.status, submittedBy: draft.submittedBy, submittedRole: draft.submittedRole, submittedAt: draft.submittedAt, reviewedBy: draft.reviewedBy, reviewedAt: draft.reviewedAt, reviewNote: draft.reviewNote, customerId: draft.customerId, customer: draft.customer }; } function createSessionToken() { if (typeof crypto.randomUUID === "function") return crypto.randomUUID(); return crypto.randomBytes(24).toString("hex"); } function getBearerToken(req) { const header = req.headers.authorization || ""; const [type, token] = header.split(" "); return type === "Bearer" ? token : ""; } function getCurrentUser(req) { const token = getBearerToken(req); const userId = sessions.get(token); const user = users.find((item) => item.id === userId) || null; return user && user.status !== "停用" ? user : null; } function can(user, permission) { if (!user) return false; const role = roles[user.role]; if (role.permissions.includes("*")) return true; if (permission.startsWith("page:")) return role.pages.includes(permission.replace("page:", "")); return role.permissions.includes(permission); } function normalizeTier(value) { const tier = String(value || "").trim().toUpperCase(); return customerTiers.includes(tier) ? tier : null; } function normalizeProgressStatus(value) { const status = String(value || "").trim(); return dictionaries.progressStatuses.includes(status) ? status : dictionaries.progressStatuses[0] || "正常"; } function normalizeName(value) { return String(value || "").trim().replace(/\s+/g, ""); } function normalizePhone(value) { return String(value || "").replace(/\D/g, ""); } function normalizeDepartment(value) { return normalizeName(value) .replace(/筑境/g, "") .replace(/设计工作室/g, "") .replace(/工作室/g, "") .replace(/中心/g, "") .replace(/部门/g, "部"); } function getRosterScopeDepartment(member) { return String(member?.scopeDepartment || member?.department || "").trim(); } function inferRosterScopeDepartment(member, fallbackDepartment = "") { const department = String(member?.department || "").trim(); if (department) return department; const position = String(member?.position || "").trim(); if (/运营副总|总经理助理|AI工程师/.test(position)) return "总经办"; if (/监察/.test(position)) return "监察部"; if (/客管|客户经理/.test(position)) return String(fallbackDepartment || "客管部").trim(); if (/设计经理|设计总监|设计师|深化设计|灯光设计|定制设计/.test(position)) return String(fallbackDepartment || "设计部").trim(); if (/交付总监|交付经理|交付助理|产业交付/.test(position)) return String(fallbackDepartment || "交付部").trim(); if (/供应链|采购|库管|结算|跟单|主材|辅材|定制/.test(position)) return String(fallbackDepartment || "供应链中心").trim(); return String(fallbackDepartment || "").trim(); } function getCustomerOwnerName(customer) { return String(customer?.customerOwner || "") .replace(/^客管负责人[-—::]?/, "") .replace(/^客户负责人[-—::]?/, "") .trim(); } function findRosterMemberByName(name) { const target = normalizeName(name); if (!target) return null; return rosterMembers.find((member) => normalizeName(member.name) === target) || null; } function findRosterMemberForUser(user) { if (!user) return null; const phone = normalizePhone(user.phone || user.username); if (phone) { const byPhone = rosterMembers.find((member) => normalizePhone(member.phone) === phone); if (byPhone) return byPhone; } return findRosterMemberByName(user.displayName); } function userScopeDepartment(user) { return String(user?.department || getRosterScopeDepartment(findRosterMemberForUser(user))).trim(); } function sameDepartmentGroup(left, right) { const a = normalizeDepartment(left); const b = normalizeDepartment(right); if (!a || !b) return false; return a === b || a.includes(b) || b.includes(a); } function personMatchesField(field, personName) { const fieldText = normalizeName(field); const name = normalizeName(personName); if (!fieldText || !name) return false; return fieldText === name || fieldText.includes(name) || name.includes(fieldText); } function mapRosterMemberToRole(member) { const text = `${getRosterScopeDepartment(member)} ${member.position || ""}`; if (/供应链/.test(text)) return "supplyChain"; if (/监察|质检/.test(text)) return "auditor"; if (/交付总监|交付经理/.test(text)) return "deliveryManager"; if (/客管经理|用户服务经理/.test(text)) return "customerManager"; if (/客管|客户经理|家访师|用户服务/.test(text)) return "customerOwner"; if (/设计总监|设计经理|定制设计主管/.test(text)) return "designManager"; if (/设计师|深化设计|灯光设计|定制设计/.test(text)) return "designer"; if (/监察|质检/.test(text)) return "auditor"; if (/董事长|总经理|副总|总监|管理型|AI工程师/.test(text)) return "executive"; if (/交付助理|产业交付|销售/.test(text)) return "delivery"; if (/项目|工种|售后|安装|采购|库管|结算|跟单|主材|辅材|主管|技师/.test(text)) return "manager"; return "delivery"; } function findRosterMember(name, phone) { const targetName = normalizeName(name); const targetPhone = normalizePhone(phone); if (!targetName || !targetPhone) return null; return rosterMembers.find((member) => ( normalizeName(member.name) === targetName && normalizePhone(member.phone) === targetPhone )) || null; } function publicRosterMember(member) { return { name: member.name, department: getRosterScopeDepartment(member), position: member.position || "", role: mapRosterMemberToRole(member), roleName: roles[mapRosterMemberToRole(member)]?.roleName || mapRosterMemberToRole(member) }; } function normalizeCustomerKeyPart(value) { return String(value || "").trim().replace(/\s+/g, " ").toLowerCase(); } function customerIdentityKey(customer) { return [ customer.name, customer.address, customer.tier, customer.manager, customer.startDate, customer.plannedFinishDate ].map(normalizeCustomerKeyPart).join("|"); } function findDuplicateCustomer(candidate) { const key = customerIdentityKey(candidate); return customers.find((customer) => customerIdentityKey(customer) === key); } function findDuplicateCustomerDraft(candidate) { const key = customerIdentityKey(candidate); return customerDrafts.find((draft) => { if (!["待审核", "已通过"].includes(draft.status)) return false; return draft.customer && customerIdentityKey(draft.customer) === key; }); } function nowText() { const now = new Date(); const pad = (value) => String(value).padStart(2, "0"); return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; } function requireAuth(req, res) { const user = getCurrentUser(req); if (!user) { sendError(res, 401, "UNAUTHORIZED", "请先登录后再操作。"); return null; } return user; } function requirePermission(req, res, permission) { const user = requireAuth(req, res); if (!user) return null; if (!can(user, permission)) { sendError(res, 403, "NO_PERMISSION", "当前登录人没有后台授权,操作已被服务器拒绝。"); return null; } return user; } function nextCustomerId() { const max = customers.reduce((acc, customer) => Math.max(acc, Number(customer.id.slice(1)) || 0), 0); return `C${String(max + 1).padStart(3, "0")}`; } function nextCustomerDraftId() { const max = customerDrafts.reduce((acc, draft) => Math.max(acc, Number(draft.id.replace("CR-", "")) || 240700), 240700); return `CR-${max + 1}`; } function nextUserId() { const max = users.reduce((acc, user) => Math.max(acc, Number(user.id.slice(1)) || 0), 0); return `U${String(max + 1).padStart(3, "0")}`; } function normalizeStringList(value) { const list = Array.isArray(value) ? value : String(value || "").split(/[,,\n]/); return [...new Set(list.map((item) => String(item || "").trim()).filter(Boolean))]; } function normalizeRolePages(pages) { const allowed = new Set(pageCatalog.map((page) => page.key)); return normalizeStringList(pages).filter((page) => allowed.has(page)); } function normalizeRolePermissions(permissions) { const allowed = new Set(permissionCatalog.map((permission) => permission.key)); const list = normalizeStringList(permissions); if (list.includes("*")) return ["*"]; return list.filter((permission) => allowed.has(permission)); } function nextIssueId() { const max = issues.reduce((acc, issue) => Math.max(acc, Number(issue.id.replace("I-", "")) || 240700), 240700); return `I-${max + 1}`; } function nextInspectionId() { const max = inspections.reduce((acc, item) => Math.max(acc, Number(item.id.replace("R-", "")) || 240700), 240700); return `R-${max + 1}`; } function buildCustomerCandidate(body) { const tier = normalizeTier(body.tier); if (!tier) { const error = new Error("本系统仅保留 A / A+ 客户,请选择 A 或 A+。"); error.status = 400; error.code = "INVALID_TIER"; throw error; } const candidate = { name: String(body.name || "").trim(), tier, source: String(body.source || "待确认").trim(), keyCustomerReason: String(body.keyCustomerReason || "").trim(), customerOwner: String(body.customerOwner || "").trim(), address: String(body.address || "").trim(), department: String(body.department || "待确认").trim(), designer: String(body.designer || "").trim(), manager: String(body.manager || "").trim(), stage: String(body.stage || "待确认").trim(), startDate: String(body.startDate || "").trim() || "-", plannedFinishDate: String(body.plannedFinishDate || "").trim() || "-", progressStatus: normalizeProgressStatus(body.progressStatus), satisfaction: Number(body.satisfaction || 5) }; if (!candidate.name || !candidate.address || !candidate.designer || !candidate.manager || !candidate.customerOwner || !candidate.keyCustomerReason) { const error = new Error("请补全客户姓名、地址、设计师、项目经理、客管人员负责人和重点客户入选说明。"); error.status = 400; error.code = "INVALID_CUSTOMER"; throw error; } return candidate; } function promoteCustomer(candidate, reviewer) { return { id: nextCustomerId(), ...candidate, approvedBy: reviewer.displayName, approvedAt: nowText(), inspectionStatus: "未巡查", inspectionAt: "-", inspector: "-", inspectionSummary: "新建客户,等待首次每日线上巡查。" }; } function scoreByStatus(status) { if (status === "红色预警") return 90; if (status === "橙色预警") return 78; return 62; } function findIssue(res, issueId) { const issue = issues.find((item) => item.id === issueId); if (!issue) sendError(res, 404, "NOT_FOUND", "未找到对应的问题反馈。"); return issue; } function hasFullCustomerAccess(user) { return ["admin", "executive", "auditor", "supplyChain", "deliveryManager", "delivery"].includes(user?.role); } function canAccessCustomer(user, customer) { if (!user || !customer) return false; if (hasFullCustomerAccess(user)) return true; if (user.role === "designer") { return personMatchesField(customer.designer, user.displayName); } if (user.role === "designManager") { const department = userScopeDepartment(user); const designerMember = findRosterMemberByName(customer.designer); const customerDesignerDepartment = getRosterScopeDepartment(designerMember) || customer.department; return sameDepartmentGroup(department, customerDesignerDepartment); } if (user.role === "manager") { return personMatchesField(customer.manager, user.displayName); } if (user.role === "customerOwner") { return personMatchesField(customer.customerOwner, user.displayName); } if (user.role === "customerManager") { const department = userScopeDepartment(user); const ownerMember = findRosterMemberByName(getCustomerOwnerName(customer)); return sameDepartmentGroup(department, getRosterScopeDepartment(ownerMember)); } return false; } function visibleCustomersFor(user) { return customers.filter((customer) => canAccessCustomer(user, customer)); } function visibleCustomerIdSet(user) { return new Set(visibleCustomersFor(user).map((customer) => customer.id)); } function canAccessIssue(user, issue) { const customer = customers.find((item) => item.id === issue?.customerId); return canAccessCustomer(user, customer); } function visibleIssuesFor(user) { return issues.filter((issue) => canAccessIssue(user, issue)); } function visibleInspectionsFor(user) { const customerIds = visibleCustomerIdSet(user); return inspections.filter((inspection) => customerIds.has(inspection.customerId)); } function requireCustomerAccess(user, res, customer) { if (canAccessCustomer(user, customer)) return true; sendError(res, 403, "CUSTOMER_SCOPE_DENIED", "当前登录人无权查看或操作该客户。"); return false; } function requireIssueAccess(user, res, issue) { if (canAccessIssue(user, issue)) return true; sendError(res, 403, "CUSTOMER_SCOPE_DENIED", "当前登录人无权查看或操作该客户的问题反馈。"); return false; } async function handleApi(req, res, pathname) { if (req.method === "GET" && pathname === "/api/auth/options") { sendJson(res, 200, { accounts: users.filter((user) => user.status !== "停用").map((user) => ({ displayName: user.displayName, roleName: roles[user.role].roleName, summary: roles[user.role].summary })) }); return; } if (req.method === "POST" && pathname === "/api/auth/login") { const body = await readBody(req); const user = users.find((item) => item.username === body.username && item.status !== "停用" && verifyUserPassword(item, body.password)); if (!user) { sendError(res, 401, "LOGIN_FAILED", "账号或密码错误。"); return; } const token = createSessionToken(); sessions.set(token, user.id); sendJson(res, 200, { token, user: publicUser(user) }); return; } if (req.method === "POST" && pathname === "/api/auth/register") { const body = await readBody(req); const name = normalizeName(body.name); const phone = normalizePhone(body.phone); const password = String(body.password || "").trim(); const confirmPassword = body.confirmPassword == null ? password : String(body.confirmPassword || "").trim(); if (!name || !phone || !password) { sendError(res, 400, "INVALID_REGISTER_INFO", "请填写姓名、手机号和登录密码。"); return; } if (password.length < 6) { sendError(res, 400, "INVALID_PASSWORD", "密码至少需要 6 位。"); return; } if (password !== confirmPassword) { sendError(res, 400, "PASSWORD_NOT_MATCH", "两次输入的密码不一致。"); return; } const member = findRosterMember(name, phone); if (!member) { sendError(res, 403, "ROSTER_NOT_MATCHED", "姓名和手机号未在名册中匹配,暂不能注册。"); return; } const role = mapRosterMemberToRole(member); let user = users.find((item) => normalizePhone(item.phone || item.username) === phone); if (user && normalizeName(user.displayName) !== normalizeName(member.name)) { sendError(res, 409, "PHONE_ALREADY_REGISTERED", "该手机号已被其他账号注册,请联系系统管理员。"); return; } if (user) { if (user.status === "停用") { sendError(res, 403, "USER_DISABLED", "该账号已停用,请联系系统管理员。"); return; } sendError(res, 409, "USER_ALREADY_REGISTERED", "该手机号已完成注册,请直接使用手机号和已设置密码登录,或联系系统管理员重置密码。"); return; } user = { id: nextUserId(), username: phone, displayName: member.name, phone, department: getRosterScopeDepartment(member), position: member.position || "", role, status: "启用", registerSource: "roster" }; setUserPassword(user, password); users.push(user); await persistState(); const token = createSessionToken(); sessions.set(token, user.id); sendJson(res, 201, { token, user: publicUser(user), roster: publicRosterMember(member), message: `注册成功,已自动匹配为${roles[role]?.roleName || role}。登录账号为手机号,请使用刚设置的密码登录。` }); return; } if (req.method === "GET" && pathname === "/api/auth/me") { const user = requireAuth(req, res); if (!user) return; sendJson(res, 200, { user: publicUser(user) }); return; } if (req.method === "POST" && pathname === "/api/auth/logout") { const token = getBearerToken(req); if (token) sessions.delete(token); sendJson(res, 200, { message: "已退出登录。" }); return; } if (req.method === "GET" && pathname === "/api/meta") { const user = requireAuth(req, res); if (!user) return; sendJson(res, 200, { dictionaries }); return; } if (req.method === "GET" && pathname === "/api/admin/config") { const user = requirePermission(req, res, "admin:manage"); if (!user) return; sendJson(res, 200, { users: users.map(publicManagedUser), roles, pageCatalog, permissionCatalog, customerDrafts: customerDrafts.map(publicCustomerDraft), dictionaries, rules: rulesState }); return; } if (req.method === "POST" && pathname === "/api/admin/users") { const user = requirePermission(req, res, "admin:manage"); if (!user) return; const body = await readBody(req); const username = String(body.username || "").trim(); const password = String(body.password || "").trim(); const displayName = String(body.displayName || "").trim(); const role = String(body.role || "").trim(); if (!username || !password || !displayName || !roles[role]) { sendError(res, 400, "INVALID_USER", "请补全账号、密码、姓名,并选择有效角色。"); return; } if (users.some((item) => item.username === username)) { sendError(res, 400, "DUPLICATE_USER", "该登录账号已存在。"); return; } const managedUser = { id: nextUserId(), username, displayName, phone: normalizePhone(body.phone), department: String(body.department || "").trim(), position: String(body.position || "").trim(), role, status: body.status === "停用" ? "停用" : "启用" }; setUserPassword(managedUser, password); users.push(managedUser); await persistState(); sendJson(res, 201, { user: publicManagedUser(managedUser), message: "后台账号已创建。" }); return; } const adminUserMatch = pathname.match(/^\/api\/admin\/users\/([^/]+)$/); if (req.method === "PATCH" && adminUserMatch) { const operator = requirePermission(req, res, "admin:manage"); if (!operator) return; const target = users.find((item) => item.id === decodeURIComponent(adminUserMatch[1])); if (!target) { sendError(res, 404, "NOT_FOUND", "未找到对应账号。"); return; } const body = await readBody(req); if (body.username !== undefined) { const username = String(body.username || "").trim(); if (!username) { sendError(res, 400, "INVALID_USER", "登录账号不能为空。"); return; } if (users.some((item) => item.id !== target.id && item.username === username)) { sendError(res, 400, "DUPLICATE_USER", "该登录账号已存在。"); return; } target.username = username; } if (body.password !== undefined) { const nextPassword = String(body.password || "").trim(); if (nextPassword) setUserPassword(target, nextPassword); } if (body.displayName !== undefined) target.displayName = String(body.displayName || "").trim() || target.displayName; if (body.phone !== undefined) target.phone = normalizePhone(body.phone); if (body.department !== undefined) target.department = String(body.department || "").trim(); if (body.position !== undefined) target.position = String(body.position || "").trim(); if (body.role !== undefined) { const role = String(body.role || "").trim(); if (!roles[role]) { sendError(res, 400, "INVALID_ROLE", "请选择有效角色。"); return; } target.role = role; } if (body.status !== undefined) { const nextStatus = body.status === "停用" ? "停用" : "启用"; const activeAdmins = users.filter((item) => item.status !== "停用" && item.role === "admin"); if (target.role === "admin" && nextStatus === "停用" && activeAdmins.length <= 1 && activeAdmins[0]?.id === target.id) { sendError(res, 400, "LAST_ADMIN", "至少保留一个启用的系统管理员。"); return; } target.status = nextStatus; if (nextStatus === "停用") { [...sessions.entries()].forEach(([token, userId]) => { if (userId === target.id) sessions.delete(token); }); } } await persistState(); sendJson(res, 200, { user: publicManagedUser(target), message: "账号信息已由后台更新。" }); return; } const adminRoleMatch = pathname.match(/^\/api\/admin\/roles\/([^/]+)$/); if (req.method === "PATCH" && adminRoleMatch) { const user = requirePermission(req, res, "admin:manage"); if (!user) return; const roleKey = decodeURIComponent(adminRoleMatch[1]); const role = roles[roleKey]; if (!role) { sendError(res, 404, "NOT_FOUND", "未找到对应角色。"); return; } const body = await readBody(req); if (body.roleName !== undefined) role.roleName = String(body.roleName || "").trim() || role.roleName; if (body.summary !== undefined) role.summary = String(body.summary || "").trim() || role.summary; if (roleKey === "admin") { role.pages = pageCatalog.map((page) => page.key); role.permissions = ["*", "admin:manage"]; } else { if (body.pages !== undefined) role.pages = normalizeRolePages(body.pages); if (body.permissions !== undefined) role.permissions = normalizeRolePermissions(body.permissions); } await persistState(); sendJson(res, 200, { roleKey, role, message: "角色权限已由后台更新。" }); return; } if (req.method === "PATCH" && pathname === "/api/admin/dictionaries") { const user = requirePermission(req, res, "admin:manage"); if (!user) return; const body = await readBody(req); Object.keys(dictionaries).forEach((key) => { if (body[key] !== undefined) { const values = normalizeStringList(body[key]); if (values.length) dictionaries[key] = values; } }); await persistState(); sendJson(res, 200, { dictionaries, message: "基础字典已由后台更新。" }); return; } const customerDraftMatch = pathname.match(/^\/api\/admin\/customer-drafts\/([^/]+)$/); if (req.method === "PATCH" && customerDraftMatch) { const user = requirePermission(req, res, "customer:approve"); if (!user) return; const draft = customerDrafts.find((item) => item.id === decodeURIComponent(customerDraftMatch[1])); if (!draft) { sendError(res, 404, "NOT_FOUND", "未找到对应的客户入池审核单。"); return; } if (draft.status !== "待审核") { sendError(res, 400, "ALREADY_REVIEWED", "该客户入池申请已经审核完成。"); return; } const body = await readBody(req); const action = String(body.action || "").trim(); draft.reviewedBy = user.displayName; draft.reviewedAt = nowText(); draft.reviewNote = String(body.reviewNote || "").trim(); if (action === "approve") { const duplicateCustomer = findDuplicateCustomer(draft.customer); if (duplicateCustomer) { draft.status = "已通过"; draft.customerId = duplicateCustomer.id; await persistState(); sendJson(res, 200, { customer: duplicateCustomer, draft: publicCustomerDraft(draft), message: "已存在相同客户档案,本次未重复入库。" }); return; } const customer = promoteCustomer(draft.customer, user); customers.unshift(customer); draft.status = "已通过"; draft.customerId = customer.id; await persistState(); sendJson(res, 200, { customer, draft: publicCustomerDraft(draft), message: "客户入池申请已通过,客户已进入 A/A+ 客户库。" }); return; } if (action === "reject") { draft.status = "已驳回"; await persistState(); sendJson(res, 200, { draft: publicCustomerDraft(draft), message: "客户入池申请已驳回,未进入客户库。" }); return; } sendError(res, 400, "INVALID_ACTION", "请选择通过或驳回该客户入池申请。"); return; } if (req.method === "GET" && pathname === "/api/customers") { const user = requireAuth(req, res); if (!user) return; sendJson(res, 200, { customers: visibleCustomersFor(user) }); return; } if (req.method === "POST" && pathname === "/api/customers") { const user = requirePermission(req, res, "customer:submit"); if (!user) return; const body = await readBody(req); let candidate; try { candidate = buildCustomerCandidate(body); } catch (error) { sendError(res, error.status || 400, error.code || "INVALID_CUSTOMER", error.message); return; } const duplicateCustomer = findDuplicateCustomer(candidate); if (duplicateCustomer) { const payload = { message: "已存在相同客户档案,本次未重复入库。" }; if (canAccessCustomer(user, duplicateCustomer)) payload.customer = duplicateCustomer; sendJson(res, 200, payload); return; } const duplicateDraft = findDuplicateCustomerDraft(candidate); if (duplicateDraft) { sendJson(res, 200, { draft: publicCustomerDraft(duplicateDraft), message: "已存在相同客户入池申请,本次未重复提交。" }); return; } if (user.role === "admin") { const customer = promoteCustomer(candidate, user); customers.unshift(customer); await persistState(); sendJson(res, 201, { customer, message: "管理员已直接保存客户档案,客户已进入 A/A+ 客户库。" }); return; } const draft = { id: nextCustomerDraftId(), status: "待审核", submittedBy: user.displayName, submittedRole: roles[user.role]?.roleName || user.role, submittedAt: nowText(), reviewedBy: "", reviewedAt: "", reviewNote: "", customerId: "", customer: candidate }; customerDrafts.unshift(draft); await persistState(); sendJson(res, 202, { draft: publicCustomerDraft(draft), message: "客户入池申请已提交,等待管理员审核后进入客户库。" }); return; } if (req.method === "GET" && pathname === "/api/inspections") { const user = requirePermission(req, res, "inspection:view"); if (!user) return; sendJson(res, 200, { inspections: visibleInspectionsFor(user) }); return; } if (req.method === "POST" && pathname === "/api/inspections") { const user = requirePermission(req, res, "inspection:create"); if (!user) return; const body = await readBody(req); const customer = customers.find((item) => item.id === body.customerId); if (!customer) { sendError(res, 400, "INVALID_CUSTOMER", "请选择有效的巡查客户。"); return; } if (!requireCustomerAccess(user, res, customer)) return; const status = ["已巡查", "未巡查", "异常"].includes(body.status) ? body.status : "已巡查"; const inspection = { id: nextInspectionId(), customerId: customer.id, customerName: customer.name, inspector: String(body.inspector || user.displayName).trim(), status, createdAt: nowText(), siteQuality: String(body.siteQuality || "未填写").trim(), civility: String(body.civility || "未填写").trim(), groupFeedback: String(body.groupFeedback || "未填写").trim(), summary: String(body.summary || "每日线上巡查已记录。").trim() }; inspections.unshift(inspection); customer.inspectionStatus = inspection.status; customer.inspectionAt = inspection.createdAt; customer.inspector = inspection.inspector; customer.inspectionSummary = inspection.summary; await persistState(); sendJson(res, 201, { inspection, message: "每日线上巡查已提交。" }); return; } if (req.method === "GET" && pathname === "/api/issues") { const user = requirePermission(req, res, "issue:view"); if (!user) return; sendJson(res, 200, { issues: visibleIssuesFor(user) }); return; } if (req.method === "POST" && pathname === "/api/issues") { const user = requirePermission(req, res, "issue:create"); if (!user) return; const body = await readBody(req); const customer = customers.find((item) => item.id === body.customerId); if (!customer) { sendError(res, 400, "INVALID_CUSTOMER", "请选择有效的关联客户。"); return; } if (!requireCustomerAccess(user, res, customer)) return; const status = body.status || "蓝色关注"; const issue = { id: nextIssueId(), customerId: customer.id, type: body.type || "客户反馈", source: body.source || dictionaries.issueSources[0] || "线下巡检", status, owner: String(body.owner || "").trim() || customer.manager, score: scoreByStatus(status), summary: String(body.summary || "客户反馈已记录").trim(), rootCause: String(body.rootCause || "待责任人补充根因分析").trim(), action: "新反馈已进入预警池,等待责任人确认处置动作", handlerNote: "待提交处置记录", postSatisfaction: "待处置", submitter: user.displayName, auditDue: body.auditDue || "", handlingRecords: [], timeline: [ String(body.summary || "客户反馈已记录").trim(), String(body.rootCause || "待补充根因分析").trim(), "系统已生成预警并同步至问题反馈池" ] }; issues.unshift(issue); await persistState(); sendJson(res, 201, { issue, message: "问题反馈已由后台提交,并进入预警池。" }); return; } const issueActionMatch = pathname.match(/^\/api\/issues\/([^/]+)\/([^/]+)$/); if (req.method === "PATCH" && issueActionMatch) { const [, encodedIssueId, action] = issueActionMatch; const issueId = decodeURIComponent(encodedIssueId); const issue = findIssue(res, issueId); if (!issue) return; const body = await readBody(req); if (action === "assign") { const user = requirePermission(req, res, "issue:assign"); if (!user) return; if (!requireIssueAccess(user, res, issue)) return; issue.owner = String(body.owner || "部门负责人").trim(); issue.action = "已由管理层指派责任人牵头处理"; issue.timeline.push(`${user.displayName} 指派 ${issue.owner} 负责闭环`); await persistState(); sendJson(res, 200, { issue, message: "后台已完成责任人指派。" }); return; } if (action === "quick-handle") { const user = requirePermission(req, res, "issue:handle"); if (!user) return; if (!requireIssueAccess(user, res, issue)) return; issue.status = "处理中"; issue.action = "已进入处置流程,等待补充根因与整改记录"; issue.handlerNote = `${user.displayName} 已接收并开始处置`; issue.handlingRecords = issue.handlingRecords || []; issue.handlingRecords.push({ handler: user.displayName, handledAt: nowText(), note: "接收问题并进入处置流程。", satisfaction: issue.postSatisfaction || "待处置", feedback: "等待补充处理方案。" }); issue.timeline.push(`${user.displayName} 接收问题并进入处置流程`); await persistState(); sendJson(res, 200, { issue, message: "后台已将问题切换为处理中。" }); return; } if (action === "handle") { const user = requirePermission(req, res, "issue:handle"); if (!user) return; if (!requireIssueAccess(user, res, issue)) return; issue.status = "待核查"; issue.action = "处置完成,等待监察核查"; issue.handlerNote = String(body.handlerNote || "项目经理已提交处置记录").trim(); issue.postSatisfaction = String(body.satisfaction || "待回访").trim(); issue.handlingRecords = issue.handlingRecords || []; issue.handlingRecords.push({ handler: user.displayName, handledAt: nowText(), note: issue.handlerNote, satisfaction: issue.postSatisfaction, feedback: String(body.customerFeedback || "待客户回访确认").trim() }); issue.timeline.push(`${user.displayName} 提交处置记录,处置后满意度:${issue.postSatisfaction}`); await persistState(); sendJson(res, 200, { issue, message: "处置记录已提交,后台已转入待核查。" }); return; } if (action === "audit-pass") { const user = requirePermission(req, res, "audit:review"); if (!user) return; if (!requireIssueAccess(user, res, issue)) return; issue.status = "已处理"; issue.action = "监察核查通过,问题已处理"; issue.handlerNote = "监察部核查通过"; issue.timeline.push(`${user.displayName} 核查通过,问题已处理`); await persistState(); sendJson(res, 200, { issue, message: "监察核查已通过,问题已处理。" }); return; } if (action === "audit-reject") { const user = requirePermission(req, res, "audit:review"); if (!user) return; if (!requireIssueAccess(user, res, issue)) return; issue.status = "红色预警"; issue.action = "监察驳回,需重新提交整改方案"; issue.handlerNote = "监察部驳回整改"; issue.timeline.push(`${user.displayName} 驳回整改,问题回到红色预警`); await persistState(); sendJson(res, 200, { issue, message: "已驳回整改,问题回到红色预警。" }); return; } } if (req.method === "POST" && pathname === "/api/rules") { const user = requirePermission(req, res, "rules:edit"); if (!user) return; rulesState = { updatedAt: new Date().toISOString(), updatedBy: user.displayName }; await persistState(); sendJson(res, 200, { rules: rulesState, message: "预警规则已由后台保存。" }); return; } sendError(res, 404, "NOT_FOUND", "未找到对应的后台接口。"); } async function serveStatic(req, res, pathname) { if (pathname === "/") { res.writeHead(302, { Location: "/prototype/index.html" }); res.end(); return; } let decodedPath; try { decodedPath = decodeURIComponent(pathname); } catch { res.writeHead(400); res.end("Bad request"); return; } const filePath = path.resolve(rootDir, `.${decodedPath}`); if (!filePath.startsWith(rootDir)) { res.writeHead(403); res.end("Forbidden"); return; } try { const stat = await fs.stat(filePath); const finalPath = stat.isDirectory() ? path.join(filePath, "index.html") : filePath; const data = await fs.readFile(finalPath); const type = mimeTypes[path.extname(finalPath).toLowerCase()] || "application/octet-stream"; res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" }); res.end(data); } catch { res.writeHead(404); res.end("Not found"); } } const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host || "127.0.0.1"}`); try { if (url.pathname.startsWith("/api/")) { await handleApi(req, res, url.pathname); return; } await serveStatic(req, res, url.pathname); } catch (error) { const status = error.status || 500; sendError(res, status, "SERVER_ERROR", error.message || "服务器处理失败。"); } }); async function start() { await loadRoster(); await loadState(); server.listen(port, host, () => { const shownHost = host === "0.0.0.0" ? "127.0.0.1" : host; console.log(`A+在建客户动态管理系统 running at http://${shownHost}:${port}/prototype/index.html`); }); } start().catch((error) => { console.error("Failed to start server", error); process.exit(1); });