| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609 |
- 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);
- });
|