server.js 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609
  1. const http = require("http");
  2. const fs = require("fs").promises;
  3. const path = require("path");
  4. const crypto = require("crypto");
  5. const rootDir = __dirname;
  6. const port = Number(process.env.PORT || 8787);
  7. const host = process.env.HOST || "0.0.0.0";
  8. const sessions = new Map();
  9. const customerTiers = ["A", "A+"];
  10. function getInitialPassword(username) {
  11. const key = `KEYISSUE_${username.toUpperCase()}_PASSWORD`;
  12. if (process.env[key]) return process.env[key];
  13. const digest = crypto.createHash("sha256").update(`${username}:${rootDir}`).digest("hex").slice(0, 12);
  14. return `keyissue-${digest}`;
  15. }
  16. function hashPassword(password, salt = crypto.randomBytes(16).toString("hex")) {
  17. const hash = crypto.pbkdf2Sync(String(password), salt, 120000, 32, "sha256").toString("hex");
  18. return `pbkdf2:sha256:120000:${salt}:${hash}`;
  19. }
  20. function verifyPasswordHash(password, encodedHash) {
  21. const parts = String(encodedHash || "").split(":");
  22. if (parts.length !== 5 || parts[0] !== "pbkdf2" || parts[1] !== "sha256") return false;
  23. const iterations = Number(parts[2]);
  24. const salt = parts[3];
  25. const expected = parts[4];
  26. if (!iterations || !salt || !expected) return false;
  27. const actual = crypto.pbkdf2Sync(String(password), salt, iterations, 32, "sha256").toString("hex");
  28. const expectedBuffer = Buffer.from(expected, "hex");
  29. const actualBuffer = Buffer.from(actual, "hex");
  30. return expectedBuffer.length === actualBuffer.length && crypto.timingSafeEqual(expectedBuffer, actualBuffer);
  31. }
  32. function setUserPassword(user, password) {
  33. user.passwordHash = hashPassword(password);
  34. delete user.password;
  35. }
  36. function verifyUserPassword(user, password) {
  37. if (!user) return false;
  38. if (user.passwordHash) return verifyPasswordHash(password, user.passwordHash);
  39. return user.password === password;
  40. }
  41. const pageCatalog = [
  42. { key: "dashboard", name: "动态驾驶舱" },
  43. { key: "customers", name: "A/A+客户库" },
  44. { key: "inspections", name: "每日线上巡查" },
  45. { key: "issues", name: "问题反馈池" },
  46. { key: "audit", name: "监察核查" },
  47. { key: "analysis", name: "问题分析" },
  48. { key: "rules", name: "预警规则" },
  49. { key: "admin", name: "系统后台" }
  50. ];
  51. const permissionCatalog = [
  52. { key: "issue:view", name: "查看问题详情" },
  53. { key: "issue:create", name: "提交问题反馈" },
  54. { key: "issue:assign", name: "指派责任人" },
  55. { key: "issue:handle", name: "提交处置" },
  56. { key: "inspection:view", name: "查看每日巡查" },
  57. { key: "inspection:create", name: "提交每日巡查" },
  58. { key: "audit:review", name: "监察核查" },
  59. { key: "customer:submit", name: "提交客户入池申请" },
  60. { key: "customer:approve", name: "审核客户入池" },
  61. { key: "rules:edit", name: "保存预警规则" },
  62. { key: "admin:manage", name: "系统后台管理" }
  63. ];
  64. let roles = {
  65. admin: {
  66. roleName: "系统管理员",
  67. summary: "全量功能、规则配置、后台权限管理",
  68. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis", "rules", "admin"],
  69. permissions: ["*", "admin:manage"]
  70. },
  71. executive: {
  72. roleName: "管理层",
  73. summary: "全局查看、督办、指派责任人",
  74. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
  75. permissions: ["issue:view", "issue:assign", "inspection:view", "customer:submit"]
  76. },
  77. delivery: {
  78. roleName: "交付助理",
  79. summary: "提交每日巡查、问题反馈、查看客户与问题",
  80. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  81. permissions: ["issue:view", "issue:create", "inspection:view", "inspection:create", "customer:submit"]
  82. },
  83. deliveryManager: {
  84. roleName: "交付经理",
  85. summary: "交付线全量查看、问题督办与客户入池提交",
  86. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
  87. permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  88. },
  89. customerOwner: {
  90. roleName: "客管人员",
  91. summary: "仅查看本人负责客户、提交反馈与客户入池申请",
  92. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  93. permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  94. },
  95. customerManager: {
  96. roleName: "客管经理",
  97. summary: "查看本部门客管人员名下客户、督办反馈与客户入池提交",
  98. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  99. permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  100. },
  101. manager: {
  102. roleName: "项目经理",
  103. summary: "问题处置、提交巡查、提交核查、补充根因",
  104. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  105. permissions: ["issue:view", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  106. },
  107. designer: {
  108. roleName: "设计师",
  109. summary: "查看客户与问题、提交设计相关反馈、补充处置记录",
  110. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  111. permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  112. },
  113. designManager: {
  114. roleName: "设计经理",
  115. summary: "设计问题统筹、责任指派、处置跟进与客户入池提交",
  116. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  117. permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  118. },
  119. auditor: {
  120. roleName: "监察部",
  121. summary: "查看巡查、监察核查、通过或驳回整改",
  122. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
  123. permissions: ["issue:view", "audit:review", "inspection:view"]
  124. },
  125. supplyChain: {
  126. roleName: "供应链",
  127. summary: "供应链相关岗位全量查看客户与问题反馈",
  128. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  129. permissions: ["issue:view", "issue:create", "inspection:view", "customer:submit"]
  130. }
  131. };
  132. let users = [
  133. { id: "U001", username: "admin", password: getInitialPassword("admin"), displayName: "肖总", role: "admin", status: "启用" },
  134. { id: "U002", username: "executive", password: getInitialPassword("executive"), displayName: "管理层", role: "executive", status: "启用" },
  135. { id: "U003", username: "delivery", password: getInitialPassword("delivery"), displayName: "交付助理", role: "delivery", status: "启用" },
  136. { id: "U004", username: "manager", password: getInitialPassword("manager"), displayName: "项目经理", role: "manager", status: "启用" },
  137. { id: "U005", username: "auditor", password: getInitialPassword("auditor"), displayName: "监察部", role: "auditor", status: "启用" },
  138. { id: "U006", username: "designer", password: getInitialPassword("designer"), displayName: "设计师", role: "designer", status: "启用" },
  139. { id: "U007", username: "design_manager", password: getInitialPassword("design_manager"), displayName: "设计经理", role: "designManager", status: "启用" }
  140. ];
  141. let dictionaries = {
  142. departments: ["光宸", "澜光", "蘭堂", "焕境"],
  143. customerSources: ["转介绍", "公司特殊关系", "标杆项目", "风险关注"],
  144. issueSources: ["线下巡检", "客户群反馈", "每日线上巡查", "客户主动反馈", "管理层转办", "监察部发现"],
  145. issueTypes: ["施工质量", "设计变更", "沟通响应", "材料交付", "客户满意度"],
  146. progressStatuses: ["正常", "延期"]
  147. };
  148. const requiredRoleDefaults = {
  149. executive: {
  150. roleName: "管理层",
  151. summary: "全局查看、督办、指派责任人",
  152. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
  153. permissions: ["issue:view", "issue:assign", "inspection:view", "customer:submit"]
  154. },
  155. deliveryManager: {
  156. roleName: "交付经理",
  157. summary: "交付线全量查看、问题督办与客户入池提交",
  158. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
  159. permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  160. },
  161. customerOwner: {
  162. roleName: "客管人员",
  163. summary: "仅查看本人负责客户、提交反馈与客户入池申请",
  164. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  165. permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  166. },
  167. customerManager: {
  168. roleName: "客管经理",
  169. summary: "查看本部门客管人员名下客户、督办反馈与客户入池提交",
  170. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  171. permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  172. },
  173. designer: {
  174. roleName: "设计师",
  175. summary: "查看客户与问题、提交设计相关反馈、补充处置记录",
  176. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  177. permissions: ["issue:view", "issue:create", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  178. },
  179. designManager: {
  180. roleName: "设计经理",
  181. summary: "设计问题统筹、责任指派、处置跟进与客户入池提交",
  182. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  183. permissions: ["issue:view", "issue:create", "issue:assign", "issue:handle", "inspection:view", "inspection:create", "customer:submit"]
  184. },
  185. auditor: {
  186. roleName: "监察部",
  187. summary: "查看客户、巡查、监察核查、通过或驳回整改",
  188. pages: ["dashboard", "customers", "inspections", "issues", "audit", "analysis"],
  189. permissions: ["issue:view", "audit:review", "inspection:view"]
  190. },
  191. supplyChain: {
  192. roleName: "供应链",
  193. summary: "供应链相关岗位全量查看客户与问题反馈",
  194. pages: ["dashboard", "customers", "inspections", "issues", "analysis"],
  195. permissions: ["issue:view", "issue:create", "inspection:view", "customer:submit"]
  196. }
  197. };
  198. const requiredUserDefaults = [
  199. { username: "designer", displayName: "设计师", role: "designer" },
  200. { username: "design_manager", displayName: "设计经理", role: "designManager" }
  201. ];
  202. let rosterMembers = [];
  203. let customers = [
  204. { 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: "客户群反馈木作收口疑问,现场材料堆放偏乱。" },
  205. { 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: "群内沟通已回复,水电验收节点正常。" },
  206. { 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: "主材确认进度正常,客户暂无新增反馈。" },
  207. { 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: "今日尚未提交线上巡查。" },
  208. { 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: "客户提到软装点位变更记录需要再次确认。" }
  209. ];
  210. let customerDrafts = [
  211. {
  212. id: "CR-240701",
  213. dataTag: "测试",
  214. status: "待审核",
  215. submittedBy: "交付助理",
  216. submittedRole: "交付助理",
  217. submittedAt: "2026-07-04 14:20",
  218. reviewedBy: "",
  219. reviewedAt: "",
  220. reviewNote: "",
  221. customerId: "",
  222. customer: {
  223. name: "许女士",
  224. tier: "A+",
  225. source: "客户群反馈",
  226. keyCustomerReason: "客户在群内明确表达对交付节奏的担忧,同时具备转介绍潜力,建议纳入重点客户跟进。",
  227. customerOwner: "客管负责人-周芸",
  228. address: "麓湖云境 7 栋 1501",
  229. department: "澜光",
  230. designer: "何敏",
  231. manager: "张峥",
  232. stage: "泥木阶段",
  233. startDate: "2026-06-18",
  234. plannedFinishDate: "2026-09-28",
  235. progressStatus: "正常",
  236. satisfaction: 3
  237. }
  238. }
  239. ];
  240. let inspections = [
  241. { id: "R-240701", dataTag: "测试", customerId: "C001", customerName: "肖女士", inspector: "交付助理", status: "异常", createdAt: "2026-07-04 09:30", siteQuality: "木作收口与效果图有差异", civility: "现场材料堆放偏乱", groupFeedback: "客户群追问整改到场时间", summary: "需设计师和项目经理共同到场说明整改口径。" },
  242. { id: "R-240702", dataTag: "测试", customerId: "C002", customerName: "周先生", inspector: "设计师", status: "已巡查", createdAt: "2026-07-04 10:10", siteQuality: "水电验收节点正常", civility: "现场整洁", groupFeedback: "客户群暂无新增疑问", summary: "按计划推进,保持每日同步。" },
  243. { id: "R-240703", dataTag: "测试", customerId: "C005", customerName: "王女士", inspector: "服务部", status: "异常", createdAt: "2026-07-04 11:20", siteQuality: "竣工收口基本完成", civility: "现场已清理", groupFeedback: "客户要求复核软装点位", summary: "建议补齐变更确认链路后回访客户。" }
  244. ];
  245. let issues = [
  246. {
  247. id: "I-240701",
  248. dataTag: "测试",
  249. customerId: "C001",
  250. type: "施工质量",
  251. source: "客户主动反馈",
  252. status: "红色预警",
  253. owner: "刘江新",
  254. score: 91,
  255. summary: "客户反馈木作收口与效果图存在明显差异,希望项目经理今天到场确认整改方案。",
  256. rootCause: "设计交底与现场深化尺寸未完全同步",
  257. action: "项目经理 24 小时内到场复核",
  258. handlerNote: "已约设计师、项目经理今日 16:00 到场",
  259. postSatisfaction: "待回访",
  260. submitter: "交付助理",
  261. auditDue: "2026-07-02",
  262. handlingRecords: [
  263. { handler: "刘江新", handledAt: "2026-07-04 10:30", note: "已约设计师、项目经理到场复核木作收口,现场确认整改口径。", satisfaction: "待回访", feedback: "客户要求当天给出明确整改时间。" }
  264. ],
  265. timeline: ["客户反馈木作收口与效果图存在明显差异", "二次反馈触发红色预警", "建议设计师、项目经理、监察部联合到场确认整改口径"]
  266. },
  267. {
  268. id: "I-240702",
  269. dataTag: "测试",
  270. customerId: "C002",
  271. type: "沟通响应",
  272. source: "管理层转办",
  273. status: "红色预警",
  274. owner: "张峥",
  275. score: 86,
  276. summary: "客户连续两次反馈响应慢,担心后续交付过程失控。",
  277. rootCause: "设计师与项目经理对外答复口径不一致",
  278. action: "高层陪访并明确沟通机制",
  279. handlerNote: "已指定单一客户接口,每日 18:00 同步",
  280. postSatisfaction: "一般",
  281. submitter: "客户负责人",
  282. auditDue: "2026-07-03",
  283. handlingRecords: [
  284. { handler: "张峥", handledAt: "2026-07-03 18:00", note: "指定单一客户接口,每日固定同步进度。", satisfaction: "一般", feedback: "客户接受固定同步,但要求本周持续观察。" }
  285. ],
  286. timeline: ["客户连续两次反馈响应慢", "担心后续交付失控", "建议设定单一接口与固定反馈节奏"]
  287. },
  288. {
  289. id: "I-240703",
  290. dataTag: "测试",
  291. customerId: "C003",
  292. type: "材料交付",
  293. source: "线下巡检",
  294. status: "橙色预警",
  295. owner: "顾森",
  296. score: 73,
  297. summary: "进口岩板到货时间不确定,客户担心影响工期。",
  298. rootCause: "供应商排产反馈滞后",
  299. action: "采购补充替代方案",
  300. handlerNote: "待采购提供同等级替代材料",
  301. postSatisfaction: "待处置",
  302. submitter: "交付助理",
  303. auditDue: "2026-07-04",
  304. handlingRecords: [],
  305. timeline: ["进口岩板到货时间不确定", "客户担心影响工期", "建议 48 小时内提供替代材料与影响测算"]
  306. },
  307. {
  308. id: "I-240704",
  309. dataTag: "测试",
  310. customerId: "C004",
  311. type: "施工质量",
  312. source: "监察部发现",
  313. status: "待核查",
  314. owner: "蒋川",
  315. score: 68,
  316. summary: "监察部发现墙面基层处理验收不细,要求复验整改。",
  317. rootCause: "基层处理验收不细",
  318. action: "等待监察部复验",
  319. handlerNote: "墙面修补已完成,提交复验",
  320. postSatisfaction: "满意",
  321. submitter: "项目经理",
  322. auditDue: "2026-07-01",
  323. handlingRecords: [
  324. { handler: "蒋川", handledAt: "2026-07-01 16:20", note: "完成墙面修补并提交复验。", satisfaction: "满意", feedback: "客户确认修补效果可接受。" }
  325. ],
  326. timeline: ["墙面修补已完成", "项目经理提交完成申请", "监察部复验通过后标记已处理并纳入班组质量复盘"]
  327. },
  328. {
  329. id: "I-240705",
  330. dataTag: "测试",
  331. customerId: "C005",
  332. type: "设计变更",
  333. source: "客户主动反馈",
  334. status: "待核查",
  335. owner: "苏禧",
  336. score: 79,
  337. summary: "客户反馈软装点位与最终确认稿不一致,要求重新核对。",
  338. rootCause: "变更记录分散在微信沟通中",
  339. action: "核查变更签字与交付一致性",
  340. handlerNote: "已补齐变更确认链路",
  341. postSatisfaction: "待核查",
  342. submitter: "设计师",
  343. auditDue: "2026-07-01",
  344. handlingRecords: [
  345. { handler: "苏禧", handledAt: "2026-07-02 11:10", note: "补齐点位变更确认记录,并提交监察核查。", satisfaction: "待核查", feedback: "客户要求交付前再核对一次。" }
  346. ],
  347. timeline: ["客户反馈软装点位与最终确认稿不一致", "变更记录未固化到项目台账", "建议形成交付前核对清单"]
  348. },
  349. {
  350. id: "I-240706",
  351. dataTag: "测试",
  352. customerId: "C002",
  353. type: "客户满意度",
  354. source: "线下巡检",
  355. status: "已处理",
  356. owner: "何敏",
  357. score: 66,
  358. summary: "满意度低于 3 分,客户认为节点说明不够清晰。",
  359. rootCause: "客户期望与节点说明不一致",
  360. action: "已完成解释与节点确认",
  361. handlerNote: "监察部核查通过",
  362. postSatisfaction: "满意",
  363. submitter: "监察部",
  364. auditDue: "2026-06-29",
  365. handlingRecords: [
  366. { handler: "何敏", handledAt: "2026-06-29 15:00", note: "补充节点说明并重新确认计划。", satisfaction: "满意", feedback: "客户确认接受新计划。" }
  367. ],
  368. timeline: ["满意度低于 3 分触发预警", "设计师补充节点说明", "客户确认接受新计划,监察部标记已处理"]
  369. }
  370. ];
  371. let rulesState = {
  372. updatedAt: null,
  373. updatedBy: null
  374. };
  375. const dataFile = process.env.DATA_FILE || (
  376. process.platform === "win32"
  377. ? path.join(rootDir, "data", "app-state.json")
  378. : "/var/lib/keyissue-prototype/app-state.json"
  379. );
  380. let persistQueue = Promise.resolve();
  381. function getStateSnapshot() {
  382. return {
  383. version: 1,
  384. savedAt: new Date().toISOString(),
  385. users,
  386. roles,
  387. dictionaries,
  388. customers,
  389. customerDrafts,
  390. inspections,
  391. issues,
  392. rulesState
  393. };
  394. }
  395. function cloneDefault(value) {
  396. return JSON.parse(JSON.stringify(value));
  397. }
  398. function ensureRequiredRolesAndUsers() {
  399. let changed = false;
  400. Object.entries(requiredRoleDefaults).forEach(([roleKey, defaults]) => {
  401. if (!roles[roleKey]) {
  402. roles[roleKey] = cloneDefault(defaults);
  403. changed = true;
  404. return;
  405. }
  406. ["roleName", "summary", "pages", "permissions"].forEach((field) => {
  407. if (!roles[roleKey][field]) {
  408. roles[roleKey][field] = cloneDefault(defaults[field]);
  409. changed = true;
  410. return;
  411. }
  412. if (Array.isArray(defaults[field]) && Array.isArray(roles[roleKey][field])) {
  413. defaults[field].forEach((item) => {
  414. if (!roles[roleKey][field].includes(item)) {
  415. roles[roleKey][field].push(item);
  416. changed = true;
  417. }
  418. });
  419. }
  420. });
  421. });
  422. requiredUserDefaults.forEach((spec) => {
  423. if (users.some((user) => user.username === spec.username)) return;
  424. const managedUser = {
  425. id: nextUserId(),
  426. username: spec.username,
  427. displayName: spec.displayName,
  428. role: spec.role,
  429. status: "启用"
  430. };
  431. setUserPassword(managedUser, getInitialPassword(spec.username));
  432. users.push(managedUser);
  433. changed = true;
  434. });
  435. users.forEach((user) => {
  436. if (user.password && !user.passwordHash) {
  437. setUserPassword(user, user.password);
  438. changed = true;
  439. }
  440. if (user.registerSource !== "roster") return;
  441. const member = findRosterMemberForUser(user);
  442. if (!member) return;
  443. const nextRole = mapRosterMemberToRole(member);
  444. const nextDepartment = getRosterScopeDepartment(member);
  445. const nextPosition = member.position || "";
  446. if (user.role !== nextRole) {
  447. user.role = nextRole;
  448. changed = true;
  449. }
  450. if ((user.department || "") !== nextDepartment) {
  451. user.department = nextDepartment;
  452. changed = true;
  453. }
  454. if ((user.position || "") !== nextPosition) {
  455. user.position = nextPosition;
  456. changed = true;
  457. }
  458. });
  459. return changed;
  460. }
  461. function applyPersistedState(state) {
  462. if (!state || typeof state !== "object") return;
  463. if (Array.isArray(state.users)) users = state.users;
  464. if (state.roles && typeof state.roles === "object") roles = state.roles;
  465. if (state.dictionaries && typeof state.dictionaries === "object") dictionaries = state.dictionaries;
  466. if (Array.isArray(state.customers)) customers = state.customers;
  467. if (Array.isArray(state.customerDrafts)) customerDrafts = state.customerDrafts;
  468. if (Array.isArray(state.inspections)) inspections = state.inspections;
  469. if (Array.isArray(state.issues)) issues = state.issues;
  470. if (state.rulesState && typeof state.rulesState === "object") rulesState = state.rulesState;
  471. }
  472. async function loadState() {
  473. let loadedPersistedState = false;
  474. try {
  475. const raw = await fs.readFile(dataFile, "utf8");
  476. applyPersistedState(JSON.parse(raw));
  477. loadedPersistedState = true;
  478. console.log(`Loaded persisted data from ${dataFile}`);
  479. } catch (error) {
  480. if (error.code !== "ENOENT") throw error;
  481. }
  482. const changed = ensureRequiredRolesAndUsers();
  483. if (loadedPersistedState && changed) await persistState();
  484. }
  485. async function loadRoster() {
  486. const rosterFile = path.join(rootDir, "data", "roster-members.json");
  487. try {
  488. const raw = await fs.readFile(rosterFile, "utf8");
  489. const data = JSON.parse(raw);
  490. let activeDepartment = "";
  491. rosterMembers = (Array.isArray(data.members) ? data.members : []).map((member) => {
  492. const department = String(member.department || "").trim();
  493. if (department) activeDepartment = department;
  494. return {
  495. ...member,
  496. scopeDepartment: inferRosterScopeDepartment(member, activeDepartment)
  497. };
  498. });
  499. console.log(`Loaded roster whitelist: ${rosterMembers.length} members`);
  500. } catch (error) {
  501. if (error.code !== "ENOENT") throw error;
  502. rosterMembers = [];
  503. console.warn("Roster whitelist file not found; registration is disabled.");
  504. }
  505. }
  506. async function persistState() {
  507. persistQueue = persistQueue.catch(() => {}).then(async () => {
  508. await fs.mkdir(path.dirname(dataFile), { recursive: true });
  509. const tempFile = `${dataFile}.${process.pid}.tmp`;
  510. await fs.writeFile(tempFile, JSON.stringify(getStateSnapshot(), null, 2), "utf8");
  511. await fs.rename(tempFile, dataFile);
  512. });
  513. return persistQueue;
  514. }
  515. const mimeTypes = {
  516. ".html": "text/html; charset=utf-8",
  517. ".css": "text/css; charset=utf-8",
  518. ".js": "application/javascript; charset=utf-8",
  519. ".json": "application/json; charset=utf-8",
  520. ".md": "text/markdown; charset=utf-8",
  521. ".png": "image/png",
  522. ".jpg": "image/jpeg",
  523. ".jpeg": "image/jpeg",
  524. ".svg": "image/svg+xml; charset=utf-8",
  525. ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  526. };
  527. function sendJson(res, status, body) {
  528. const payload = JSON.stringify(body);
  529. res.writeHead(status, {
  530. "Content-Type": "application/json; charset=utf-8",
  531. "Content-Length": Buffer.byteLength(payload),
  532. "Cache-Control": "no-store"
  533. });
  534. res.end(payload);
  535. }
  536. function sendError(res, status, code, message) {
  537. sendJson(res, status, { error: code, message });
  538. }
  539. async function readBody(req) {
  540. let raw = "";
  541. for await (const chunk of req) raw += chunk;
  542. if (!raw) return {};
  543. try {
  544. return JSON.parse(raw);
  545. } catch {
  546. const error = new Error("请求体不是有效的 JSON。");
  547. error.status = 400;
  548. throw error;
  549. }
  550. }
  551. function publicUser(user) {
  552. const role = roles[user.role];
  553. return {
  554. id: user.id,
  555. loginName: user.username,
  556. userName: user.displayName,
  557. roleKey: user.role,
  558. roleName: role.roleName,
  559. status: user.status,
  560. pages: role.pages,
  561. permissions: role.permissions
  562. };
  563. }
  564. function publicManagedUser(user) {
  565. return {
  566. id: user.id,
  567. username: user.username,
  568. displayName: user.displayName,
  569. phone: user.phone || "",
  570. department: user.department || "",
  571. position: user.position || "",
  572. registerSource: user.registerSource || "",
  573. role: user.role,
  574. roleName: roles[user.role]?.roleName || user.role,
  575. status: user.status || "启用"
  576. };
  577. }
  578. function publicCustomerDraft(draft) {
  579. return {
  580. id: draft.id,
  581. dataTag: draft.dataTag,
  582. status: draft.status,
  583. submittedBy: draft.submittedBy,
  584. submittedRole: draft.submittedRole,
  585. submittedAt: draft.submittedAt,
  586. reviewedBy: draft.reviewedBy,
  587. reviewedAt: draft.reviewedAt,
  588. reviewNote: draft.reviewNote,
  589. customerId: draft.customerId,
  590. customer: draft.customer
  591. };
  592. }
  593. function createSessionToken() {
  594. if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
  595. return crypto.randomBytes(24).toString("hex");
  596. }
  597. function getBearerToken(req) {
  598. const header = req.headers.authorization || "";
  599. const [type, token] = header.split(" ");
  600. return type === "Bearer" ? token : "";
  601. }
  602. function getCurrentUser(req) {
  603. const token = getBearerToken(req);
  604. const userId = sessions.get(token);
  605. const user = users.find((item) => item.id === userId) || null;
  606. return user && user.status !== "停用" ? user : null;
  607. }
  608. function can(user, permission) {
  609. if (!user) return false;
  610. const role = roles[user.role];
  611. if (role.permissions.includes("*")) return true;
  612. if (permission.startsWith("page:")) return role.pages.includes(permission.replace("page:", ""));
  613. return role.permissions.includes(permission);
  614. }
  615. function normalizeTier(value) {
  616. const tier = String(value || "").trim().toUpperCase();
  617. return customerTiers.includes(tier) ? tier : null;
  618. }
  619. function normalizeProgressStatus(value) {
  620. const status = String(value || "").trim();
  621. return dictionaries.progressStatuses.includes(status) ? status : dictionaries.progressStatuses[0] || "正常";
  622. }
  623. function normalizeName(value) {
  624. return String(value || "").trim().replace(/\s+/g, "");
  625. }
  626. function normalizePhone(value) {
  627. return String(value || "").replace(/\D/g, "");
  628. }
  629. function normalizeDepartment(value) {
  630. return normalizeName(value)
  631. .replace(/筑境/g, "")
  632. .replace(/设计工作室/g, "")
  633. .replace(/工作室/g, "")
  634. .replace(/中心/g, "")
  635. .replace(/部门/g, "部");
  636. }
  637. function getRosterScopeDepartment(member) {
  638. return String(member?.scopeDepartment || member?.department || "").trim();
  639. }
  640. function inferRosterScopeDepartment(member, fallbackDepartment = "") {
  641. const department = String(member?.department || "").trim();
  642. if (department) return department;
  643. const position = String(member?.position || "").trim();
  644. if (/运营副总|总经理助理|AI工程师/.test(position)) return "总经办";
  645. if (/监察/.test(position)) return "监察部";
  646. if (/客管|客户经理/.test(position)) return String(fallbackDepartment || "客管部").trim();
  647. if (/设计经理|设计总监|设计师|深化设计|灯光设计|定制设计/.test(position)) return String(fallbackDepartment || "设计部").trim();
  648. if (/交付总监|交付经理|交付助理|产业交付/.test(position)) return String(fallbackDepartment || "交付部").trim();
  649. if (/供应链|采购|库管|结算|跟单|主材|辅材|定制/.test(position)) return String(fallbackDepartment || "供应链中心").trim();
  650. return String(fallbackDepartment || "").trim();
  651. }
  652. function getCustomerOwnerName(customer) {
  653. return String(customer?.customerOwner || "")
  654. .replace(/^客管负责人[-—::]?/, "")
  655. .replace(/^客户负责人[-—::]?/, "")
  656. .trim();
  657. }
  658. function findRosterMemberByName(name) {
  659. const target = normalizeName(name);
  660. if (!target) return null;
  661. return rosterMembers.find((member) => normalizeName(member.name) === target) || null;
  662. }
  663. function findRosterMemberForUser(user) {
  664. if (!user) return null;
  665. const phone = normalizePhone(user.phone || user.username);
  666. if (phone) {
  667. const byPhone = rosterMembers.find((member) => normalizePhone(member.phone) === phone);
  668. if (byPhone) return byPhone;
  669. }
  670. return findRosterMemberByName(user.displayName);
  671. }
  672. function userScopeDepartment(user) {
  673. return String(user?.department || getRosterScopeDepartment(findRosterMemberForUser(user))).trim();
  674. }
  675. function sameDepartmentGroup(left, right) {
  676. const a = normalizeDepartment(left);
  677. const b = normalizeDepartment(right);
  678. if (!a || !b) return false;
  679. return a === b || a.includes(b) || b.includes(a);
  680. }
  681. function personMatchesField(field, personName) {
  682. const fieldText = normalizeName(field);
  683. const name = normalizeName(personName);
  684. if (!fieldText || !name) return false;
  685. return fieldText === name || fieldText.includes(name) || name.includes(fieldText);
  686. }
  687. function mapRosterMemberToRole(member) {
  688. const text = `${getRosterScopeDepartment(member)} ${member.position || ""}`;
  689. if (/供应链/.test(text)) return "supplyChain";
  690. if (/监察|质检/.test(text)) return "auditor";
  691. if (/交付总监|交付经理/.test(text)) return "deliveryManager";
  692. if (/客管经理|用户服务经理/.test(text)) return "customerManager";
  693. if (/客管|客户经理|家访师|用户服务/.test(text)) return "customerOwner";
  694. if (/设计总监|设计经理|定制设计主管/.test(text)) return "designManager";
  695. if (/设计师|深化设计|灯光设计|定制设计/.test(text)) return "designer";
  696. if (/监察|质检/.test(text)) return "auditor";
  697. if (/董事长|总经理|副总|总监|管理型|AI工程师/.test(text)) return "executive";
  698. if (/交付助理|产业交付|销售/.test(text)) return "delivery";
  699. if (/项目|工种|售后|安装|采购|库管|结算|跟单|主材|辅材|主管|技师/.test(text)) return "manager";
  700. return "delivery";
  701. }
  702. function findRosterMember(name, phone) {
  703. const targetName = normalizeName(name);
  704. const targetPhone = normalizePhone(phone);
  705. if (!targetName || !targetPhone) return null;
  706. return rosterMembers.find((member) => (
  707. normalizeName(member.name) === targetName &&
  708. normalizePhone(member.phone) === targetPhone
  709. )) || null;
  710. }
  711. function publicRosterMember(member) {
  712. return {
  713. name: member.name,
  714. department: getRosterScopeDepartment(member),
  715. position: member.position || "",
  716. role: mapRosterMemberToRole(member),
  717. roleName: roles[mapRosterMemberToRole(member)]?.roleName || mapRosterMemberToRole(member)
  718. };
  719. }
  720. function normalizeCustomerKeyPart(value) {
  721. return String(value || "").trim().replace(/\s+/g, " ").toLowerCase();
  722. }
  723. function customerIdentityKey(customer) {
  724. return [
  725. customer.name,
  726. customer.address,
  727. customer.tier,
  728. customer.manager,
  729. customer.startDate,
  730. customer.plannedFinishDate
  731. ].map(normalizeCustomerKeyPart).join("|");
  732. }
  733. function findDuplicateCustomer(candidate) {
  734. const key = customerIdentityKey(candidate);
  735. return customers.find((customer) => customerIdentityKey(customer) === key);
  736. }
  737. function findDuplicateCustomerDraft(candidate) {
  738. const key = customerIdentityKey(candidate);
  739. return customerDrafts.find((draft) => {
  740. if (!["待审核", "已通过"].includes(draft.status)) return false;
  741. return draft.customer && customerIdentityKey(draft.customer) === key;
  742. });
  743. }
  744. function nowText() {
  745. const now = new Date();
  746. const pad = (value) => String(value).padStart(2, "0");
  747. return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
  748. }
  749. function requireAuth(req, res) {
  750. const user = getCurrentUser(req);
  751. if (!user) {
  752. sendError(res, 401, "UNAUTHORIZED", "请先登录后再操作。");
  753. return null;
  754. }
  755. return user;
  756. }
  757. function requirePermission(req, res, permission) {
  758. const user = requireAuth(req, res);
  759. if (!user) return null;
  760. if (!can(user, permission)) {
  761. sendError(res, 403, "NO_PERMISSION", "当前登录人没有后台授权,操作已被服务器拒绝。");
  762. return null;
  763. }
  764. return user;
  765. }
  766. function nextCustomerId() {
  767. const max = customers.reduce((acc, customer) => Math.max(acc, Number(customer.id.slice(1)) || 0), 0);
  768. return `C${String(max + 1).padStart(3, "0")}`;
  769. }
  770. function nextCustomerDraftId() {
  771. const max = customerDrafts.reduce((acc, draft) => Math.max(acc, Number(draft.id.replace("CR-", "")) || 240700), 240700);
  772. return `CR-${max + 1}`;
  773. }
  774. function nextUserId() {
  775. const max = users.reduce((acc, user) => Math.max(acc, Number(user.id.slice(1)) || 0), 0);
  776. return `U${String(max + 1).padStart(3, "0")}`;
  777. }
  778. function normalizeStringList(value) {
  779. const list = Array.isArray(value) ? value : String(value || "").split(/[,,\n]/);
  780. return [...new Set(list.map((item) => String(item || "").trim()).filter(Boolean))];
  781. }
  782. function normalizeRolePages(pages) {
  783. const allowed = new Set(pageCatalog.map((page) => page.key));
  784. return normalizeStringList(pages).filter((page) => allowed.has(page));
  785. }
  786. function normalizeRolePermissions(permissions) {
  787. const allowed = new Set(permissionCatalog.map((permission) => permission.key));
  788. const list = normalizeStringList(permissions);
  789. if (list.includes("*")) return ["*"];
  790. return list.filter((permission) => allowed.has(permission));
  791. }
  792. function nextIssueId() {
  793. const max = issues.reduce((acc, issue) => Math.max(acc, Number(issue.id.replace("I-", "")) || 240700), 240700);
  794. return `I-${max + 1}`;
  795. }
  796. function nextInspectionId() {
  797. const max = inspections.reduce((acc, item) => Math.max(acc, Number(item.id.replace("R-", "")) || 240700), 240700);
  798. return `R-${max + 1}`;
  799. }
  800. function buildCustomerCandidate(body) {
  801. const tier = normalizeTier(body.tier);
  802. if (!tier) {
  803. const error = new Error("本系统仅保留 A / A+ 客户,请选择 A 或 A+。");
  804. error.status = 400;
  805. error.code = "INVALID_TIER";
  806. throw error;
  807. }
  808. const candidate = {
  809. name: String(body.name || "").trim(),
  810. tier,
  811. source: String(body.source || "待确认").trim(),
  812. keyCustomerReason: String(body.keyCustomerReason || "").trim(),
  813. customerOwner: String(body.customerOwner || "").trim(),
  814. address: String(body.address || "").trim(),
  815. department: String(body.department || "待确认").trim(),
  816. designer: String(body.designer || "").trim(),
  817. manager: String(body.manager || "").trim(),
  818. stage: String(body.stage || "待确认").trim(),
  819. startDate: String(body.startDate || "").trim() || "-",
  820. plannedFinishDate: String(body.plannedFinishDate || "").trim() || "-",
  821. progressStatus: normalizeProgressStatus(body.progressStatus),
  822. satisfaction: Number(body.satisfaction || 5)
  823. };
  824. if (!candidate.name || !candidate.address || !candidate.designer || !candidate.manager || !candidate.customerOwner || !candidate.keyCustomerReason) {
  825. const error = new Error("请补全客户姓名、地址、设计师、项目经理、客管人员负责人和重点客户入选说明。");
  826. error.status = 400;
  827. error.code = "INVALID_CUSTOMER";
  828. throw error;
  829. }
  830. return candidate;
  831. }
  832. function promoteCustomer(candidate, reviewer) {
  833. return {
  834. id: nextCustomerId(),
  835. ...candidate,
  836. approvedBy: reviewer.displayName,
  837. approvedAt: nowText(),
  838. inspectionStatus: "未巡查",
  839. inspectionAt: "-",
  840. inspector: "-",
  841. inspectionSummary: "新建客户,等待首次每日线上巡查。"
  842. };
  843. }
  844. function scoreByStatus(status) {
  845. if (status === "红色预警") return 90;
  846. if (status === "橙色预警") return 78;
  847. return 62;
  848. }
  849. function findIssue(res, issueId) {
  850. const issue = issues.find((item) => item.id === issueId);
  851. if (!issue) sendError(res, 404, "NOT_FOUND", "未找到对应的问题反馈。");
  852. return issue;
  853. }
  854. function hasFullCustomerAccess(user) {
  855. return ["admin", "executive", "auditor", "supplyChain", "deliveryManager", "delivery"].includes(user?.role);
  856. }
  857. function canAccessCustomer(user, customer) {
  858. if (!user || !customer) return false;
  859. if (hasFullCustomerAccess(user)) return true;
  860. if (user.role === "designer") {
  861. return personMatchesField(customer.designer, user.displayName);
  862. }
  863. if (user.role === "designManager") {
  864. const department = userScopeDepartment(user);
  865. const designerMember = findRosterMemberByName(customer.designer);
  866. const customerDesignerDepartment = getRosterScopeDepartment(designerMember) || customer.department;
  867. return sameDepartmentGroup(department, customerDesignerDepartment);
  868. }
  869. if (user.role === "manager") {
  870. return personMatchesField(customer.manager, user.displayName);
  871. }
  872. if (user.role === "customerOwner") {
  873. return personMatchesField(customer.customerOwner, user.displayName);
  874. }
  875. if (user.role === "customerManager") {
  876. const department = userScopeDepartment(user);
  877. const ownerMember = findRosterMemberByName(getCustomerOwnerName(customer));
  878. return sameDepartmentGroup(department, getRosterScopeDepartment(ownerMember));
  879. }
  880. return false;
  881. }
  882. function visibleCustomersFor(user) {
  883. return customers.filter((customer) => canAccessCustomer(user, customer));
  884. }
  885. function visibleCustomerIdSet(user) {
  886. return new Set(visibleCustomersFor(user).map((customer) => customer.id));
  887. }
  888. function canAccessIssue(user, issue) {
  889. const customer = customers.find((item) => item.id === issue?.customerId);
  890. return canAccessCustomer(user, customer);
  891. }
  892. function visibleIssuesFor(user) {
  893. return issues.filter((issue) => canAccessIssue(user, issue));
  894. }
  895. function visibleInspectionsFor(user) {
  896. const customerIds = visibleCustomerIdSet(user);
  897. return inspections.filter((inspection) => customerIds.has(inspection.customerId));
  898. }
  899. function requireCustomerAccess(user, res, customer) {
  900. if (canAccessCustomer(user, customer)) return true;
  901. sendError(res, 403, "CUSTOMER_SCOPE_DENIED", "当前登录人无权查看或操作该客户。");
  902. return false;
  903. }
  904. function requireIssueAccess(user, res, issue) {
  905. if (canAccessIssue(user, issue)) return true;
  906. sendError(res, 403, "CUSTOMER_SCOPE_DENIED", "当前登录人无权查看或操作该客户的问题反馈。");
  907. return false;
  908. }
  909. async function handleApi(req, res, pathname) {
  910. if (req.method === "GET" && pathname === "/api/auth/options") {
  911. sendJson(res, 200, {
  912. accounts: users.filter((user) => user.status !== "停用").map((user) => ({
  913. displayName: user.displayName,
  914. roleName: roles[user.role].roleName,
  915. summary: roles[user.role].summary
  916. }))
  917. });
  918. return;
  919. }
  920. if (req.method === "POST" && pathname === "/api/auth/login") {
  921. const body = await readBody(req);
  922. const user = users.find((item) => item.username === body.username && item.status !== "停用" && verifyUserPassword(item, body.password));
  923. if (!user) {
  924. sendError(res, 401, "LOGIN_FAILED", "账号或密码错误。");
  925. return;
  926. }
  927. const token = createSessionToken();
  928. sessions.set(token, user.id);
  929. sendJson(res, 200, { token, user: publicUser(user) });
  930. return;
  931. }
  932. if (req.method === "POST" && pathname === "/api/auth/register") {
  933. const body = await readBody(req);
  934. const name = normalizeName(body.name);
  935. const phone = normalizePhone(body.phone);
  936. const password = String(body.password || "").trim();
  937. const confirmPassword = body.confirmPassword == null ? password : String(body.confirmPassword || "").trim();
  938. if (!name || !phone || !password) {
  939. sendError(res, 400, "INVALID_REGISTER_INFO", "请填写姓名、手机号和登录密码。");
  940. return;
  941. }
  942. if (password.length < 6) {
  943. sendError(res, 400, "INVALID_PASSWORD", "密码至少需要 6 位。");
  944. return;
  945. }
  946. if (password !== confirmPassword) {
  947. sendError(res, 400, "PASSWORD_NOT_MATCH", "两次输入的密码不一致。");
  948. return;
  949. }
  950. const member = findRosterMember(name, phone);
  951. if (!member) {
  952. sendError(res, 403, "ROSTER_NOT_MATCHED", "姓名和手机号未在名册中匹配,暂不能注册。");
  953. return;
  954. }
  955. const role = mapRosterMemberToRole(member);
  956. let user = users.find((item) => normalizePhone(item.phone || item.username) === phone);
  957. if (user && normalizeName(user.displayName) !== normalizeName(member.name)) {
  958. sendError(res, 409, "PHONE_ALREADY_REGISTERED", "该手机号已被其他账号注册,请联系系统管理员。");
  959. return;
  960. }
  961. if (user) {
  962. if (user.status === "停用") {
  963. sendError(res, 403, "USER_DISABLED", "该账号已停用,请联系系统管理员。");
  964. return;
  965. }
  966. sendError(res, 409, "USER_ALREADY_REGISTERED", "该手机号已完成注册,请直接使用手机号和已设置密码登录,或联系系统管理员重置密码。");
  967. return;
  968. }
  969. user = {
  970. id: nextUserId(),
  971. username: phone,
  972. displayName: member.name,
  973. phone,
  974. department: getRosterScopeDepartment(member),
  975. position: member.position || "",
  976. role,
  977. status: "启用",
  978. registerSource: "roster"
  979. };
  980. setUserPassword(user, password);
  981. users.push(user);
  982. await persistState();
  983. const token = createSessionToken();
  984. sessions.set(token, user.id);
  985. sendJson(res, 201, {
  986. token,
  987. user: publicUser(user),
  988. roster: publicRosterMember(member),
  989. message: `注册成功,已自动匹配为${roles[role]?.roleName || role}。登录账号为手机号,请使用刚设置的密码登录。`
  990. });
  991. return;
  992. }
  993. if (req.method === "GET" && pathname === "/api/auth/me") {
  994. const user = requireAuth(req, res);
  995. if (!user) return;
  996. sendJson(res, 200, { user: publicUser(user) });
  997. return;
  998. }
  999. if (req.method === "POST" && pathname === "/api/auth/logout") {
  1000. const token = getBearerToken(req);
  1001. if (token) sessions.delete(token);
  1002. sendJson(res, 200, { message: "已退出登录。" });
  1003. return;
  1004. }
  1005. if (req.method === "GET" && pathname === "/api/meta") {
  1006. const user = requireAuth(req, res);
  1007. if (!user) return;
  1008. sendJson(res, 200, { dictionaries });
  1009. return;
  1010. }
  1011. if (req.method === "GET" && pathname === "/api/admin/config") {
  1012. const user = requirePermission(req, res, "admin:manage");
  1013. if (!user) return;
  1014. sendJson(res, 200, {
  1015. users: users.map(publicManagedUser),
  1016. roles,
  1017. pageCatalog,
  1018. permissionCatalog,
  1019. customerDrafts: customerDrafts.map(publicCustomerDraft),
  1020. dictionaries,
  1021. rules: rulesState
  1022. });
  1023. return;
  1024. }
  1025. if (req.method === "POST" && pathname === "/api/admin/users") {
  1026. const user = requirePermission(req, res, "admin:manage");
  1027. if (!user) return;
  1028. const body = await readBody(req);
  1029. const username = String(body.username || "").trim();
  1030. const password = String(body.password || "").trim();
  1031. const displayName = String(body.displayName || "").trim();
  1032. const role = String(body.role || "").trim();
  1033. if (!username || !password || !displayName || !roles[role]) {
  1034. sendError(res, 400, "INVALID_USER", "请补全账号、密码、姓名,并选择有效角色。");
  1035. return;
  1036. }
  1037. if (users.some((item) => item.username === username)) {
  1038. sendError(res, 400, "DUPLICATE_USER", "该登录账号已存在。");
  1039. return;
  1040. }
  1041. const managedUser = {
  1042. id: nextUserId(),
  1043. username,
  1044. displayName,
  1045. phone: normalizePhone(body.phone),
  1046. department: String(body.department || "").trim(),
  1047. position: String(body.position || "").trim(),
  1048. role,
  1049. status: body.status === "停用" ? "停用" : "启用"
  1050. };
  1051. setUserPassword(managedUser, password);
  1052. users.push(managedUser);
  1053. await persistState();
  1054. sendJson(res, 201, { user: publicManagedUser(managedUser), message: "后台账号已创建。" });
  1055. return;
  1056. }
  1057. const adminUserMatch = pathname.match(/^\/api\/admin\/users\/([^/]+)$/);
  1058. if (req.method === "PATCH" && adminUserMatch) {
  1059. const operator = requirePermission(req, res, "admin:manage");
  1060. if (!operator) return;
  1061. const target = users.find((item) => item.id === decodeURIComponent(adminUserMatch[1]));
  1062. if (!target) {
  1063. sendError(res, 404, "NOT_FOUND", "未找到对应账号。");
  1064. return;
  1065. }
  1066. const body = await readBody(req);
  1067. if (body.username !== undefined) {
  1068. const username = String(body.username || "").trim();
  1069. if (!username) {
  1070. sendError(res, 400, "INVALID_USER", "登录账号不能为空。");
  1071. return;
  1072. }
  1073. if (users.some((item) => item.id !== target.id && item.username === username)) {
  1074. sendError(res, 400, "DUPLICATE_USER", "该登录账号已存在。");
  1075. return;
  1076. }
  1077. target.username = username;
  1078. }
  1079. if (body.password !== undefined) {
  1080. const nextPassword = String(body.password || "").trim();
  1081. if (nextPassword) setUserPassword(target, nextPassword);
  1082. }
  1083. if (body.displayName !== undefined) target.displayName = String(body.displayName || "").trim() || target.displayName;
  1084. if (body.phone !== undefined) target.phone = normalizePhone(body.phone);
  1085. if (body.department !== undefined) target.department = String(body.department || "").trim();
  1086. if (body.position !== undefined) target.position = String(body.position || "").trim();
  1087. if (body.role !== undefined) {
  1088. const role = String(body.role || "").trim();
  1089. if (!roles[role]) {
  1090. sendError(res, 400, "INVALID_ROLE", "请选择有效角色。");
  1091. return;
  1092. }
  1093. target.role = role;
  1094. }
  1095. if (body.status !== undefined) {
  1096. const nextStatus = body.status === "停用" ? "停用" : "启用";
  1097. const activeAdmins = users.filter((item) => item.status !== "停用" && item.role === "admin");
  1098. if (target.role === "admin" && nextStatus === "停用" && activeAdmins.length <= 1 && activeAdmins[0]?.id === target.id) {
  1099. sendError(res, 400, "LAST_ADMIN", "至少保留一个启用的系统管理员。");
  1100. return;
  1101. }
  1102. target.status = nextStatus;
  1103. if (nextStatus === "停用") {
  1104. [...sessions.entries()].forEach(([token, userId]) => {
  1105. if (userId === target.id) sessions.delete(token);
  1106. });
  1107. }
  1108. }
  1109. await persistState();
  1110. sendJson(res, 200, { user: publicManagedUser(target), message: "账号信息已由后台更新。" });
  1111. return;
  1112. }
  1113. const adminRoleMatch = pathname.match(/^\/api\/admin\/roles\/([^/]+)$/);
  1114. if (req.method === "PATCH" && adminRoleMatch) {
  1115. const user = requirePermission(req, res, "admin:manage");
  1116. if (!user) return;
  1117. const roleKey = decodeURIComponent(adminRoleMatch[1]);
  1118. const role = roles[roleKey];
  1119. if (!role) {
  1120. sendError(res, 404, "NOT_FOUND", "未找到对应角色。");
  1121. return;
  1122. }
  1123. const body = await readBody(req);
  1124. if (body.roleName !== undefined) role.roleName = String(body.roleName || "").trim() || role.roleName;
  1125. if (body.summary !== undefined) role.summary = String(body.summary || "").trim() || role.summary;
  1126. if (roleKey === "admin") {
  1127. role.pages = pageCatalog.map((page) => page.key);
  1128. role.permissions = ["*", "admin:manage"];
  1129. } else {
  1130. if (body.pages !== undefined) role.pages = normalizeRolePages(body.pages);
  1131. if (body.permissions !== undefined) role.permissions = normalizeRolePermissions(body.permissions);
  1132. }
  1133. await persistState();
  1134. sendJson(res, 200, { roleKey, role, message: "角色权限已由后台更新。" });
  1135. return;
  1136. }
  1137. if (req.method === "PATCH" && pathname === "/api/admin/dictionaries") {
  1138. const user = requirePermission(req, res, "admin:manage");
  1139. if (!user) return;
  1140. const body = await readBody(req);
  1141. Object.keys(dictionaries).forEach((key) => {
  1142. if (body[key] !== undefined) {
  1143. const values = normalizeStringList(body[key]);
  1144. if (values.length) dictionaries[key] = values;
  1145. }
  1146. });
  1147. await persistState();
  1148. sendJson(res, 200, { dictionaries, message: "基础字典已由后台更新。" });
  1149. return;
  1150. }
  1151. const customerDraftMatch = pathname.match(/^\/api\/admin\/customer-drafts\/([^/]+)$/);
  1152. if (req.method === "PATCH" && customerDraftMatch) {
  1153. const user = requirePermission(req, res, "customer:approve");
  1154. if (!user) return;
  1155. const draft = customerDrafts.find((item) => item.id === decodeURIComponent(customerDraftMatch[1]));
  1156. if (!draft) {
  1157. sendError(res, 404, "NOT_FOUND", "未找到对应的客户入池审核单。");
  1158. return;
  1159. }
  1160. if (draft.status !== "待审核") {
  1161. sendError(res, 400, "ALREADY_REVIEWED", "该客户入池申请已经审核完成。");
  1162. return;
  1163. }
  1164. const body = await readBody(req);
  1165. const action = String(body.action || "").trim();
  1166. draft.reviewedBy = user.displayName;
  1167. draft.reviewedAt = nowText();
  1168. draft.reviewNote = String(body.reviewNote || "").trim();
  1169. if (action === "approve") {
  1170. const duplicateCustomer = findDuplicateCustomer(draft.customer);
  1171. if (duplicateCustomer) {
  1172. draft.status = "已通过";
  1173. draft.customerId = duplicateCustomer.id;
  1174. await persistState();
  1175. sendJson(res, 200, { customer: duplicateCustomer, draft: publicCustomerDraft(draft), message: "已存在相同客户档案,本次未重复入库。" });
  1176. return;
  1177. }
  1178. const customer = promoteCustomer(draft.customer, user);
  1179. customers.unshift(customer);
  1180. draft.status = "已通过";
  1181. draft.customerId = customer.id;
  1182. await persistState();
  1183. sendJson(res, 200, { customer, draft: publicCustomerDraft(draft), message: "客户入池申请已通过,客户已进入 A/A+ 客户库。" });
  1184. return;
  1185. }
  1186. if (action === "reject") {
  1187. draft.status = "已驳回";
  1188. await persistState();
  1189. sendJson(res, 200, { draft: publicCustomerDraft(draft), message: "客户入池申请已驳回,未进入客户库。" });
  1190. return;
  1191. }
  1192. sendError(res, 400, "INVALID_ACTION", "请选择通过或驳回该客户入池申请。");
  1193. return;
  1194. }
  1195. if (req.method === "GET" && pathname === "/api/customers") {
  1196. const user = requireAuth(req, res);
  1197. if (!user) return;
  1198. sendJson(res, 200, { customers: visibleCustomersFor(user) });
  1199. return;
  1200. }
  1201. if (req.method === "POST" && pathname === "/api/customers") {
  1202. const user = requirePermission(req, res, "customer:submit");
  1203. if (!user) return;
  1204. const body = await readBody(req);
  1205. let candidate;
  1206. try {
  1207. candidate = buildCustomerCandidate(body);
  1208. } catch (error) {
  1209. sendError(res, error.status || 400, error.code || "INVALID_CUSTOMER", error.message);
  1210. return;
  1211. }
  1212. const duplicateCustomer = findDuplicateCustomer(candidate);
  1213. if (duplicateCustomer) {
  1214. const payload = { message: "已存在相同客户档案,本次未重复入库。" };
  1215. if (canAccessCustomer(user, duplicateCustomer)) payload.customer = duplicateCustomer;
  1216. sendJson(res, 200, payload);
  1217. return;
  1218. }
  1219. const duplicateDraft = findDuplicateCustomerDraft(candidate);
  1220. if (duplicateDraft) {
  1221. sendJson(res, 200, { draft: publicCustomerDraft(duplicateDraft), message: "已存在相同客户入池申请,本次未重复提交。" });
  1222. return;
  1223. }
  1224. if (user.role === "admin") {
  1225. const customer = promoteCustomer(candidate, user);
  1226. customers.unshift(customer);
  1227. await persistState();
  1228. sendJson(res, 201, { customer, message: "管理员已直接保存客户档案,客户已进入 A/A+ 客户库。" });
  1229. return;
  1230. }
  1231. const draft = {
  1232. id: nextCustomerDraftId(),
  1233. status: "待审核",
  1234. submittedBy: user.displayName,
  1235. submittedRole: roles[user.role]?.roleName || user.role,
  1236. submittedAt: nowText(),
  1237. reviewedBy: "",
  1238. reviewedAt: "",
  1239. reviewNote: "",
  1240. customerId: "",
  1241. customer: candidate
  1242. };
  1243. customerDrafts.unshift(draft);
  1244. await persistState();
  1245. sendJson(res, 202, { draft: publicCustomerDraft(draft), message: "客户入池申请已提交,等待管理员审核后进入客户库。" });
  1246. return;
  1247. }
  1248. if (req.method === "GET" && pathname === "/api/inspections") {
  1249. const user = requirePermission(req, res, "inspection:view");
  1250. if (!user) return;
  1251. sendJson(res, 200, { inspections: visibleInspectionsFor(user) });
  1252. return;
  1253. }
  1254. if (req.method === "POST" && pathname === "/api/inspections") {
  1255. const user = requirePermission(req, res, "inspection:create");
  1256. if (!user) return;
  1257. const body = await readBody(req);
  1258. const customer = customers.find((item) => item.id === body.customerId);
  1259. if (!customer) {
  1260. sendError(res, 400, "INVALID_CUSTOMER", "请选择有效的巡查客户。");
  1261. return;
  1262. }
  1263. if (!requireCustomerAccess(user, res, customer)) return;
  1264. const status = ["已巡查", "未巡查", "异常"].includes(body.status) ? body.status : "已巡查";
  1265. const inspection = {
  1266. id: nextInspectionId(),
  1267. customerId: customer.id,
  1268. customerName: customer.name,
  1269. inspector: String(body.inspector || user.displayName).trim(),
  1270. status,
  1271. createdAt: nowText(),
  1272. siteQuality: String(body.siteQuality || "未填写").trim(),
  1273. civility: String(body.civility || "未填写").trim(),
  1274. groupFeedback: String(body.groupFeedback || "未填写").trim(),
  1275. summary: String(body.summary || "每日线上巡查已记录。").trim()
  1276. };
  1277. inspections.unshift(inspection);
  1278. customer.inspectionStatus = inspection.status;
  1279. customer.inspectionAt = inspection.createdAt;
  1280. customer.inspector = inspection.inspector;
  1281. customer.inspectionSummary = inspection.summary;
  1282. await persistState();
  1283. sendJson(res, 201, { inspection, message: "每日线上巡查已提交。" });
  1284. return;
  1285. }
  1286. if (req.method === "GET" && pathname === "/api/issues") {
  1287. const user = requirePermission(req, res, "issue:view");
  1288. if (!user) return;
  1289. sendJson(res, 200, { issues: visibleIssuesFor(user) });
  1290. return;
  1291. }
  1292. if (req.method === "POST" && pathname === "/api/issues") {
  1293. const user = requirePermission(req, res, "issue:create");
  1294. if (!user) return;
  1295. const body = await readBody(req);
  1296. const customer = customers.find((item) => item.id === body.customerId);
  1297. if (!customer) {
  1298. sendError(res, 400, "INVALID_CUSTOMER", "请选择有效的关联客户。");
  1299. return;
  1300. }
  1301. if (!requireCustomerAccess(user, res, customer)) return;
  1302. const status = body.status || "蓝色关注";
  1303. const issue = {
  1304. id: nextIssueId(),
  1305. customerId: customer.id,
  1306. type: body.type || "客户反馈",
  1307. source: body.source || dictionaries.issueSources[0] || "线下巡检",
  1308. status,
  1309. owner: String(body.owner || "").trim() || customer.manager,
  1310. score: scoreByStatus(status),
  1311. summary: String(body.summary || "客户反馈已记录").trim(),
  1312. rootCause: String(body.rootCause || "待责任人补充根因分析").trim(),
  1313. action: "新反馈已进入预警池,等待责任人确认处置动作",
  1314. handlerNote: "待提交处置记录",
  1315. postSatisfaction: "待处置",
  1316. submitter: user.displayName,
  1317. auditDue: body.auditDue || "",
  1318. handlingRecords: [],
  1319. timeline: [
  1320. String(body.summary || "客户反馈已记录").trim(),
  1321. String(body.rootCause || "待补充根因分析").trim(),
  1322. "系统已生成预警并同步至问题反馈池"
  1323. ]
  1324. };
  1325. issues.unshift(issue);
  1326. await persistState();
  1327. sendJson(res, 201, { issue, message: "问题反馈已由后台提交,并进入预警池。" });
  1328. return;
  1329. }
  1330. const issueActionMatch = pathname.match(/^\/api\/issues\/([^/]+)\/([^/]+)$/);
  1331. if (req.method === "PATCH" && issueActionMatch) {
  1332. const [, encodedIssueId, action] = issueActionMatch;
  1333. const issueId = decodeURIComponent(encodedIssueId);
  1334. const issue = findIssue(res, issueId);
  1335. if (!issue) return;
  1336. const body = await readBody(req);
  1337. if (action === "assign") {
  1338. const user = requirePermission(req, res, "issue:assign");
  1339. if (!user) return;
  1340. if (!requireIssueAccess(user, res, issue)) return;
  1341. issue.owner = String(body.owner || "部门负责人").trim();
  1342. issue.action = "已由管理层指派责任人牵头处理";
  1343. issue.timeline.push(`${user.displayName} 指派 ${issue.owner} 负责闭环`);
  1344. await persistState();
  1345. sendJson(res, 200, { issue, message: "后台已完成责任人指派。" });
  1346. return;
  1347. }
  1348. if (action === "quick-handle") {
  1349. const user = requirePermission(req, res, "issue:handle");
  1350. if (!user) return;
  1351. if (!requireIssueAccess(user, res, issue)) return;
  1352. issue.status = "处理中";
  1353. issue.action = "已进入处置流程,等待补充根因与整改记录";
  1354. issue.handlerNote = `${user.displayName} 已接收并开始处置`;
  1355. issue.handlingRecords = issue.handlingRecords || [];
  1356. issue.handlingRecords.push({
  1357. handler: user.displayName,
  1358. handledAt: nowText(),
  1359. note: "接收问题并进入处置流程。",
  1360. satisfaction: issue.postSatisfaction || "待处置",
  1361. feedback: "等待补充处理方案。"
  1362. });
  1363. issue.timeline.push(`${user.displayName} 接收问题并进入处置流程`);
  1364. await persistState();
  1365. sendJson(res, 200, { issue, message: "后台已将问题切换为处理中。" });
  1366. return;
  1367. }
  1368. if (action === "handle") {
  1369. const user = requirePermission(req, res, "issue:handle");
  1370. if (!user) return;
  1371. if (!requireIssueAccess(user, res, issue)) return;
  1372. issue.status = "待核查";
  1373. issue.action = "处置完成,等待监察核查";
  1374. issue.handlerNote = String(body.handlerNote || "项目经理已提交处置记录").trim();
  1375. issue.postSatisfaction = String(body.satisfaction || "待回访").trim();
  1376. issue.handlingRecords = issue.handlingRecords || [];
  1377. issue.handlingRecords.push({
  1378. handler: user.displayName,
  1379. handledAt: nowText(),
  1380. note: issue.handlerNote,
  1381. satisfaction: issue.postSatisfaction,
  1382. feedback: String(body.customerFeedback || "待客户回访确认").trim()
  1383. });
  1384. issue.timeline.push(`${user.displayName} 提交处置记录,处置后满意度:${issue.postSatisfaction}`);
  1385. await persistState();
  1386. sendJson(res, 200, { issue, message: "处置记录已提交,后台已转入待核查。" });
  1387. return;
  1388. }
  1389. if (action === "audit-pass") {
  1390. const user = requirePermission(req, res, "audit:review");
  1391. if (!user) return;
  1392. if (!requireIssueAccess(user, res, issue)) return;
  1393. issue.status = "已处理";
  1394. issue.action = "监察核查通过,问题已处理";
  1395. issue.handlerNote = "监察部核查通过";
  1396. issue.timeline.push(`${user.displayName} 核查通过,问题已处理`);
  1397. await persistState();
  1398. sendJson(res, 200, { issue, message: "监察核查已通过,问题已处理。" });
  1399. return;
  1400. }
  1401. if (action === "audit-reject") {
  1402. const user = requirePermission(req, res, "audit:review");
  1403. if (!user) return;
  1404. if (!requireIssueAccess(user, res, issue)) return;
  1405. issue.status = "红色预警";
  1406. issue.action = "监察驳回,需重新提交整改方案";
  1407. issue.handlerNote = "监察部驳回整改";
  1408. issue.timeline.push(`${user.displayName} 驳回整改,问题回到红色预警`);
  1409. await persistState();
  1410. sendJson(res, 200, { issue, message: "已驳回整改,问题回到红色预警。" });
  1411. return;
  1412. }
  1413. }
  1414. if (req.method === "POST" && pathname === "/api/rules") {
  1415. const user = requirePermission(req, res, "rules:edit");
  1416. if (!user) return;
  1417. rulesState = {
  1418. updatedAt: new Date().toISOString(),
  1419. updatedBy: user.displayName
  1420. };
  1421. await persistState();
  1422. sendJson(res, 200, { rules: rulesState, message: "预警规则已由后台保存。" });
  1423. return;
  1424. }
  1425. sendError(res, 404, "NOT_FOUND", "未找到对应的后台接口。");
  1426. }
  1427. async function serveStatic(req, res, pathname) {
  1428. if (pathname === "/") {
  1429. res.writeHead(302, { Location: "/prototype/index.html" });
  1430. res.end();
  1431. return;
  1432. }
  1433. let decodedPath;
  1434. try {
  1435. decodedPath = decodeURIComponent(pathname);
  1436. } catch {
  1437. res.writeHead(400);
  1438. res.end("Bad request");
  1439. return;
  1440. }
  1441. const filePath = path.resolve(rootDir, `.${decodedPath}`);
  1442. if (!filePath.startsWith(rootDir)) {
  1443. res.writeHead(403);
  1444. res.end("Forbidden");
  1445. return;
  1446. }
  1447. try {
  1448. const stat = await fs.stat(filePath);
  1449. const finalPath = stat.isDirectory() ? path.join(filePath, "index.html") : filePath;
  1450. const data = await fs.readFile(finalPath);
  1451. const type = mimeTypes[path.extname(finalPath).toLowerCase()] || "application/octet-stream";
  1452. res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" });
  1453. res.end(data);
  1454. } catch {
  1455. res.writeHead(404);
  1456. res.end("Not found");
  1457. }
  1458. }
  1459. const server = http.createServer(async (req, res) => {
  1460. const url = new URL(req.url, `http://${req.headers.host || "127.0.0.1"}`);
  1461. try {
  1462. if (url.pathname.startsWith("/api/")) {
  1463. await handleApi(req, res, url.pathname);
  1464. return;
  1465. }
  1466. await serveStatic(req, res, url.pathname);
  1467. } catch (error) {
  1468. const status = error.status || 500;
  1469. sendError(res, status, "SERVER_ERROR", error.message || "服务器处理失败。");
  1470. }
  1471. });
  1472. async function start() {
  1473. await loadRoster();
  1474. await loadState();
  1475. server.listen(port, host, () => {
  1476. const shownHost = host === "0.0.0.0" ? "127.0.0.1" : host;
  1477. console.log(`A+在建客户动态管理系统 running at http://${shownHost}:${port}/prototype/index.html`);
  1478. });
  1479. }
  1480. start().catch((error) => {
  1481. console.error("Failed to start server", error);
  1482. process.exit(1);
  1483. });