diff --git a/backend/backups/backup_2026-01-21T01-50-59-025Z.json b/backend/backups/backup_2026-01-21T01-50-59-025Z.json deleted file mode 100644 index c729abe..0000000 --- a/backend/backups/backup_2026-01-21T01-50-59-025Z.json +++ /dev/null @@ -1,1280 +0,0 @@ -{ - "timestamp": "2026-01-21T01:50:59.026Z", - "version": "1.0.0", - "data": { - "devices": [ - { - "deviceId": "DEV001", - "name": "Cisco 路由器002", - "type": "router", - "model": "Cisco ISR 4451", - "serialNumber": "SN9358685702", - "rackId": "A05", - "position": 6, - "height": 2, - "powerConsumption": 266, - "status": "offline", - "purchaseDate": "2023-07-12 16:00:00.000 +00:00", - "warrantyExpiry": "2027-07-11 16:00:00.000 +00:00", - "ipAddress": "192.168.12.51", - "description": "接入路由器,用于路由器002业务", - "customFields": "{\"position\":6,\"height\":2,\"powerConsumption\":266,\"rackId\":\"A05\"}", - "createdAt": "2025-12-30 06:00:33.729 +00:00", - "updatedAt": "2025-12-30 06:00:33.729 +00:00" - }, - { - "deviceId": "KuaiShou_Server_1", - "name": "KuaiShou_Server_1", - "type": "server", - "model": "SR570", - "serialNumber": "819185652", - "rackId": "C06", - "position": 1, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":1,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.243 +00:00", - "updatedAt": "2025-12-30 06:00:56.243 +00:00" - }, - { - "deviceId": "KuaiShou_Server_2", - "name": "KuaiShou_Server_2", - "type": "server", - "model": "SR570", - "serialNumber": "819185655", - "rackId": "C06", - "position": 3, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":3,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.343 +00:00", - "updatedAt": "2025-12-30 06:00:56.343 +00:00" - }, - { - "deviceId": "KuaiShou_Server_3", - "name": "KuaiShou_Server_3", - "type": "server", - "model": "SR570", - "serialNumber": "819185650", - "rackId": "C06", - "position": 5, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":5,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.413 +00:00", - "updatedAt": "2025-12-30 06:00:56.413 +00:00" - }, - { - "deviceId": "KuaiShou_Server_4", - "name": "KuaiShou_Server_4", - "type": "server", - "model": "SR570", - "serialNumber": "819185653", - "rackId": "C06", - "position": 7, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":7,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.482 +00:00", - "updatedAt": "2025-12-30 06:00:56.482 +00:00" - }, - { - "deviceId": "KuaiShou_Server_5", - "name": "KuaiShou_Server_5", - "type": "server", - "model": "SR570", - "serialNumber": "J3012OPF", - "rackId": "C06", - "position": 9, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":9,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.552 +00:00", - "updatedAt": "2025-12-30 06:00:56.552 +00:00" - }, - { - "deviceId": "KuaiShou_Server_6", - "name": "KuaiShou_Server_6", - "type": "server", - "model": "SR570", - "serialNumber": "J30120PE", - "rackId": "C06", - "position": 11, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":11,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.623 +00:00", - "updatedAt": "2025-12-30 06:00:56.623 +00:00" - }, - { - "deviceId": "KuaiShou_Server_7", - "name": "KuaiShou_Server_7", - "type": "server", - "model": "SR570", - "serialNumber": "J3015R86", - "rackId": "C06", - "position": 13, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":13,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.695 +00:00", - "updatedAt": "2025-12-30 06:00:56.695 +00:00" - }, - { - "deviceId": "KuaiShou_Server_8", - "name": "KuaiShou_Server_8", - "type": "server", - "model": "SR570", - "serialNumber": "J3012OPG", - "rackId": "C06", - "position": 15, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":15,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.774 +00:00", - "updatedAt": "2025-12-30 06:00:56.774 +00:00" - }, - { - "deviceId": "KuaiShou_Server_9", - "name": "KuaiShou_Server_9", - "type": "server", - "model": "SR570", - "serialNumber": "J3012ONZ", - "rackId": "C06", - "position": 17, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":17,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.843 +00:00", - "updatedAt": "2025-12-30 06:00:56.843 +00:00" - }, - { - "deviceId": "KuaiShou_Server_10", - "name": "KuaiShou_Server_10", - "type": "server", - "model": "SR570", - "serialNumber": "J300Z86R", - "rackId": "C06", - "position": 19, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":19,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:56.912 +00:00", - "updatedAt": "2025-12-30 06:00:56.912 +00:00" - }, - { - "deviceId": "测试", - "name": "测试", - "type": "server", - "model": "RH2288HV3", - "serialNumber": "2102310YJV10H1001224", - "rackId": "C07", - "position": 6, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":6,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:56.982 +00:00", - "updatedAt": "2025-12-30 06:00:56.982 +00:00" - }, - { - "deviceId": "透明缓存_万兆switch", - "name": "透明缓存_万兆switch", - "type": "switch", - "model": "DELL N4032F", - "serialNumber": "3X9L0Z1", - "rackId": "C06", - "position": 37, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":37,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.053 +00:00", - "updatedAt": "2025-12-30 06:00:57.053 +00:00" - }, - { - "deviceId": "透明缓存_千兆管理switch", - "name": "透明缓存_千兆管理switch", - "type": "switch", - "model": "catalyst C3560G", - "serialNumber": "F0C1652V0EM", - "rackId": "C06", - "position": 35, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":35,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.122 +00:00", - "updatedAt": "2025-12-30 06:00:57.122 +00:00" - }, - { - "deviceId": "透明缓存_网络流量处理系统", - "name": "透明缓存_网络流量处理系统", - "type": "switch", - "model": "A10", - "serialNumber": "NUlll", - "rackId": "C06", - "position": 33, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":33,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.191 +00:00", - "updatedAt": "2025-12-30 06:00:57.191 +00:00" - }, - { - "deviceId": "透明缓存_安全堡垒机", - "name": "透明缓存_安全堡垒机", - "type": "server", - "model": "DELL R730XD", - "serialNumber": "7YFJXP2", - "rackId": "C06", - "position": 30, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":30,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.275 +00:00", - "updatedAt": "2025-12-30 06:00:57.275 +00:00" - }, - { - "deviceId": "透明缓存_AU3000", - "name": "透明缓存_AU3000", - "type": "server", - "model": "DELL R730XD", - "serialNumber": "7Y4LXP2", - "rackId": "C06", - "position": 27, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":27,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.345 +00:00", - "updatedAt": "2025-12-30 06:00:57.345 +00:00" - }, - { - "deviceId": "透明缓存_GC2508_1", - "name": "透明缓存_GC2508_1", - "type": "server", - "model": "DELL R730XD", - "serialNumber": "7ZCQXP2", - "rackId": "C06", - "position": 24, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":24,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.413 +00:00", - "updatedAt": "2025-12-30 06:00:57.413 +00:00" - }, - { - "deviceId": "透明缓存_GC2508_2", - "name": "透明缓存_GC2508_2", - "type": "server", - "model": "DELL R730XD", - "serialNumber": "7ZCJXP2", - "rackId": "C06", - "position": 21, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":21,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:57.593 +00:00", - "updatedAt": "2025-12-30 06:00:57.593 +00:00" - }, - { - "deviceId": "lvs1.cn3096", - "name": "lvs1.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "CK15B81R03E", - "rackId": "D11", - "position": 24, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":24,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:57.678 +00:00", - "updatedAt": "2025-12-30 06:00:57.678 +00:00" - }, - { - "deviceId": "HUB-1", - "name": "HUB-1", - "type": "switch", - "model": "锐捷", - "serialNumber": "G1LQ6AN015306", - "rackId": "D11", - "position": 41, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":41,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:57.781 +00:00", - "updatedAt": "2025-12-30 06:00:57.781 +00:00" - }, - { - "deviceId": "CNW-EC-SELBPART-G1-1.CN3096", - "name": "CNW-EC-SELBPART-G1-1.CN3096", - "type": "switch", - "model": "锐捷", - "serialNumber": "G1PH70Q10380A", - "rackId": "D11", - "position": 43, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":43,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:57.880 +00:00", - "updatedAt": "2025-12-30 06:00:57.880 +00:00" - }, - { - "deviceId": "CNW-EC-SELBPART-G1-2.CN3096", - "name": "CNW-EC-SELBPART-G1-2.CN3096", - "type": "switch", - "model": "锐捷", - "serialNumber": "G1PH70Q106626", - "rackId": "D12", - "position": 43, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":43,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:57.963 +00:00", - "updatedAt": "2025-12-30 06:00:57.963 +00:00" - }, - { - "deviceId": "lvs2.cn3096", - "name": "lvs2.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "CK15B81R03C", - "rackId": "D12", - "position": 21, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":21,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:58.035 +00:00", - "updatedAt": "2025-12-30 06:00:58.035 +00:00" - }, - { - "deviceId": "LS-6850-56HF-H1", - "name": "LS-6850-56HF-H1", - "type": "switch", - "model": "S6850", - "serialNumber": "210235A2PQP213000096", - "rackId": "C06", - "position": 41, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":41,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:58.113 +00:00", - "updatedAt": "2025-12-30 06:00:58.113 +00:00" - }, - { - "deviceId": "LS-5130S-52S-EI", - "name": "LS-5130S-52S-EI", - "type": "switch", - "model": "S5130", - "serialNumber": "219801A12F9211Q00168", - "rackId": "C06", - "position": 39, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":39,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C06\"}", - "createdAt": "2025-12-30 06:00:58.185 +00:00", - "updatedAt": "2025-12-30 06:00:58.185 +00:00" - }, - { - "deviceId": "KuaiShou_Server_13", - "name": "KuaiShou_Server_10", - "type": "server", - "model": "Dell R540", - "serialNumber": "GLR4D43", - "rackId": "D11", - "position": 30, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1900-01-01 00:00:00.000 +00:00", - "warrantyExpiry": "1920-01-02 00:00:00.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":30,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:58.266 +00:00", - "updatedAt": "2025-12-30 06:00:58.266 +00:00" - }, - { - "deviceId": "KuaiShou_Server_11", - "name": "KuaiShou_Server_11", - "type": "server", - "model": "Dell R540", - "serialNumber": "GLQXC43", - "rackId": "D11", - "position": 33, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":33,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:58.334 +00:00", - "updatedAt": "2025-12-30 06:00:58.334 +00:00" - }, - { - "deviceId": "KuaiShou_Server_12", - "name": "KuaiShou_Server_12", - "type": "server", - "model": "Dell R540", - "serialNumber": "GLR6D43", - "rackId": "D11", - "position": 36, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":36,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:58.405 +00:00", - "updatedAt": "2025-12-30 06:00:58.405 +00:00" - }, - { - "deviceId": "测试1", - "name": "测试1", - "type": "server", - "model": "DELL R730XD", - "serialNumber": "713L52X", - "rackId": "C07", - "position": 12, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1900-01-01 00:00:00.000 +00:00", - "warrantyExpiry": "1920-01-02 00:00:00.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":12,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:58.475 +00:00", - "updatedAt": "2025-12-30 06:00:58.475 +00:00" - }, - { - "deviceId": "测试2", - "name": "测试1", - "type": "server", - "model": "华为2288HV3", - "serialNumber": "2102311QGK10H1000053", - "rackId": "C07", - "position": 15, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1900-01-01 00:00:00.000 +00:00", - "warrantyExpiry": "1920-01-02 00:00:00.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":15,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:58.545 +00:00", - "updatedAt": "2025-12-30 06:00:58.545 +00:00" - }, - { - "deviceId": "测试3", - "name": "测试3", - "type": "server", - "model": "华为2288HV3", - "serialNumber": "210235G78M10E1000139", - "rackId": "C08", - "position": 3, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1900-01-01 00:00:00.000 +00:00", - "warrantyExpiry": "1920-01-02 00:00:00.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":3,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C08\"}", - "createdAt": "2025-12-30 06:00:58.614 +00:00", - "updatedAt": "2025-12-30 06:00:58.614 +00:00" - }, - { - "deviceId": "快手扩容-1", - "name": "快手扩容-1", - "type": "server", - "model": "Lenovo SR570", - "serialNumber": "J300LENE", - "rackId": "C07", - "position": 27, - "height": 1, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":27,\"height\":1,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:58.685 +00:00", - "updatedAt": "2025-12-30 06:00:58.685 +00:00" - }, - { - "deviceId": "快手扩容-2", - "name": "快手扩容-2", - "type": "server", - "model": "Inspur SA5212M5", - "serialNumber": "819434370", - "rackId": "C07", - "position": 24, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":24,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:58.755 +00:00", - "updatedAt": "2025-12-30 06:00:58.755 +00:00" - }, - { - "deviceId": "快手扩容-3", - "name": "快手扩容-3", - "type": "server", - "model": "Inspur SA5212M4", - "serialNumber": "819280575", - "rackId": "C07", - "position": 21, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":21,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:58.825 +00:00", - "updatedAt": "2025-12-30 06:00:58.825 +00:00" - }, - { - "deviceId": "快手扩容-4", - "name": "快手扩容-4", - "type": "server", - "model": "Inspur SA5212M5", - "serialNumber": "819452802", - "rackId": "C07", - "position": 18, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":18,\"height\":2,\"powerConsumption\":500,\"rackId\":\"C07\"}", - "createdAt": "2025-12-30 06:00:58.950 +00:00", - "updatedAt": "2025-12-30 06:00:58.950 +00:00" - }, - { - "deviceId": "merge1.cn3096", - "name": "merge1.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "818307883", - "rackId": "D11", - "position": 3, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":3,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.023 +00:00", - "updatedAt": "2025-12-30 06:00:59.023 +00:00" - }, - { - "deviceId": "cache6.cn3096", - "name": "cache6.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "818306720", - "rackId": "D11", - "position": 6, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":6,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.102 +00:00", - "updatedAt": "2025-12-30 06:00:59.102 +00:00" - }, - { - "deviceId": "cache5.cn3096", - "name": "cache5.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "819585227", - "rackId": "D11", - "position": 9, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":9,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.172 +00:00", - "updatedAt": "2025-12-30 06:00:59.172 +00:00" - }, - { - "deviceId": "cache4.cn3096", - "name": "cache4.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "219830538", - "rackId": "D11", - "position": 12, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":12,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.243 +00:00", - "updatedAt": "2025-12-30 06:00:59.243 +00:00" - }, - { - "deviceId": "cache3.cn3096", - "name": "cache3.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "218269526", - "rackId": "D11", - "position": 15, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":15,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.435 +00:00", - "updatedAt": "2025-12-30 06:00:59.435 +00:00" - }, - { - "deviceId": "cache2.cn3096", - "name": "cache2.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "818294133", - "rackId": "D11", - "position": 18, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":18,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.505 +00:00", - "updatedAt": "2025-12-30 06:00:59.505 +00:00" - }, - { - "deviceId": "cache1.cn3096", - "name": "cache1.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "818458511", - "rackId": "D11", - "position": 21, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":21,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D11\"}", - "createdAt": "2025-12-30 06:00:59.575 +00:00", - "updatedAt": "2025-12-30 06:00:59.575 +00:00" - }, - { - "deviceId": "cache7.cn3196", - "name": "cache7.cn3196", - "type": "server", - "model": "阿里定制", - "serialNumber": "819268886", - "rackId": "D12", - "position": 18, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":18,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:59.645 +00:00", - "updatedAt": "2025-12-30 06:00:59.645 +00:00" - }, - { - "deviceId": "cache8.cn3196", - "name": "cache8.cn3196", - "type": "server", - "model": "阿里定制", - "serialNumber": "818281019", - "rackId": "D12", - "position": 15, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":15,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:59.714 +00:00", - "updatedAt": "2025-12-30 06:00:59.714 +00:00" - }, - { - "deviceId": "cache9.cn3196", - "name": "cache9.cn3196", - "type": "server", - "model": "阿里定制", - "serialNumber": "218280243", - "rackId": "D12", - "position": 12, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":12,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:59.784 +00:00", - "updatedAt": "2025-12-30 06:00:59.784 +00:00" - }, - { - "deviceId": "cache10.cn3096", - "name": "cache10.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "218269518", - "rackId": "D12", - "position": 9, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":9,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:59.855 +00:00", - "updatedAt": "2025-12-30 06:00:59.855 +00:00" - }, - { - "deviceId": "cache11.cn3096", - "name": "cache11.cn3096", - "type": "server", - "model": "阿里定制", - "serialNumber": "819585174", - "rackId": "D12", - "position": 6, - "height": 2, - "powerConsumption": 500, - "status": "running", - "purchaseDate": "1899-12-31 15:54:17.000 +00:00", - "warrantyExpiry": "1900-01-01 15:54:17.000 +00:00", - "ipAddress": "", - "description": "", - "customFields": "{\"position\":6,\"height\":2,\"powerConsumption\":500,\"rackId\":\"D12\"}", - "createdAt": "2025-12-30 06:00:59.925 +00:00", - "updatedAt": "2025-12-30 06:00:59.925 +00:00" - } - ], - "racks": [ - { - "rackId": "A04", - "name": "A04", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:50.616 +00:00", - "updatedAt": "2025-12-30 06:00:29.539 +00:00" - }, - { - "rackId": "A05", - "name": "A05", - "height": 42, - "maxPower": 10000, - "currentPower": 266, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:51.135 +00:00", - "updatedAt": "2025-12-30 06:00:33.800 +00:00" - }, - { - "rackId": "A06", - "name": "A06", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:51.240 +00:00", - "updatedAt": "2025-12-30 03:12:08.209 +00:00" - }, - { - "rackId": "A01", - "name": "A01", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:51.344 +00:00", - "updatedAt": "2025-12-30 03:16:57.752 +00:00" - }, - { - "rackId": "A02", - "name": "A02", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:51.519 +00:00", - "updatedAt": "2025-12-30 03:12:09.515 +00:00" - }, - { - "rackId": "A07", - "name": "A07", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:51.622 +00:00", - "updatedAt": "2025-12-30 03:12:09.923 +00:00" - }, - { - "rackId": "A08", - "name": "A08", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:51.867 +00:00", - "updatedAt": "2025-12-30 03:12:09.250 +00:00" - }, - { - "rackId": "A09", - "name": "A09", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:52.416 +00:00", - "updatedAt": "2025-12-30 03:12:09.753 +00:00" - }, - { - "rackId": "A10", - "name": "A10", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:52.701 +00:00", - "updatedAt": "2025-12-30 03:12:09.621 +00:00" - }, - { - "rackId": "A03", - "name": "A03", - "height": 42, - "maxPower": 10000, - "currentPower": 0, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-25 05:08:52.806 +00:00", - "updatedAt": "2025-12-30 03:12:09.483 +00:00" - }, - { - "rackId": "C06", - "name": "C06", - "height": 42, - "maxPower": 10000, - "currentPower": 9500, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-30 03:17:44.632 +00:00", - "updatedAt": "2025-12-30 06:00:58.228 +00:00" - }, - { - "rackId": "C07", - "name": "C07", - "height": 42, - "maxPower": 10000, - "currentPower": 3500, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-30 03:17:44.689 +00:00", - "updatedAt": "2025-12-30 06:00:58.984 +00:00" - }, - { - "rackId": "D11", - "name": "D11", - "height": 42, - "maxPower": 10000, - "currentPower": 6500, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-30 03:17:44.752 +00:00", - "updatedAt": "2025-12-30 06:00:59.610 +00:00" - }, - { - "rackId": "D12", - "name": "D12", - "height": 42, - "maxPower": 10000, - "currentPower": 3500, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-30 03:17:44.796 +00:00", - "updatedAt": "2025-12-30 06:00:59.957 +00:00" - }, - { - "rackId": "C08", - "name": "C08", - "height": 42, - "maxPower": 10000, - "currentPower": 500, - "status": "active", - "roomId": "ROOM001", - "createdAt": "2025-12-30 03:17:44.869 +00:00", - "updatedAt": "2025-12-30 06:00:58.650 +00:00" - } - ], - "rooms": [ - { - "roomId": "ROOM001", - "name": "测试机房", - "location": "测试位置", - "area": 100, - "capacity": 100, - "status": "active", - "description": null, - "createdAt": "2025-12-25 05:06:53.179 +00:00", - "updatedAt": "2025-12-25 05:06:53.179 +00:00" - } - ], - "consumables": [], - "deviceFields": [ - { - "fieldId": 1, - "fieldName": "deviceId", - "displayName": "设备ID", - "fieldType": "string", - "required": 1, - "options": null, - "order": 1, - "visible": 1, - "createdAt": "2025-12-25 05:10:03.898 +00:00", - "updatedAt": "2025-12-29 08:28:41.999 +00:00" - }, - { - "fieldId": 2, - "fieldName": "name", - "displayName": "设备名称", - "fieldType": "string", - "required": 1, - "options": null, - "order": 2, - "visible": 1, - "createdAt": "2025-12-25 05:10:04.777 +00:00", - "updatedAt": "2025-12-29 08:28:42.068 +00:00" - }, - { - "fieldId": 3, - "fieldName": "type", - "displayName": "设备类型", - "fieldType": "select", - "required": 1, - "options": "[{\"value\":\"server\",\"label\":\"服务器\"},{\"value\":\"switch\",\"label\":\"交换机\"},{\"value\":\"router\",\"label\":\"路由器\"},{\"value\":\"storage\",\"label\":\"存储设备\"},{\"value\":\"other\",\"label\":\"其他设备\"}]", - "order": 3, - "visible": 1, - "createdAt": "2025-12-25 05:10:05.492 +00:00", - "updatedAt": "2025-12-29 08:28:42.103 +00:00" - }, - { - "fieldId": 4, - "fieldName": "model", - "displayName": "型号", - "fieldType": "string", - "required": 1, - "options": null, - "order": 4, - "visible": 1, - "createdAt": "2025-12-25 05:10:06.166 +00:00", - "updatedAt": "2025-12-29 08:28:42.139 +00:00" - }, - { - "fieldId": 5, - "fieldName": "serialNumber", - "displayName": "序列号", - "fieldType": "string", - "required": 1, - "options": null, - "order": 5, - "visible": 1, - "createdAt": "2025-12-25 05:10:06.886 +00:00", - "updatedAt": "2025-12-29 08:28:42.173 +00:00" - }, - { - "fieldId": 6, - "fieldName": "rackId", - "displayName": "所在机柜", - "fieldType": "select", - "required": 1, - "options": null, - "order": 6, - "visible": 1, - "createdAt": "2025-12-25 05:10:07.572 +00:00", - "updatedAt": "2025-12-29 08:40:39.158 +00:00" - }, - { - "fieldId": 7, - "fieldName": "position", - "displayName": "位置(U)", - "fieldType": "number", - "required": 1, - "options": null, - "order": 7, - "visible": 1, - "createdAt": "2025-12-25 05:10:08.259 +00:00", - "updatedAt": "2025-12-29 08:28:42.242 +00:00" - }, - { - "fieldId": 8, - "fieldName": "height", - "displayName": "高度(U)", - "fieldType": "number", - "required": 1, - "options": null, - "order": 8, - "visible": 1, - "createdAt": "2025-12-25 05:10:08.973 +00:00", - "updatedAt": "2025-12-29 08:28:42.279 +00:00" - }, - { - "fieldId": 9, - "fieldName": "powerConsumption", - "displayName": "功率(W)", - "fieldType": "number", - "required": 1, - "options": null, - "order": 9, - "visible": 1, - "createdAt": "2025-12-25 05:10:09.051 +00:00", - "updatedAt": "2025-12-29 08:40:39.242 +00:00" - }, - { - "fieldId": 10, - "fieldName": "status", - "displayName": "状态", - "fieldType": "select", - "required": 1, - "options": "[{\"value\":\"running\",\"label\":\"运行中\"},{\"value\":\"maintenance\",\"label\":\"维护中\"},{\"value\":\"offline\",\"label\":\"离线\"},{\"value\":\"fault\",\"label\":\"故障\"}]", - "order": 10, - "visible": 1, - "createdAt": "2025-12-25 05:10:09.087 +00:00", - "updatedAt": "2025-12-29 08:28:42.347 +00:00" - }, - { - "fieldId": 11, - "fieldName": "purchaseDate", - "displayName": "购买日期", - "fieldType": "date", - "required": 1, - "options": null, - "order": 11, - "visible": 1, - "createdAt": "2025-12-25 05:10:09.121 +00:00", - "updatedAt": "2025-12-30 05:59:06.031 +00:00" - }, - { - "fieldId": 12, - "fieldName": "warrantyExpiry", - "displayName": "保修到期", - "fieldType": "date", - "required": 1, - "options": null, - "order": 12, - "visible": 1, - "createdAt": "2025-12-25 05:10:09.157 +00:00", - "updatedAt": "2025-12-30 05:59:06.104 +00:00" - }, - { - "fieldId": 13, - "fieldName": "ipAddress", - "displayName": "IP地址", - "fieldType": "string", - "required": 0, - "options": null, - "order": 13, - "visible": 1, - "createdAt": "2025-12-25 05:10:09.192 +00:00", - "updatedAt": "2025-12-29 08:40:39.417 +00:00" - }, - { - "fieldId": 14, - "fieldName": "description", - "displayName": "描述", - "fieldType": "textarea", - "required": 0, - "options": null, - "order": 14, - "visible": 1, - "createdAt": "2025-12-25 05:10:09.226 +00:00", - "updatedAt": "2025-12-29 08:40:39.523 +00:00" - } - ], - "users": [ - { - "userId": "user_mjkrahqi31ud7tdo5", - "username": "admin", - "status": "active", - "createdAt": "2025-12-25 01:20:37.194 +00:00", - "updatedAt": "2026-01-20 06:50:21.848 +00:00" - } - ] - } -} \ No newline at end of file diff --git a/backend/middleware/validation.js b/backend/middleware/validation.js new file mode 100644 index 0000000..6f5c28b --- /dev/null +++ b/backend/middleware/validation.js @@ -0,0 +1,49 @@ +/** + * 请求参数验证中间件 + * 使用Joi进行参数校验 + */ + +const validate = (schema, source = 'body') => { + return (req, res, next) => { + const data = source === 'query' ? req.query : req.body; + + const { error, value } = schema.validate(data, { + abortEarly: false, // 返回所有错误 + stripUnknown: true, // 移除未定义的字段 + allowUnknown: source === 'query' // 查询参数允许未知字段 + }); + + if (error) { + const errorMessages = error.details.map(detail => ({ + field: detail.path.join('.'), + message: detail.message + })); + + return res.status(400).json({ + error: '参数验证失败', + details: errorMessages + }); + } + + // 将验证后的值替换到请求对象 + if (source === 'query') { + req.query = value; + } else { + req.body = value; + } + + next(); + }; +}; + +// 验证查询参数 +const validateQuery = (schema) => validate(schema, 'query'); + +// 验证请求体 +const validateBody = (schema) => validate(schema, 'body'); + +module.exports = { + validate, + validateQuery, + validateBody +}; diff --git a/backend/models/Consumable.js b/backend/models/Consumable.js index cd9f727..2cdaa1b 100644 --- a/backend/models/Consumable.js +++ b/backend/models/Consumable.js @@ -54,6 +54,12 @@ const Consumable = sequelize.define('Consumable', { status: { type: DataTypes.STRING, defaultValue: 'active' + }, + version: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '乐观锁版本号' } }, { tableName: 'consumables', @@ -61,7 +67,8 @@ const Consumable = sequelize.define('Consumable', { indexes: [ { fields: ['category'] }, { fields: ['status'] }, - { fields: ['category', 'status'] } + { fields: ['category', 'status'] }, + { fields: ['updatedAt'] } ] }); diff --git a/backend/package-lock.json b/backend/package-lock.json index 36b86fb..923dd88 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,6 +8,7 @@ "name": "idc-backend", "version": "1.0.0", "dependencies": { + "axios": "^1.13.4", "bcryptjs": "^3.0.3", "cors": "^2.8.5", "csv-parser": "^3.2.0", @@ -17,6 +18,7 @@ "express": "^4.18.2", "express-fileupload": "^1.5.2", "iconv-lite": "^0.6.3", + "joi": "^18.0.2", "jsonwebtoken": "^9.0.3", "mysql2": "^3.16.0", "sequelize": "^6.32.1", @@ -677,6 +679,54 @@ "license": "MIT", "optional": true }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1379,6 +1429,12 @@ "text-hex": "1.0.x" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2013,7 +2069,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/aws-ssl-profiles": { @@ -2025,6 +2080,17 @@ "node": ">= 6.0.0" } }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -2726,7 +2792,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -2935,7 +3000,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3182,7 +3246,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3458,6 +3521,26 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3492,7 +3575,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3795,7 +3877,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5067,6 +5148,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6281,6 +6380,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/backend/package.json b/backend/package.json index 91f385f..5e57085 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "drop-indexes": "node create_indexes.js drop" }, "dependencies": { + "axios": "^1.13.4", "bcryptjs": "^3.0.3", "cors": "^2.8.5", "csv-parser": "^3.2.0", @@ -19,6 +20,7 @@ "express": "^4.18.2", "express-fileupload": "^1.5.2", "iconv-lite": "^0.6.3", + "joi": "^18.0.2", "jsonwebtoken": "^9.0.3", "mysql2": "^3.16.0", "sequelize": "^6.32.1", diff --git a/backend/routes/consumables.js b/backend/routes/consumables.js index 6ec5376..a558d4d 100644 --- a/backend/routes/consumables.js +++ b/backend/routes/consumables.js @@ -250,187 +250,281 @@ router.get('/inout/records', async (req, res) => { }); router.post('/quick-inout', async (req, res) => { - const transaction = await sequelize.transaction(); - try { - const { consumableId, type, quantity, operator, reason, notes } = req.body; - - const consumable = await Consumable.findByPk(consumableId, { transaction }); - if (!consumable) { - await transaction.rollback(); - return res.status(404).json({ error: '耗材不存在' }); - } - - const previousStock = parseFloat(consumable.currentStock); - let newStock; - - if (type === 'in') { - newStock = previousStock + parseFloat(quantity); - } else if (type === 'out') { - newStock = previousStock - parseFloat(quantity); - if (newStock < 0) { + const MAX_RETRIES = 3; + let attempt = 0; + + while (attempt < MAX_RETRIES) { + const transaction = await sequelize.transaction(); + try { + const { consumableId, type, quantity, operator, reason, notes } = req.body; + + const consumable = await Consumable.findByPk(consumableId, { transaction }); + if (!consumable) { await transaction.rollback(); - return res.status(400).json({ error: '库存不足' }); + return res.status(404).json({ error: '耗材不存在' }); } - } else { + + const previousStock = parseFloat(consumable.currentStock); + let newStock; + + if (type === 'in') { + newStock = previousStock + parseFloat(quantity); + } else if (type === 'out') { + newStock = previousStock - parseFloat(quantity); + if (newStock < 0) { + await transaction.rollback(); + return res.status(400).json({ error: '库存不足' }); + } + } else { + await transaction.rollback(); + return res.status(400).json({ error: '操作类型无效' }); + } + + const [affectedRows] = await Consumable.update( + { + currentStock: newStock, + version: sequelize.literal('version + 1') + }, + { + where: { + consumableId, + version: consumable.version + }, + transaction + } + ); + + if (affectedRows === 0) { + await transaction.rollback(); + attempt++; + if (attempt >= MAX_RETRIES) { + return res.status(409).json({ error: '并发冲突,请稍后重试' }); + } + continue; + } + + const record = await ConsumableRecord.create({ + consumableId, + type, + quantity, + previousStock, + currentStock: newStock, + operator, + reason, + notes + }, { transaction }); + + await ConsumableLog.create({ + consumableId, + consumableName: consumable.name, + operationType: type, + quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity), + previousStock, + currentStock: newStock, + operator, + reason, + notes + }, { transaction }); + + await transaction.commit(); + + res.json({ + message: '操作成功', + record, + consumable: await Consumable.findByPk(consumableId) + }); + return; + } catch (error) { await transaction.rollback(); - return res.status(400).json({ error: '操作类型无效' }); + if (attempt >= MAX_RETRIES - 1) { + return res.status(500).json({ error: error.message }); + } + attempt++; } - - await consumable.update({ currentStock: newStock }, { transaction }); - - const record = await ConsumableRecord.create({ - consumableId, - type, - quantity, - previousStock, - currentStock: newStock, - operator, - reason, - notes - }, { transaction }); - - await ConsumableLog.create({ - consumableId, - consumableName: consumable.name, - operationType: type, - quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity), - previousStock, - currentStock: newStock, - operator, - reason, - notes - }, { transaction }); - - await transaction.commit(); - - res.json({ - message: '操作成功', - record, - consumable: await consumable.reload() - }); - } catch (error) { - await transaction.rollback(); - res.status(500).json({ error: error.message }); } }); router.post('/inout', async (req, res) => { - const transaction = await sequelize.transaction(); - try { - const { consumableId, type, quantity, operator, reason, recipient, notes } = req.body; - - const consumable = await Consumable.findByPk(consumableId, { transaction }); - if (!consumable) { - await transaction.rollback(); - return res.status(404).json({ error: '耗材不存在' }); - } - - const previousStock = parseFloat(consumable.currentStock); - let newStock; - - if (type === 'in') { - newStock = previousStock + parseFloat(quantity); - } else { - newStock = previousStock - parseFloat(quantity); - if (newStock < 0) { + const MAX_RETRIES = 3; + let attempt = 0; + + while (attempt < MAX_RETRIES) { + const transaction = await sequelize.transaction(); + try { + const { consumableId, type, quantity, operator, reason, recipient, notes } = req.body; + + const consumable = await Consumable.findByPk(consumableId, { transaction }); + if (!consumable) { await transaction.rollback(); - return res.status(400).json({ error: '库存不足' }); + return res.status(404).json({ error: '耗材不存在' }); } + + const previousStock = parseFloat(consumable.currentStock); + let newStock; + + if (type === 'in') { + newStock = previousStock + parseFloat(quantity); + } else { + newStock = previousStock - parseFloat(quantity); + if (newStock < 0) { + await transaction.rollback(); + return res.status(400).json({ error: '库存不足' }); + } + } + + const [affectedRows] = await Consumable.update( + { + currentStock: newStock, + version: sequelize.literal('version + 1') + }, + { + where: { + consumableId, + version: consumable.version + }, + transaction + } + ); + + if (affectedRows === 0) { + await transaction.rollback(); + attempt++; + if (attempt >= MAX_RETRIES) { + return res.status(409).json({ error: '并发冲突,请稍后重试' }); + } + continue; + } + + const record = await ConsumableRecord.create({ + consumableId, + type, + quantity, + previousStock, + currentStock: newStock, + operator, + reason, + recipient, + notes + }, { transaction }); + + await ConsumableLog.create({ + consumableId, + consumableName: consumable.name, + operationType: type, + quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity), + previousStock, + currentStock: newStock, + operator, + reason, + notes + }, { transaction }); + + await transaction.commit(); + + res.json({ + message: '操作成功', + record, + consumable: await Consumable.findByPk(consumableId) + }); + return; + } catch (error) { + await transaction.rollback(); + if (attempt >= MAX_RETRIES - 1) { + return res.status(500).json({ error: error.message }); + } + attempt++; } - - await consumable.update({ currentStock: newStock }, { transaction }); - - const record = await ConsumableRecord.create({ - consumableId, - type, - quantity, - previousStock, - currentStock: newStock, - operator, - reason, - recipient, - notes - }, { transaction }); - - await ConsumableLog.create({ - consumableId, - consumableName: consumable.name, - operationType: type, - quantity: type === 'in' ? parseFloat(quantity) : -parseFloat(quantity), - previousStock, - currentStock: newStock, - operator, - reason, - notes - }, { transaction }); - - await transaction.commit(); - - res.json({ - message: '操作成功', - record, - consumable: await consumable.reload() - }); - } catch (error) { - await transaction.rollback(); - res.status(500).json({ error: error.message }); } }); router.post('/adjust', async (req, res) => { - const transaction = await sequelize.transaction(); - try { - const { consumableId, adjustType, quantity, operator, reason, notes } = req.body; - - const consumable = await Consumable.findByPk(consumableId, { transaction }); - if (!consumable) { - await transaction.rollback(); - return res.status(404).json({ error: '耗材不存在' }); - } - - const previousStock = parseFloat(consumable.currentStock); - let newStock; - - if (adjustType === 'add') { - newStock = previousStock + parseFloat(quantity); - } else if (adjustType === 'subtract') { - newStock = previousStock - parseFloat(quantity); - if (newStock < 0) { + const MAX_RETRIES = 3; + let attempt = 0; + + while (attempt < MAX_RETRIES) { + const transaction = await sequelize.transaction(); + try { + const { consumableId, adjustType, quantity, operator, reason, notes } = req.body; + + const consumable = await Consumable.findByPk(consumableId, { transaction }); + if (!consumable) { await transaction.rollback(); - return res.status(400).json({ error: '调整后库存不能为负' }); + return res.status(404).json({ error: '耗材不存在' }); } - } else if (adjustType === 'set') { - newStock = parseFloat(quantity); - } else { + + const previousStock = parseFloat(consumable.currentStock); + let newStock; + + if (adjustType === 'add') { + newStock = previousStock + parseFloat(quantity); + } else if (adjustType === 'subtract') { + newStock = previousStock - parseFloat(quantity); + if (newStock < 0) { + await transaction.rollback(); + return res.status(400).json({ error: '调整后库存不能为负' }); + } + } else if (adjustType === 'set') { + newStock = parseFloat(quantity); + if (newStock < 0) { + await transaction.rollback(); + return res.status(400).json({ error: '库存不能设置为负数' }); + } + } else { + await transaction.rollback(); + return res.status(400).json({ error: '调整类型无效' }); + } + + const [affectedRows] = await Consumable.update( + { + currentStock: newStock, + version: sequelize.literal('version + 1') + }, + { + where: { + consumableId, + version: consumable.version + }, + transaction + } + ); + + if (affectedRows === 0) { + await transaction.rollback(); + attempt++; + if (attempt >= MAX_RETRIES) { + return res.status(409).json({ error: '并发冲突,请稍后重试' }); + } + continue; + } + + const changeQuantity = newStock - previousStock; + + await ConsumableLog.create({ + consumableId, + consumableName: consumable.name, + operationType: 'adjust', + quantity: changeQuantity, + previousStock, + currentStock: newStock, + operator, + reason: reason || (adjustType === 'set' ? '库存调整为 ' + newStock : reason), + notes: adjustType === 'set' ? `库存从 ${previousStock} 调整为 ${newStock}` : notes + }, { transaction }); + + await transaction.commit(); + + res.json({ + message: '调整成功', + consumable: await Consumable.findByPk(consumableId) + }); + return; + } catch (error) { await transaction.rollback(); - return res.status(400).json({ error: '调整类型无效' }); + if (attempt >= MAX_RETRIES - 1) { + return res.status(500).json({ error: error.message }); + } + attempt++; } - - await consumable.update({ currentStock: newStock }, { transaction }); - - const changeQuantity = newStock - previousStock; - - await ConsumableLog.create({ - consumableId, - consumableName: consumable.name, - operationType: 'adjust', - quantity: changeQuantity, - previousStock, - currentStock: newStock, - operator, - reason: reason || (adjustType === 'set' ? '库存调整为 ' + newStock : reason), - notes: adjustType === 'set' ? `库存从 ${previousStock} 调整为 ${newStock}` : notes - }, { transaction }); - - await transaction.commit(); - - res.json({ - message: '调整成功', - consumable: await consumable.reload() - }); - } catch (error) { - await transaction.rollback(); - res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/devices.js b/backend/routes/devices.js index 2d5ef65..d383c40 100644 --- a/backend/routes/devices.js +++ b/backend/routes/devices.js @@ -14,9 +14,18 @@ const DeviceField = require('../models/DeviceField'); const Ticket = require('../models/Ticket'); const DevicePort = require('../models/DevicePort'); // Import DevicePort const Cable = require('../models/Cable'); // Import Cable +const { validateBody, validateQuery } = require('../middleware/validation'); +const { + createDeviceSchema, + updateDeviceSchema, + batchDeviceIdsSchema, + batchStatusSchema, + batchMoveSchema, + queryDeviceSchema +} = require('../validation/deviceSchema'); // 获取所有设备(支持搜索和筛选) -router.get('/', async (req, res) => { +router.get('/', validateQuery(queryDeviceSchema), async (req, res) => { try { const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query; const offset = (page - 1) * pageSize; @@ -80,7 +89,7 @@ router.get('/', async (req, res) => { }); // 创建设备 -router.post('/', async (req, res) => { +router.post('/', validateBody(createDeviceSchema), async (req, res) => { try { const device = await Device.create(req.body); @@ -602,7 +611,7 @@ router.post('/import', async (req, res) => { }); // 批量上线设备 -router.put('/batch-online', async (req, res) => { +router.put('/batch-online', validateBody(batchDeviceIdsSchema), async (req, res) => { try { const { deviceIds } = req.body; @@ -626,7 +635,7 @@ router.put('/batch-online', async (req, res) => { }); // 批量下线设备 -router.put('/batch-offline', async (req, res) => { +router.put('/batch-offline', validateBody(batchDeviceIdsSchema), async (req, res) => { try { const { deviceIds } = req.body; @@ -654,8 +663,6 @@ router.put('/batch-status', async (req, res) => { try { const { deviceIds, status } = req.body; - console.log('批量状态变更请求:', { deviceIds, status }); - if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) { return res.status(400).json({ error: '请提供有效的设备ID列表' }); } @@ -666,15 +673,11 @@ router.put('/batch-status', async (req, res) => { attributes: ['deviceId'] }); - console.log('数据库中找到的设备:', existingDevices.map(d => d.deviceId)); - console.log('请求的设备ID:', deviceIds); - // 检查是否有不存在的设备 const existingIds = existingDevices.map(d => d.deviceId); const missingIds = deviceIds.filter(id => !existingIds.includes(id)); if (missingIds.length > 0) { - console.log('不存在的设备ID:', missingIds); return res.status(404).json({ error: `设备不存在: ${missingIds.join(', ')}` }); } @@ -780,7 +783,7 @@ router.get('/:deviceId', async (req, res) => { }); // 更新设备 -router.put('/:deviceId', async (req, res) => { +router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => { try { // 获取旧设备信息以更新功率 const oldDevice = await Device.findByPk(req.params.deviceId); @@ -843,7 +846,7 @@ router.put('/batch-offline', async (req, res) => { }); // 批量删除设备 -router.delete('/batch-delete', async (req, res) => { +router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res) => { const t = await sequelize.transaction(); try { const { deviceIds } = req.body; @@ -1045,8 +1048,6 @@ router.put('/batch-status', async (req, res) => { try { const { deviceIds, status } = req.body; - console.log('批量状态变更请求:', { deviceIds, status }); - if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) { return res.status(400).json({ error: '请提供有效的设备ID列表' }); } @@ -1057,15 +1058,11 @@ router.put('/batch-status', async (req, res) => { attributes: ['deviceId'] }); - console.log('数据库中找到的设备:', existingDevices.map(d => d.deviceId)); - console.log('请求的设备ID:', deviceIds); - // 检查是否有不存在的设备 const existingIds = existingDevices.map(d => d.deviceId); const missingIds = deviceIds.filter(id => !existingIds.includes(id)); if (missingIds.length > 0) { - console.log('不存在的设备ID:', missingIds); return res.status(404).json({ error: `设备不存在: ${missingIds.join(', ')}` }); } diff --git a/backend/routes/racks.js b/backend/routes/racks.js index 6a780ad..353a272 100644 --- a/backend/routes/racks.js +++ b/backend/routes/racks.js @@ -6,6 +6,8 @@ const Room = require('../models/Room'); const XLSX = require('xlsx'); const fs = require('fs'); const path = require('path'); +const { validateBody, validateQuery } = require('../middleware/validation'); +const { createRackSchema, updateRackSchema, queryRackSchema } = require('../validation/rackSchema'); // 获取所有机柜 router.get('/', async (req, res) => { @@ -57,7 +59,7 @@ router.get('/:rackId', async (req, res) => { }); // 创建机柜 -router.post('/', async (req, res) => { +router.post('/', validateBody(createRackSchema), async (req, res) => { try { const rack = await Rack.create(req.body); res.status(201).json(rack); @@ -67,7 +69,7 @@ router.post('/', async (req, res) => { }); // 更新机柜 -router.put('/:rackId', async (req, res) => { +router.put('/:rackId', validateBody(updateRackSchema), async (req, res) => { try { const [updated] = await Rack.update(req.body, { where: { rackId: req.params.rackId } diff --git a/backend/routes/rooms.js b/backend/routes/rooms.js index 34fbe05..70a7de0 100644 --- a/backend/routes/rooms.js +++ b/backend/routes/rooms.js @@ -2,6 +2,8 @@ const express = require('express'); const router = express.Router(); const Room = require('../models/Room'); const Rack = require('../models/Rack'); +const { validateBody } = require('../middleware/validation'); +const { createRoomSchema, updateRoomSchema } = require('../validation/roomSchema'); // 获取所有机房 router.get('/', async (req, res) => { @@ -41,7 +43,7 @@ router.post('/', async (req, res) => { }); // 更新机房 -router.put('/:roomId', async (req, res) => { +router.put('/:roomId', validateBody(updateRoomSchema), async (req, res) => { try { const [updated] = await Room.update(req.body, { where: { roomId: req.params.roomId } diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js index 3b8d0f0..21ef8b5 100644 --- a/backend/routes/systemSettings.js +++ b/backend/routes/systemSettings.js @@ -20,19 +20,11 @@ const initDefaultSettings = async () => { // 外观设置 { settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true }, { settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true }, - { settingKey: 'compact_mode', settingValue: JSON.stringify(false), type: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true }, + { settingKey: 'compact_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true }, { settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true }, { settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true }, { settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true }, - // 数据备份设置 - { settingKey: 'auto_backup_enabled', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'backup', description: '启用自动备份', isEditable: true }, - { settingKey: 'backup_interval', settingValue: JSON.stringify(24), settingType: 'number', category: 'backup', description: '备份间隔(小时)', isEditable: true }, - { settingKey: 'backup_retention', settingValue: JSON.stringify(7), settingType: 'number', category: 'backup', description: '备份保留天数', isEditable: true }, - { settingKey: 'backup_path', settingValue: JSON.stringify('./backups'), settingType: 'string', category: 'backup', description: '备份存储路径', isEditable: true }, - { settingKey: 'last_backup_time', settingValue: JSON.stringify(null), settingType: 'string', category: 'backup', description: '上次备份时间', isEditable: false }, - { settingKey: 'backup_count', settingValue: JSON.stringify(0), settingType: 'number', category: 'backup', description: '备份文件数量', isEditable: false }, - // 关于页面 { settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false }, { settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true }, @@ -241,10 +233,6 @@ router.post('/reset/:key', async (req, res) => { sidebar_collapsed: false, table_row_height: 'default', animation_enabled: true, - auto_backup_enabled: false, - backup_interval: 24, - backup_retention: 7, - backup_path: './backups', company_name: '', contact_email: '', contact_phone: '', diff --git a/backend/scripts/migrate-consumable-version.js b/backend/scripts/migrate-consumable-version.js new file mode 100644 index 0000000..ea6e4eb --- /dev/null +++ b/backend/scripts/migrate-consumable-version.js @@ -0,0 +1,97 @@ +/** + * 耗材管理乐观锁迁移脚本 + * 为 consumables 表添加 version 字段 + */ + +const { sequelize, dbDialect } = require('../db'); + +async function migrate() { + try { + console.log('开始执行耗材乐观锁迁移...'); + console.log('数据库类型:', dbDialect); + + if (dbDialect === 'sqlite') { + // SQLite: 检查字段是否存在 + const tableInfo = await sequelize.query( + "PRAGMA table_info(consumables)", + { type: sequelize.QueryTypes.SELECT } + ); + + const hasVersion = tableInfo.some(col => col.name === 'version'); + + if (!hasVersion) { + console.log('添加 version 字段...'); + await sequelize.query( + "ALTER TABLE consumables ADD COLUMN version INTEGER DEFAULT 0" + ); + console.log('version 字段添加成功'); + } else { + console.log('version 字段已存在,跳过'); + } + + // 检查 updatedAt 索引 + const indexes = await sequelize.query( + "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='consumables'", + { type: sequelize.QueryTypes.SELECT } + ); + + const hasUpdatedAtIndex = indexes.some(idx => idx.name === 'consumables_updatedAt'); + + if (!hasUpdatedAtIndex) { + console.log('添加 updatedAt 索引...'); + await sequelize.query( + "CREATE INDEX consumables_updatedAt ON consumables(updatedAt)" + ); + console.log('updatedAt 索引添加成功'); + } else { + console.log('updatedAt 索引已存在,跳过'); + } + + } else if (dbDialect === 'mysql') { + // MySQL: 检查并添加字段 + try { + console.log('添加 version 字段...'); + await sequelize.query( + "ALTER TABLE consumables ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'" + ); + console.log('version 字段添加成功'); + } catch (err) { + if (err.message.includes('Duplicate column')) { + console.log('version 字段已存在,跳过'); + } else { + throw err; + } + } + + // 添加索引 + try { + console.log('添加 updatedAt 索引...'); + await sequelize.query( + "CREATE INDEX idx_consumables_updatedAt ON consumables(updatedAt)" + ); + console.log('updatedAt 索引添加成功'); + } catch (err) { + if (err.message.includes('Duplicate key')) { + console.log('updatedAt 索引已存在,跳过'); + } else { + throw err; + } + } + } + + // 初始化现有数据的 version 值 + console.log('初始化现有数据的 version 值...'); + await sequelize.query( + "UPDATE consumables SET version = 0 WHERE version IS NULL" + ); + console.log('version 值初始化完成'); + + console.log('迁移完成!'); + process.exit(0); + } catch (error) { + console.error('迁移失败:', error); + process.exit(1); + } +} + +migrate(); diff --git a/backend/validation/deviceSchema.js b/backend/validation/deviceSchema.js new file mode 100644 index 0000000..28ba6ea --- /dev/null +++ b/backend/validation/deviceSchema.js @@ -0,0 +1,327 @@ +const Joi = require('joi'); + +// 设备类型枚举 +const DEVICE_TYPES = ['server', 'switch', 'router', 'storage', 'other']; + +// 设备状态枚举 +const DEVICE_STATUS = ['running', 'maintenance', 'offline', 'fault']; + +// 创建设备验证Schema +const createDeviceSchema = Joi.object({ + deviceId: Joi.string() + .required() + .max(50) + .pattern(/^[a-zA-Z0-9_-]+$/) + .messages({ + 'string.empty': '设备ID不能为空', + 'string.max': '设备ID不能超过50个字符', + 'string.pattern.base': '设备ID只能包含字母、数字、下划线和横线', + 'any.required': '设备ID是必填字段' + }), + + name: Joi.string() + .required() + .max(100) + .messages({ + 'string.empty': '设备名称不能为空', + 'string.max': '设备名称不能超过100个字符', + 'any.required': '设备名称是必填字段' + }), + + type: Joi.string() + .valid(...DEVICE_TYPES) + .required() + .messages({ + 'string.empty': '设备类型不能为空', + 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}`, + 'any.required': '设备类型是必填字段' + }), + + model: Joi.string() + .max(100) + .allow('', null) + .messages({ + 'string.max': '型号不能超过100个字符' + }), + + serialNumber: Joi.string() + .max(100) + .allow('', null) + .messages({ + 'string.max': '序列号不能超过100个字符' + }), + + rackId: Joi.string() + .required() + .max(50) + .messages({ + 'string.empty': '机柜ID不能为空', + 'string.max': '机柜ID不能超过50个字符', + 'any.required': '机柜ID是必填字段' + }), + + position: Joi.number() + .integer() + .min(1) + .max(100) + .required() + .messages({ + 'number.base': '位置必须是数字', + 'number.integer': '位置必须是整数', + 'number.min': '位置不能小于1', + 'number.max': '位置不能大于100', + 'any.required': '位置是必填字段' + }), + + height: Joi.number() + .integer() + .min(1) + .max(50) + .required() + .messages({ + 'number.base': '高度必须是数字', + 'number.integer': '高度必须是整数', + 'number.min': '高度不能小于1', + 'number.max': '高度不能大于50', + 'any.required': '高度是必填字段' + }), + + powerConsumption: Joi.number() + .min(0) + .max(100000) + .default(0) + .messages({ + 'number.base': '功率必须是数字', + 'number.min': '功率不能小于0', + 'number.max': '功率不能超过100000' + }), + + ipAddress: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .allow('', null) + .messages({ + 'string.ip': 'IP地址格式无效' + }), + + status: Joi.string() + .valid(...DEVICE_STATUS) + .default('offline') + .messages({ + 'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}` + }), + + purchaseDate: Joi.date() + .allow(null) + .messages({ + 'date.base': '购买日期格式无效' + }), + + warrantyExpiry: Joi.date() + .allow(null) + .messages({ + 'date.base': '保修到期日期格式无效' + }), + + description: Joi.string() + .max(500) + .allow('', null) + .messages({ + 'string.max': '描述不能超过500个字符' + }), + + customFields: Joi.object() + .allow(null) +}).custom((value, helpers) => { + // 验证保修日期必须晚于购买日期 + if (value.purchaseDate && value.warrantyExpiry) { + const purchase = new Date(value.purchaseDate); + const warranty = new Date(value.warrantyExpiry); + if (warranty <= purchase) { + return helpers.error('date.warrantyAfterPurchase'); + } + } + return value; +}).messages({ + 'date.warrantyAfterPurchase': '保修到期日期必须晚于购买日期' +}); + +// 更新设备验证Schema(所有字段可选) +const updateDeviceSchema = Joi.object({ + deviceId: Joi.string() + .max(50) + .pattern(/^[a-zA-Z0-9_-]+$/) + .messages({ + 'string.max': '设备ID不能超过50个字符', + 'string.pattern.base': '设备ID只能包含字母、数字、下划线和横线' + }), + + name: Joi.string() + .max(100) + .messages({ + 'string.max': '设备名称不能超过100个字符' + }), + + type: Joi.string() + .valid(...DEVICE_TYPES) + .messages({ + 'any.only': `设备类型必须是以下之一: ${DEVICE_TYPES.join(', ')}` + }), + + model: Joi.string() + .max(100) + .allow('', null), + + serialNumber: Joi.string() + .max(100) + .allow('', null), + + rackId: Joi.string() + .max(50), + + position: Joi.number() + .integer() + .min(1) + .max(100) + .messages({ + 'number.base': '位置必须是数字', + 'number.integer': '位置必须是整数', + 'number.min': '位置不能小于1', + 'number.max': '位置不能大于100' + }), + + height: Joi.number() + .integer() + .min(1) + .max(50) + .messages({ + 'number.base': '高度必须是数字', + 'number.integer': '高度必须是整数', + 'number.min': '高度不能小于1', + 'number.max': '高度不能大于50' + }), + + powerConsumption: Joi.number() + .min(0) + .max(100000) + .messages({ + 'number.base': '功率必须是数字', + 'number.min': '功率不能小于0', + 'number.max': '功率不能超过100000' + }), + + ipAddress: Joi.string() + .ip({ version: ['ipv4', 'ipv6'] }) + .allow('', null) + .messages({ + 'string.ip': 'IP地址格式无效' + }), + + status: Joi.string() + .valid(...DEVICE_STATUS) + .messages({ + 'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}` + }), + + purchaseDate: Joi.date() + .allow(null), + + warrantyExpiry: Joi.date() + .allow(null), + + description: Joi.string() + .max(500) + .allow('', null), + + customFields: Joi.object() + .allow(null) +}).min(1).messages({ + 'object.min': '至少需要提供一个字段进行更新' +}); + +// 批量操作验证Schema +const batchDeviceIdsSchema = Joi.object({ + deviceIds: Joi.array() + .items(Joi.string().required()) + .min(1) + .required() + .messages({ + 'array.base': '设备ID列表必须是数组', + 'array.min': '至少需要提供一个设备ID', + 'any.required': '设备ID列表是必填字段' + }) +}); + +// 批量状态变更验证Schema +const batchStatusSchema = Joi.object({ + deviceIds: Joi.array() + .items(Joi.string().required()) + .min(1) + .required() + .messages({ + 'array.base': '设备ID列表必须是数组', + 'array.min': '至少需要提供一个设备ID', + 'any.required': '设备ID列表是必填字段' + }), + status: Joi.string() + .valid(...DEVICE_STATUS) + .required() + .messages({ + 'any.only': `状态必须是以下之一: ${DEVICE_STATUS.join(', ')}`, + 'any.required': '状态是必填字段' + }) +}); + +// 批量移动验证Schema +const batchMoveSchema = Joi.object({ + deviceIds: Joi.array() + .items(Joi.string().required()) + .min(1) + .required(), + targetRackId: Joi.string() + .required() + .max(50) + .messages({ + 'string.empty': '目标机柜ID不能为空', + 'any.required': '目标机柜ID是必填字段' + }), + startPosition: Joi.number() + .integer() + .min(1) + .allow(null) +}); + +// 查询参数验证Schema +const queryDeviceSchema = Joi.object({ + keyword: Joi.string() + .max(100) + .allow(''), + status: Joi.string() + .valid(...DEVICE_STATUS, 'all') + .allow(''), + type: Joi.string() + .valid(...DEVICE_TYPES, 'all') + .allow(''), + rackId: Joi.string() + .max(50) + .allow(''), + page: Joi.number() + .integer() + .min(1) + .default(1), + pageSize: Joi.number() + .integer() + .min(1) + .max(100) + .default(10) +}); + +module.exports = { + createDeviceSchema, + updateDeviceSchema, + batchDeviceIdsSchema, + batchStatusSchema, + batchMoveSchema, + queryDeviceSchema, + DEVICE_TYPES, + DEVICE_STATUS +}; diff --git a/backend/validation/rackSchema.js b/backend/validation/rackSchema.js new file mode 100644 index 0000000..cb98b85 --- /dev/null +++ b/backend/validation/rackSchema.js @@ -0,0 +1,171 @@ +const Joi = require('joi'); + +// 机柜状态枚举 +const RACK_STATUS = ['active', 'inactive', 'maintenance']; + +// 创建机柜验证Schema +const createRackSchema = Joi.object({ + rackId: Joi.string() + .required() + .max(50) + .pattern(/^[a-zA-Z0-9_-]+$/) + .messages({ + 'string.empty': '机柜ID不能为空', + 'string.max': '机柜ID不能超过50个字符', + 'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线', + 'any.required': '机柜ID是必填字段' + }), + + name: Joi.string() + .required() + .max(100) + .messages({ + 'string.empty': '机柜名称不能为空', + 'string.max': '机柜名称不能超过100个字符', + 'any.required': '机柜名称是必填字段' + }), + + height: Joi.number() + .integer() + .min(1) + .max(100) + .default(42) + .messages({ + 'number.base': '高度必须是数字', + 'number.integer': '高度必须是整数', + 'number.min': '高度不能小于1', + 'number.max': '高度不能大于100' + }), + + maxPower: Joi.number() + .min(0) + .max(1000000) + .default(10000) + .messages({ + 'number.base': '最大功率必须是数字', + 'number.min': '最大功率不能小于0', + 'number.max': '最大功率不能超过1000000' + }), + + currentPower: Joi.number() + .min(0) + .default(0) + .messages({ + 'number.base': '当前功率必须是数字', + 'number.min': '当前功率不能小于0' + }), + + status: Joi.string() + .valid(...RACK_STATUS) + .default('active') + .messages({ + 'any.only': `状态必须是以下之一: ${RACK_STATUS.join(', ')}` + }), + + roomId: Joi.string() + .required() + .max(50) + .messages({ + 'string.empty': '机房ID不能为空', + 'string.max': '机房ID不能超过50个字符', + 'any.required': '机房ID是必填字段' + }), + + description: Joi.string() + .max(500) + .allow('', null) + .messages({ + 'string.max': '描述不能超过500个字符' + }) +}); + +// 更新机柜验证Schema +const updateRackSchema = Joi.object({ + rackId: Joi.string() + .max(50) + .pattern(/^[a-zA-Z0-9_-]+$/) + .messages({ + 'string.max': '机柜ID不能超过50个字符', + 'string.pattern.base': '机柜ID只能包含字母、数字、下划线和横线' + }), + + name: Joi.string() + .max(100) + .messages({ + 'string.max': '机柜名称不能超过100个字符' + }), + + height: Joi.number() + .integer() + .min(1) + .max(100) + .messages({ + 'number.base': '高度必须是数字', + 'number.integer': '高度必须是整数', + 'number.min': '高度不能小于1', + 'number.max': '高度不能大于100' + }), + + maxPower: Joi.number() + .min(0) + .max(1000000) + .messages({ + 'number.base': '最大功率必须是数字', + 'number.min': '最大功率不能小于0', + 'number.max': '最大功率不能超过1000000' + }), + + currentPower: Joi.number() + .min(0) + .messages({ + 'number.base': '当前功率必须是数字', + 'number.min': '当前功率不能小于0' + }), + + status: Joi.string() + .valid(...RACK_STATUS) + .messages({ + 'any.only': `状态必须是以下之一: ${RACK_STATUS.join(', ')}` + }), + + roomId: Joi.string() + .max(50), + + description: Joi.string() + .max(500) + .allow('', null) + .messages({ + 'string.max': '描述不能超过500个字符' + }) +}).min(1).messages({ + 'object.min': '至少需要提供一个字段进行更新' +}); + +// 查询机柜验证Schema +const queryRackSchema = Joi.object({ + roomId: Joi.string() + .max(50) + .allow(''), + status: Joi.string() + .valid(...RACK_STATUS, 'all') + .allow(''), + keyword: Joi.string() + .max(100) + .allow(''), + page: Joi.number() + .integer() + .min(1) + .default(1), + pageSize: Joi.number() + .integer() + .min(1) + .max(100) + .default(10) +}); + +module.exports = { + createRackSchema, + updateRackSchema, + queryRackSchema, + RACK_STATUS +}; diff --git a/backend/validation/roomSchema.js b/backend/validation/roomSchema.js new file mode 100644 index 0000000..8c37171 --- /dev/null +++ b/backend/validation/roomSchema.js @@ -0,0 +1,120 @@ +const Joi = require('joi'); + +// 创建机房验证Schema +const createRoomSchema = Joi.object({ + roomId: Joi.string() + .required() + .max(50) + .pattern(/^[a-zA-Z0-9_-]+$/) + .messages({ + 'string.empty': '机房ID不能为空', + 'string.max': '机房ID不能超过50个字符', + 'string.pattern.base': '机房ID只能包含字母、数字、下划线和横线', + 'any.required': '机房ID是必填字段' + }), + + name: Joi.string() + .required() + .max(100) + .messages({ + 'string.empty': '机房名称不能为空', + 'string.max': '机房名称不能超过100个字符', + 'any.required': '机房名称是必填字段' + }), + + location: Joi.string() + .max(200) + .allow('', null) + .messages({ + 'string.max': '位置不能超过200个字符' + }), + + area: Joi.number() + .min(0) + .max(1000000) + .allow(null) + .messages({ + 'number.base': '面积必须是数字', + 'number.min': '面积不能小于0', + 'number.max': '面积不能超过1000000' + }), + + capacity: Joi.number() + .integer() + .min(0) + .max(10000) + .allow(null) + .messages({ + 'number.base': '容量必须是数字', + 'number.integer': '容量必须是整数', + 'number.min': '容量不能小于0', + 'number.max': '容量不能超过10000' + }), + + description: Joi.string() + .max(500) + .allow('', null) + .messages({ + 'string.max': '描述不能超过500个字符' + }) +}); + +// 更新机房验证Schema +const updateRoomSchema = Joi.object({ + roomId: Joi.string() + .max(50) + .pattern(/^[a-zA-Z0-9_-]+$/) + .messages({ + 'string.max': '机房ID不能超过50个字符', + 'string.pattern.base': '机房ID只能包含字母、数字、下划线和横线' + }), + + name: Joi.string() + .max(100) + .messages({ + 'string.max': '机房名称不能超过100个字符' + }), + + location: Joi.string() + .max(200) + .allow('', null) + .messages({ + 'string.max': '位置不能超过200个字符' + }), + + area: Joi.number() + .min(0) + .max(1000000) + .allow(null) + .messages({ + 'number.base': '面积必须是数字', + 'number.min': '面积不能小于0', + 'number.max': '面积不能超过1000000' + }), + + capacity: Joi.number() + .integer() + .min(0) + .max(10000) + .allow(null) + .messages({ + 'number.base': '容量必须是数字', + 'number.integer': '容量必须是整数', + 'number.min': '容量不能小于0', + 'number.max': '容量不能超过10000' + }), + + description: Joi.string() + .max(500) + .allow('', null) + .messages({ + 'string.max': '描述不能超过500个字符' + }) +}).min(1).messages({ + 'object.min': '至少需要提供一个字段进行更新' +}); + +module.exports = { + createRoomSchema, + updateRoomSchema +}; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index aa6d060..cc0c106 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, us import { useAuth } from './context/AuthContext'; import { ConfigProvider, useConfig } from './context/ConfigContext'; import { Scene3DProvider } from './context/Scene3DContext'; +import { useDesignTokens } from './hooks/useDesignTokens'; import { Spin } from 'antd'; const Dashboard = lazy(() => import('./pages/Dashboard')); @@ -59,55 +60,6 @@ const AuthLoading = () => ( ); -const designTokens = { - colors: { - primary: { - main: '#667eea', - gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - light: '#8b9ff0' - }, - success: { main: '#10b981' }, - warning: { main: '#f59e0b' }, - error: { main: '#ef4444' }, - text: { - primary: '#1e293b', - secondary: '#64748b', - inverse: '#ffffff' - }, - background: { - primary: '#ffffff', - secondary: '#f8fafc', - dark: '#1e293b' - }, - border: { - light: '#e2e8f0' - }, - sidebar: { - bg: '#ffffff', - bgHover: 'rgba(102, 126, 234, 0.08)', - bgActive: 'rgba(102, 126, 234, 0.15)', - text: '#475569', - textHover: '#667eea', - textActive: '#667eea', - border: '#e2e8f0' - } - }, - shadows: { - small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', - medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', - large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' - }, - borderRadius: { - small: '6px', - medium: '10px' - }, - spacing: { - sm: '8px', - md: '16px', - lg: '24px' - } -}; - const PrivateRoute = ({ children }) => { const { token, initialized, loading } = useAuth(); const location = useLocation(); @@ -133,9 +85,10 @@ const AppLayout = ({ children }) => { const [collapsed, setCollapsed] = useState(false); const [activeKey, setActiveKey] = useState('dashboard'); const { user, logout } = useAuth(); + const { config } = useConfig(); const navigate = useNavigate(); const location = useLocation(); - const { config } = useConfig(); + const designTokens = useDesignTokens(); const handleLogout = () => { logout(); @@ -155,56 +108,6 @@ const AppLayout = ({ children }) => { return 'dashboard'; }; - // 动态设计令牌 - const designTokens = { - colors: { - primary: { - main: config.primary_color || '#667eea', - gradient: `linear-gradient(135deg, ${config.primary_color || '#667eea'} 0%, ${config.secondary_color || '#764ba2'} 100%)`, - light: '#8b9ff0' - }, - success: { main: '#10b981' }, - warning: { main: '#f59e0b' }, - error: { main: '#ef4444' }, - text: { - primary: '#1e293b', - secondary: '#64748b', - inverse: '#ffffff' - }, - background: { - primary: '#ffffff', - secondary: '#f8fafc', - dark: '#1e293b' - }, - border: { - light: '#e2e8f0' - }, - sidebar: { - bg: '#ffffff', - bgHover: 'rgba(102, 126, 234, 0.08)', - bgActive: 'rgba(102, 126, 234, 0.15)', - text: '#475569', - textHover: config.primary_color || '#667eea', - textActive: config.primary_color || '#667eea', - border: '#e2e8f0' - } - }, - shadows: { - small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', - medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', - large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' - }, - borderRadius: { - small: '6px', - medium: '10px' - }, - spacing: { - sm: '8px', - md: '16px', - lg: '24px' - } - }; - const menuItems = [ { key: 'dashboard', @@ -518,57 +421,7 @@ const AppLayout = ({ children }) => { }; const ThemeConfig = () => { - const { config } = useConfig(); - - // 动态设计令牌 - const designTokens = { - colors: { - primary: { - main: config.primary_color || '#667eea', - gradient: `linear-gradient(135deg, ${config.primary_color || '#667eea'} 0%, ${config.secondary_color || '#764ba2'} 100%)`, - light: '#8b9ff0' - }, - success: { main: '#10b981' }, - warning: { main: '#f59e0b' }, - error: { main: '#ef4444' }, - text: { - primary: '#1e293b', - secondary: '#64748b', - inverse: '#ffffff' - }, - background: { - primary: '#ffffff', - secondary: '#f8fafc', - dark: '#1e293b' - }, - border: { - light: '#e2e8f0' - }, - sidebar: { - bg: '#ffffff', - bgHover: 'rgba(102, 126, 234, 0.08)', - bgActive: 'rgba(102, 126, 234, 0.15)', - text: '#475569', - textHover: config.primary_color || '#667eea', - textActive: config.primary_color || '#667eea', - border: '#e2e8f0' - } - }, - shadows: { - small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', - medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', - large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' - }, - borderRadius: { - small: '6px', - medium: '10px' - }, - spacing: { - sm: '8px', - md: '16px', - lg: '24px' - } - }; + const designTokens = useDesignTokens(); return ( diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 327a516..d75f0be 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -15,9 +15,20 @@ api.interceptors.request.use( const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; - } else { - console.log('[API] No token found in localStorage'); } + + // 开发环境下安全日志:过滤敏感字段 + if (process.env.NODE_ENV === 'development') { + const sensitiveFields = ['password', 'oldPassword', 'newPassword', 'confirmPassword']; + const safeData = config.data ? { ...config.data } : null; + if (safeData) { + sensitiveFields.forEach(field => { + if (safeData[field]) safeData[field] = '***'; + }); + } + console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`, safeData || ''); + } + return config; }, (error) => { diff --git a/frontend/src/components/3d/DeviceModel.jsx b/frontend/src/components/3d/DeviceModel.jsx index 12ef54e..b816133 100644 --- a/frontend/src/components/3d/DeviceModel.jsx +++ b/frontend/src/components/3d/DeviceModel.jsx @@ -179,7 +179,8 @@ const InstancedStatusLights = ({ count, positions, colors: statusColors, zOffset useEffect(() => { return () => { if (meshRef.current) { - meshRef.current.dispose(); + meshRef.current.geometry?.dispose(); + meshRef.current.material?.dispose(); } }; }, []); @@ -219,8 +220,14 @@ const InstancedDriveBays = ({ count, positions, color, hasDetail = true }) => { // 资源清理 useEffect(() => { return () => { - if (meshRef.current) meshRef.current.dispose(); - if (detailRef.current) detailRef.current.dispose(); + if (meshRef.current) { + meshRef.current.geometry?.dispose(); + meshRef.current.material?.dispose(); + } + if (detailRef.current) { + detailRef.current.geometry?.dispose(); + detailRef.current.material?.dispose(); + } }; }, []); @@ -265,8 +272,14 @@ const InstancedStorageBays = ({ count, positions, color }) => { // 资源清理 useEffect(() => { return () => { - if (meshRef.current) meshRef.current.dispose(); - if (detailRef.current) detailRef.current.dispose(); + if (meshRef.current) { + meshRef.current.geometry?.dispose(); + meshRef.current.material?.dispose(); + } + if (detailRef.current) { + detailRef.current.geometry?.dispose(); + detailRef.current.material?.dispose(); + } }; }, []); @@ -335,10 +348,22 @@ const InstancedRJ45Ports = ({ count, positions, statuses, frontZ }) => { // 资源清理 useEffect(() => { return () => { - if (meshRef.current) meshRef.current.dispose(); - if (innerRef.current) innerRef.current.dispose(); - if (tabRef.current) tabRef.current.dispose(); - if (ledRef.current) ledRef.current.dispose(); + if (meshRef.current) { + meshRef.current.geometry?.dispose(); + meshRef.current.material?.dispose(); + } + if (innerRef.current) { + innerRef.current.geometry?.dispose(); + innerRef.current.material?.dispose(); + } + if (tabRef.current) { + tabRef.current.geometry?.dispose(); + tabRef.current.material?.dispose(); + } + if (ledRef.current) { + ledRef.current.geometry?.dispose(); + ledRef.current.material?.dispose(); + } }; }, []); @@ -421,10 +446,22 @@ const InstancedSFPports = ({ count, positions, statuses, frontZ }) => { // 资源清理 useEffect(() => { return () => { - if (meshRef.current) meshRef.current.dispose(); - if (innerRef.current) innerRef.current.dispose(); - if (connectorRef.current) connectorRef.current.dispose(); - if (ledRef.current) ledRef.current.dispose(); + if (meshRef.current) { + meshRef.current.geometry?.dispose(); + meshRef.current.material?.dispose(); + } + if (innerRef.current) { + innerRef.current.geometry?.dispose(); + innerRef.current.material?.dispose(); + } + if (connectorRef.current) { + connectorRef.current.geometry?.dispose(); + connectorRef.current.material?.dispose(); + } + if (ledRef.current) { + ledRef.current.geometry?.dispose(); + ledRef.current.material?.dispose(); + } }; }, []); @@ -443,10 +480,16 @@ const FirewallFace = ({ device, height, frontZ, isSelected }) => { return ( + {/* 防火墙左侧红色标识条 */} - + + {/* 防火墙主面板边框 */} + + + + @@ -631,10 +674,16 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight return ( + {/* 服务器主面板 - 深蓝色 */} + {/* 服务器左侧标识条 - 亮蓝色 */} + + + + @@ -710,11 +759,17 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight return ( + {/* 存储设备主面板 - 深紫色 */} - + {/* 存储设备左侧标识条 - 紫色 */} + + + + + + {/* 交换机主面板 - 深绿色背景突出显示 */} - + + + {/* 交换机左侧标识条 - 亮绿色 */} + + + + + {/* 交换机类型标识 */} + + + @@ -969,13 +1035,18 @@ const DeviceModel = ({ device, rackHeight, isSelected, onClick, onHover, uHeight return ( {/* 默认面板纹理 */} - - - + + + + + {/* 左侧灰色标识条 */} + + + {/* 装饰线 */} - - + + diff --git a/frontend/src/components/3d/Scene.jsx b/frontend/src/components/3d/Scene.jsx index 913e052..10f819b 100644 --- a/frontend/src/components/3d/Scene.jsx +++ b/frontend/src/components/3d/Scene.jsx @@ -1,5 +1,5 @@ -import React, { Suspense, useMemo, useRef, useEffect } from 'react'; -import { Canvas, useFrame } from '@react-three/fiber'; +import React, { Suspense, useMemo, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei'; const envMapUrl = '/assets/3d/env.hdr'; import RackModel from './RackModel'; @@ -10,16 +10,50 @@ import * as THREE from 'three'; const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); // 检测是否为小屏幕 const isSmallScreen = window.innerWidth < 768; -// 移动端或小屏幕使用 dpr=1,桌面端使用 dpr=[1, 1.5] -const deviceDpr = isMobile || isSmallScreen ? 1 : [1, 1.5]; +// 移动端或小屏幕使用 dpr=1,桌面端使用 dpr=[1, 2] 提升清晰度 +const deviceDpr = isMobile || isSmallScreen ? 1 : [1, 2]; + +// 创建全局 ref 用于外部访问 controls +const controlsRefGlobal = { current: null }; // 内部组件用于处理 OrbitControls -const Controls = ({ rack }) => { +const Controls = ({ rack, onControlsReady }) => { const controlsRef = useRef(); + const { camera } = useThree(); // 机柜中心点(中轴线) - const targetY = (rack?.height || 45) * 0.04445 / 2 + 0.5; + const rackHeight = rack?.height || 45; + const targetY = rackHeight * 0.04445 / 2 + 0.5; const fixedTarget = useMemo(() => new THREE.Vector3(0, targetY, 0), [targetY]); + // 根据机柜高度计算合适的相机距离限制 + const minDistance = useMemo(() => Math.max(1.5, rackHeight * 0.04445 * 0.3), [rackHeight]); + const maxDistance = useMemo(() => Math.max(8, rackHeight * 0.04445 * 1.5), [rackHeight]); + + // 保存相机初始位置用于重置 + const initialCameraPosition = useMemo(() => { + const rackHeightMeters = rackHeight * 0.04445; + const baseHeight = 2; + const heightFactor = rackHeightMeters * 0.6; + const distance = Math.max(3, rackHeightMeters * 1.2); + return new THREE.Vector3(distance * 0.7, baseHeight + heightFactor * 0.3, distance); + }, [rackHeight]); + + useEffect(() => { + if (controlsRef.current) { + controlsRefGlobal.current = controlsRef.current; + if (onControlsReady) { + onControlsReady({ + reset: () => { + // 重置相机位置 + camera.position.copy(initialCameraPosition); + controlsRef.current.target.copy(fixedTarget); + controlsRef.current.update(); + } + }); + } + } + }, [controlsRef.current, camera, initialCameraPosition, fixedTarget, onControlsReady]); + useFrame(() => { if (controlsRef.current) { // 强制保持 target 在机柜中轴线 @@ -32,17 +66,19 @@ const Controls = ({ rack }) => { { ); }; -const Scene = ({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }) => { +const Scene = forwardRef(({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeave }, ref) => { // 从 Context 获取3D场景状态 const { devices, @@ -60,6 +96,18 @@ const Scene = ({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeav deviceSlideEnabled } = useScene3D(); + // 用于存储 controls API + const controlsApiRef = useRef(null); + + // 使用 useImperativeHandle 暴露重置方法给父组件 + useImperativeHandle(ref, () => ({ + resetView: () => { + if (controlsApiRef.current) { + controlsApiRef.current.reset(); + } + } + })); + // 使用 useMemo 稳定 props 引用 const rackModelProps = useMemo(() => ({ rack, @@ -72,47 +120,63 @@ const Scene = ({ rack, tooltipFields, onDeviceClick, onDeviceHover, onDeviceLeav deviceSlideEnabled }), [rack, devices, selectedDevice, onDeviceClick, onDeviceLeave, onDeviceHover, tooltipFields, deviceSlideEnabled]); + // 根据机柜高度动态计算相机初始位置 + const rackHeight = rack?.height || 45; + const rackHeightMeters = rackHeight * 0.04445; + // 相机位置:确保能完整看到机柜,高度随机柜高度调整 + const cameraPosition = useMemo(() => { + const baseHeight = 2; + const heightFactor = rackHeightMeters * 0.6; + const distance = Math.max(3, rackHeightMeters * 1.2); + return [distance * 0.7, baseHeight + heightFactor * 0.3, distance]; + }, [rackHeightMeters]); + + // 相机目标点(机柜中心) + const cameraTarget = useMemo(() => { + return [0, rackHeightMeters / 2 + 0.5, 0]; + }, [rackHeightMeters]); + return ( - - + + - - + - + {/* Models */} - + {/* Controls - 使用独立组件保持旋转中心固定 */} - + { controlsApiRef.current = api; }} /> ); -}; +}); export default Scene; \ No newline at end of file diff --git a/frontend/src/hooks/useDesignTokens.js b/frontend/src/hooks/useDesignTokens.js new file mode 100644 index 0000000..c43da6c --- /dev/null +++ b/frontend/src/hooks/useDesignTokens.js @@ -0,0 +1,90 @@ +import { useMemo } from 'react'; +import { useConfig } from '../context/ConfigContext'; + +/** + * 使用设计令牌 Hook + * 集中管理主题配置,避免在多个组件中重复定义 + * @returns {Object} 设计令牌对象 + */ +export const useDesignTokens = () => { + const { config } = useConfig(); + + const designTokens = useMemo(() => { + const primaryColor = config?.primary_color || '#667eea'; + const secondaryColor = config?.secondary_color || '#764ba2'; + + return { + colors: { + primary: { + main: primaryColor, + gradient: `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`, + light: '#8b9ff0' + }, + success: { main: '#10b981' }, + warning: { main: '#f59e0b' }, + error: { main: '#ef4444' }, + text: { + primary: '#1e293b', + secondary: '#64748b', + inverse: '#ffffff' + }, + background: { + primary: '#ffffff', + secondary: '#f8fafc', + dark: '#1e293b' + }, + border: { + light: '#e2e8f0' + }, + sidebar: { + bg: '#ffffff', + bgHover: `rgba(${hexToRgb(primaryColor)}, 0.08)`, + bgActive: `rgba(${hexToRgb(primaryColor)}, 0.15)`, + text: '#475569', + textHover: primaryColor, + textActive: primaryColor, + border: '#e2e8f0' + } + }, + shadows: { + small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', + large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)' + }, + borderRadius: { + small: '6px', + medium: '10px' + }, + spacing: { + sm: '8px', + md: '16px', + lg: '24px' + } + }; + }, [config?.primary_color, config?.secondary_color]); + + return designTokens; +}; + +/** + * 将十六进制颜色转换为RGB字符串 + * @param {string} hex - 十六进制颜色值 + * @returns {string} RGB字符串 (如: "102, 126, 234") + */ +function hexToRgb(hex) { + // 移除 # 号 + const cleanHex = hex.replace('#', ''); + + // 处理简写格式 (如: #fff) + const fullHex = cleanHex.length === 3 + ? cleanHex.split('').map(c => c + c).join('') + : cleanHex; + + const r = parseInt(fullHex.substring(0, 2), 16); + const g = parseInt(fullHex.substring(2, 4), 16); + const b = parseInt(fullHex.substring(4, 6), 16); + + return `${r}, ${g}, ${b}`; +} + +export default useDesignTokens; diff --git a/frontend/src/pages/Rack3DVisualization.jsx b/frontend/src/pages/Rack3DVisualization.jsx index 9aca32b..ed90afe 100644 --- a/frontend/src/pages/Rack3DVisualization.jsx +++ b/frontend/src/pages/Rack3DVisualization.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Layout, Select, Card, Spin, message, Typography, Descriptions, Tag, Button, Space, Empty, Modal, Form, Input, InputNumber, DatePicker, Checkbox, Switch } from 'antd'; -import { CloudServerOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, UpOutlined, DownOutlined, EditOutlined, SettingOutlined, FullscreenOutlined } from '@ant-design/icons'; +import { CloudServerOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, UpOutlined, DownOutlined, EditOutlined, SettingOutlined, FullscreenOutlined, EyeOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import dayjs from 'dayjs'; @@ -17,7 +17,10 @@ const { Option } = Select; const Rack3DVisualization = () => { const navigate = useNavigate(); - + + // Scene 组件的 ref,用于调用重置视角方法 + const sceneRef = useRef(null); + // 使用 Scene3DContext 管理3D场景状态 const { devices, @@ -465,16 +468,26 @@ const Rack3DVisualization = () => { ))} - +
{
) : selectedRack ? ( <> - { const [saving, setSaving] = useState(false); const [settings, setSettings] = useState({}); const [activeTab, setActiveTab] = useState('general'); - const [backupList, setBackupList] = useState([]); - const [backupLoading, setBackupLoading] = useState(false); const [systemInfo, setSystemInfo] = useState(null); const [form] = Form.useForm(); const { reloadConfig } = useConfig(); useEffect(() => { fetchSettings(); - fetchBackupList(); fetchSystemInfo(); }, []); @@ -42,18 +39,6 @@ const SystemSettings = () => { } }; - const fetchBackupList = async () => { - setBackupLoading(true); - try { - const response = await axios.get('/api/system-settings/backup/list'); - setBackupList(response.data.backups || []); - } catch (error) { - console.error('获取备份列表失败'); - } finally { - setBackupLoading(false); - } - }; - const fetchSystemInfo = async () => { try { const response = await axios.get('/api/system-settings/system/info'); @@ -85,66 +70,6 @@ const SystemSettings = () => { } }; - const handleCreateBackup = async () => { - Modal.confirm({ - title: '确认创建备份', - icon: , - content: '确定要创建系统备份吗?这将导出所有设备、机柜、机房和耗材数据。', - onOk: async () => { - try { - message.loading('正在创建备份...', 0); - const response = await axios.post('/api/system-settings/backup'); - message.destroy(); - message.success('备份创建成功'); - fetchBackupList(); - } catch (error) { - message.destroy(); - message.error('备份创建失败'); - } - } - }); - }; - - const handleRestoreBackup = (filename) => { - Modal.confirm({ - title: '确认恢复备份', - icon: , - content: `确定要恢复备份 "${filename}" 吗?当前数据将被覆盖,且此操作不可撤销。`, - onOk: async () => { - try { - message.loading('正在恢复备份...', 0); - await axios.post('/api/system-settings/backup/restore', { filename }); - message.destroy(); - message.success('恢复成功,请刷新页面查看最新数据'); - } catch (error) { - message.destroy(); - message.error('恢复备份失败'); - } - } - }); - }; - - const handleDeleteBackup = (filename) => { - Modal.confirm({ - title: '确认删除备份', - icon: , - content: `确定要删除备份 "${filename}" 吗?`, - onOk: async () => { - try { - await axios.delete(`/api/system-settings/backup/${filename}`); - message.success('删除成功'); - fetchBackupList(); - } catch (error) { - message.error('删除失败'); - } - } - }); - }; - - const handleDownloadBackup = (filename) => { - window.open(`/api/system-settings/backup/download/${filename}`, '_blank'); - }; - const handleResetSetting = (key) => { Modal.confirm({ title: '确认重置', @@ -289,10 +214,6 @@ const SystemSettings = () => { { value: 'false', label: '展开' }, { value: 'true', label: '折叠' } ], - auto_backup_enabled: [ - { value: 'false', label: '关闭' }, - { value: 'true', label: '开启' } - ] }; return optionsMap[key] || []; }; @@ -352,86 +273,7 @@ const SystemSettings = () => { ); }; - const renderBackupSettings = () => { - const backupColumns = [ - { - title: '文件名', - dataIndex: 'filename', - key: 'filename', - render: (text) => {text} - }, - { - title: '大小', - dataIndex: 'size', - key: 'size', - render: (size) => { - const kb = size / 1024; - return kb < 1024 ? `${kb.toFixed(2)} KB` : `${(kb / 1024).toFixed(2)} MB`; - } - }, - { - title: '创建时间', - dataIndex: 'createdAt', - key: 'createdAt', - render: (date) => new Date(date).toLocaleString('zh-CN') - }, - { - title: '操作', - key: 'action', - render: (_, record) => ( - - - - - - ) - } - ]; - - // 备份设置键列表 - const backupKeys = ['auto_backup_enabled', 'backup_interval', 'backup_retention', 'backup_path', 'last_backup_time', 'backup_count']; - - return ( -
- -
- {backupKeys.map(key => { - if (settings[key]) { - return renderFormItem(key, settings[key]); - } - return null; - })} - - - - - -
-
- - - - - - - - - - - ); - }; + // 数据备份功能已移除 const renderAboutPage = () => { const aboutKeys = ['app_version', 'company_name', 'contact_email', 'contact_phone', 'company_address', 'system_description', 'privacy_policy', 'terms_of_service']; @@ -504,12 +346,6 @@ const SystemSettings = () => { > {renderAppearanceSettings()} - 数据备份} - key="backup" - > - {renderBackupSettings()} - 关于} key="about" diff --git a/frontend/src/pages/TicketManagement.jsx b/frontend/src/pages/TicketManagement.jsx index 61c8383..10aa816 100644 --- a/frontend/src/pages/TicketManagement.jsx +++ b/frontend/src/pages/TicketManagement.jsx @@ -4,6 +4,17 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined import axios from 'axios'; import dayjs from 'dayjs'; +// 安全地从 localStorage 获取用户信息 +const getUserFromStorage = () => { + try { + const userStr = localStorage.getItem('user'); + return userStr ? JSON.parse(userStr) : {}; + } catch (e) { + console.error('解析用户信息失败:', e); + return {}; + } +}; + const { Option } = Select; const { RangePicker } = DatePicker; const { TextArea } = Input; @@ -412,7 +423,7 @@ function TicketManagement() { await axios.put(`/api/tickets/${editingTicket.ticketId}`, ticketData); message.success('工单更新成功'); } else { - const user = JSON.parse(localStorage.getItem('user') || '{}'); + const user = getUserFromStorage(); ticketData.reporterId = user.userId || localStorage.getItem('userId') || 'USER001'; ticketData.reporterName = user.username || '系统用户'; await axios.post('/api/tickets', ticketData); @@ -457,10 +468,11 @@ function TicketManagement() { const handleProcessSubmit = useCallback(async (values) => { try { + const user = getUserFromStorage(); await axios.put(`/api/tickets/${selectedTicket.ticketId}/process`, { ...values, operatorId: localStorage.getItem('userId'), - operatorName: JSON.parse(localStorage.getItem('user') || '{}').username + operatorName: user.username }); message.success('工单处理完成'); setProcessingModalVisible(false); @@ -473,10 +485,11 @@ function TicketManagement() { const handleStatusChange = useCallback(async (ticketId, newStatus) => { try { + const user = getUserFromStorage(); await axios.put(`/api/tickets/${ticketId}/status`, { status: newStatus, operatorId: localStorage.getItem('userId'), - operatorName: JSON.parse(localStorage.getItem('user') || '{}').username + operatorName: user.username }); message.success('状态更新成功'); fetchTickets(); diff --git a/frontend/src/utils/crypto.js b/frontend/src/utils/crypto.js new file mode 100644 index 0000000..4be82e7 --- /dev/null +++ b/frontend/src/utils/crypto.js @@ -0,0 +1,27 @@ +/** + * 前端密码加密工具 + * 注意:这只是增加一层保护,真正的安全需要 HTTPS + */ + +/** + * 使用 SHA-256 对密码进行哈希 + * @param {string} password - 明文密码 + * @returns {Promise} - 返回十六进制哈希值 + */ +export async function hashPassword(password) { + const encoder = new TextEncoder(); + const data = encoder.encode(password); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} + +/** + * 为登录请求准备密码(双重哈希:SHA-256 + 服务器 bcrypt) + * @param {string} password - 明文密码 + * @returns {Promise} - 哈希后的密码 + */ +export async function preparePassword(password) { + if (!password) return password; + return await hashPassword(password); +}