From 16c005556f9c00fcac6c92b0ba722a10f3825b41 Mon Sep 17 00:00:00 2001 From: langzihan Date: Thu, 24 Oct 2024 09:23:39 +0800 Subject: [PATCH] SCLP --- SCLP/Pipfile | 24 + SCLP/Pipfile.lock | 685 ++++++++++++++ SCLP/app.py | 99 ++ SCLP/auto_callback.py | 51 + SCLP/backend.py | 38 + SCLP/config.py | 50 + SCLP/data/mvboli.ttf | Bin 0 -> 78272 bytes SCLP/device/call.py | 323 +++++++ SCLP/device/message.py | 100 ++ SCLP/device/services.py | 520 ++++++++++ SCLP/image_server.py | 80 ++ SCLP/main.py | 41 + SCLP/models/brands.py | 54 ++ SCLP/models/devices.py | 793 ++++++++++++++++ SCLP/models/householders.py | 1369 +++++++++++++++++++++++++++ SCLP/models/houses.py | 303 ++++++ SCLP/models/parkinglots.py | 1019 ++++++++++++++++++++ SCLP/models/products.py | 113 +++ SCLP/models/sessions.py | 113 +++ SCLP/models/spaces.py | 95 ++ SCLP/models/subsystem.py | 200 ++++ SCLP/readme.md | 34 + SCLP/routers/access_devices_0.py | 77 ++ SCLP/routers/access_devices_1.py | 193 ++++ SCLP/routers/devices_manage.py | 30 + SCLP/routers/edge_simulation_api.py | 163 ++++ SCLP/routers/householder_manage.py | 84 ++ SCLP/routers/houses_manage.py | 86 ++ SCLP/routers/login.py | 140 +++ SCLP/routers/parkinglot_0.py | 85 ++ SCLP/routers/parkinglot_1.py | 108 +++ SCLP/routers/space_manage.py | 101 ++ SCLP/run.sh | 1 + SCLP/utils/database.py | 85 ++ SCLP/utils/logger.py | 163 ++++ SCLP/utils/misc.py | 360 +++++++ 36 files changed, 7780 insertions(+) create mode 100644 SCLP/Pipfile create mode 100644 SCLP/Pipfile.lock create mode 100644 SCLP/app.py create mode 100644 SCLP/auto_callback.py create mode 100644 SCLP/backend.py create mode 100644 SCLP/config.py create mode 100644 SCLP/data/mvboli.ttf create mode 100644 SCLP/device/call.py create mode 100644 SCLP/device/message.py create mode 100644 SCLP/device/services.py create mode 100644 SCLP/image_server.py create mode 100644 SCLP/main.py create mode 100644 SCLP/models/brands.py create mode 100644 SCLP/models/devices.py create mode 100644 SCLP/models/householders.py create mode 100644 SCLP/models/houses.py create mode 100644 SCLP/models/parkinglots.py create mode 100644 SCLP/models/products.py create mode 100644 SCLP/models/sessions.py create mode 100644 SCLP/models/spaces.py create mode 100644 SCLP/models/subsystem.py create mode 100644 SCLP/readme.md create mode 100644 SCLP/routers/access_devices_0.py create mode 100644 SCLP/routers/access_devices_1.py create mode 100644 SCLP/routers/devices_manage.py create mode 100644 SCLP/routers/edge_simulation_api.py create mode 100644 SCLP/routers/householder_manage.py create mode 100644 SCLP/routers/houses_manage.py create mode 100644 SCLP/routers/login.py create mode 100644 SCLP/routers/parkinglot_0.py create mode 100644 SCLP/routers/parkinglot_1.py create mode 100644 SCLP/routers/space_manage.py create mode 100644 SCLP/run.sh create mode 100644 SCLP/utils/database.py create mode 100644 SCLP/utils/logger.py create mode 100644 SCLP/utils/misc.py diff --git a/SCLP/Pipfile b/SCLP/Pipfile new file mode 100644 index 0000000..c88c987 --- /dev/null +++ b/SCLP/Pipfile @@ -0,0 +1,24 @@ +[[source]] +url = "https://mirrors.ustc.edu.cn/pypi/simple" +verify_ssl = false +name = "pip_conf_index_global" + +[packages] +fastapi = "*" +uvicorn = "*" +argparse = "*" +pillow = "*" +paho-mqtt = "*" +requests = "*" +pandas = "*" +openpyxl = "*" +xlrd = "*" +aiofiles = "*" +psutil = "*" +python-multipart = "*" + +[dev-packages] + +[requires] +python_version = "3.10" +python_full_version = "3.10.14" diff --git a/SCLP/Pipfile.lock b/SCLP/Pipfile.lock new file mode 100644 index 0000000..85f4ddd --- /dev/null +++ b/SCLP/Pipfile.lock @@ -0,0 +1,685 @@ +{ + "_meta": { + "hash": { + "sha256": "44345757f24b73dd5c0974ed045d2479e1773441b6d789525398bd83e837bfb9" + }, + "pipfile-spec": 6, + "requires": { + "python_full_version": "3.10.14", + "python_version": "3.10" + }, + "sources": [ + { + "name": "pip_conf_index_global", + "url": "https://mirrors.ustc.edu.cn/pypi/simple", + "verify_ssl": false + } + ] + }, + "default": { + "aiofiles": { + "hashes": [ + "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", + "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==24.1.0" + }, + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, + "anyio": { + "hashes": [ + "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", + "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7" + ], + "markers": "python_version >= '3.8'", + "version": "==4.4.0" + }, + "argparse": { + "hashes": [ + "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", + "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314" + ], + "index": "pip_conf_index_global", + "version": "==1.4.0" + }, + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "et-xmlfile": { + "hashes": [ + "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", + "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, + "exceptiongroup": { + "hashes": [ + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.2" + }, + "fastapi": { + "hashes": [ + "sha256:3d4729c038414d5193840706907a41839d839523da6ed0c2811f1168cac1798c", + "sha256:db84b470bd0e2b1075942231e90e3577e12a903c4dc8696f0d206a7904a7af1c" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==0.112.2" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "idna": { + "hashes": [ + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" + ], + "markers": "python_version >= '3.6'", + "version": "==3.8" + }, + "numpy": { + "hashes": [ + "sha256:06156c55771da4952a2432aa457cd96159675dcab4336f5307bff042535cb6ea", + "sha256:06d14d20b7e98c8c06bb62e56f2b64747dd10c422bb8adbf1e6dd82cd8442e12", + "sha256:0816fd52956e14551d8d71319d4b4fcfa1bcb21641f2c603f4eb64c65b1e1009", + "sha256:08801848a40aea24ce16c2ecde3b756f9ad756586fb2d13210939eb69b023f5b", + "sha256:0937e54c09f7a9a68da6889362ddd2ff584c02d015ec92672c099b61555f8911", + "sha256:0a5a25ab780b8c29e443824abefc6ca79047ceeb889a6f76d7b1953649498e93", + "sha256:0ab32eb9170bf8ffcbb14f11613f4a0b108d3ffee0832457c5d4808233ba8977", + "sha256:0abb3916a35d9090088a748636b2c06dc9a6542f99cd476979fb156a18192b84", + "sha256:0af3a5987f59d9c529c022c8c2a64805b339b7ef506509fba7d0556649b9714b", + "sha256:0c489f6c47bbed44918c9c8036a679614920da2a45f481d0eca2ad168ca5327f", + "sha256:10e2350aea18d04832319aac0f887d5fcec1b36abd485d14f173e3e900b83e33", + "sha256:12b38b0f3ddc1342863a6849f4fcb3f506e1d21179ebd34b7aa55a30cb50899f", + "sha256:140c5ce21f1eccb254e550c8431825cb716eb76e896202cffa7a0d2a843506da", + "sha256:14dea4f0d62ddd1a7f9d7b0003b35a537ac41a2b6205deec8c9c25a8e01748b4", + "sha256:15c6bde88f242747258cfee803f3161b7a2c1ffead0817e409d95444a79b4029", + "sha256:15ef8b2177eeb7e37dd5ef4016f30b7659c57c2c0b57a779f1d537ff33a72c7b", + "sha256:17581a2080012afe603c43005c9d050570e54683dde0d395e3edb4fa9c25f328", + "sha256:1a4f960e2e5c1084cf6b1d15482a5556ecc122855d631a2395063ab703d62fdd", + "sha256:1d9e0ddfb33a7a78fe92d49aaa2992a78ed5aff4cef7a21d8b1057cca075cc85", + "sha256:1f817c71683fd1bb5cff1529a1d085a57f02ccd2ebc5cd2c566f9a01118e3b7d", + "sha256:24003ba8ff22ea29a8c306e61d316ac74111cebf942afbf692df65509a05f111", + "sha256:2b0e379a15c6b8eb69bb8170d10cfbb8a0dc9126b5402ee8860a2646f4111c3d", + "sha256:2d3d1e61191e408a11658a64e9f9bb61741ad28c160576c95dac9df6f74713b4", + "sha256:30014b234f07b5fec20f4146f69e13cfb1e33ee9a18a1879a0142fbb00d47673", + "sha256:343e3e152bf5a087511cd325e3b7ecfd5b92d369e80e74c12cd87826e263ec06", + "sha256:378cb4f24c7d93066ee4103204f73ed046eb88f9ad5bb2275bb9fa0f6a02bd36", + "sha256:398049e237d1aae53d82a416dade04defed1a47f87d18d5bd615b6e7d7e41d1f", + "sha256:3a3336fbfa0d38d3deacd3fe7f3d07e13597f29c13abf4d15c3b6dc2291cbbdd", + "sha256:3b534c62b1887b4bfa80f633485f2a9338f5d46d720b6cc695d2ba8b38d98987", + "sha256:3e9276bff9a57100b53e5f9c44469baca1e58ec612e5143db568d69ec27b65ea", + "sha256:3f79d241e4833a2a570b6e6639d2114d497011e48a351798bba81fda51560ab7", + "sha256:442596f01913656d579309edcd179a2a2f9977d9a14ff41d042475280fc7f34e", + "sha256:44e44973262dc3ae79e9063a1284a73e09d01b894b534a769732ccd46c28cc62", + "sha256:47f11bf152d8707217feb46e9662a8b1aa3554a8ee56b64d2aa99c3e9914f101", + "sha256:48a724dbfad6f4933e2c8a22239980e1b5bc16868df3450cc4ebeb9522b7902f", + "sha256:4c33387be8eadc07d0834e0b9e2ead53117fe76ab2dadd37ee80d1df80be4c05", + "sha256:4e08e733600647242a9046b6aff888e72fe8a846b00855e5136e7641b08d25d8", + "sha256:4f9317da3aa64d0ee93950d3f319b3fe0169500e25c18223715cba39e89808bd", + "sha256:50b3dab872001b87052532bd4da3879fda856a2cf6c9418c19bfc94dc290e259", + "sha256:524b5311d21741f0b3f48efcdd3442546be3b38507a4e3b0f5138e4340f5dee0", + "sha256:53979581e6acdd75b7ce94e6d3b70994f9f8cf1021316d388a159f7f388bdc7f", + "sha256:54139e0eb219f52f60656d163cbe67c31ede51d13236c950145473504fa208cb", + "sha256:5474dad8c86ee9ba9bb776f4b99ef2d41b3b8f4e0d199d4f7304728ed34d0300", + "sha256:54c6a63e9d81efe64bfb7bcb0ec64332a87d0b87575f6009c8ba67ea6374770b", + "sha256:5821c9831fad20cd1a8621a9ed483322ca97a9da9832690a4050ffedcb3e766b", + "sha256:58a07f2947aa06ca03d922a86ac30e403ce8282cd15904606bac852bf29ea2ad", + "sha256:590acae9e4b0baa895850c0edab988c329a196bacc7326f3249fa5fe7b94e5a8", + "sha256:61cf71f62033987ed49b78a19465f40fcbf6f7e94674eda21096ebde6935c2e0", + "sha256:624884b572dff8ca8f60fab591413f077471de64e376b17d291b19f56504b2bb", + "sha256:6326ab99b52fafdcdeccf602d6286191a79fe2fda0ae90573c5814cd2b0bc1b8", + "sha256:64e8de086d2e4dac41fa286412321469b4535677184e78cc78e5061b44f0e4bf", + "sha256:652e92fc409e278abdd61e9505649e3938f6d04ce7ef1953f2ec598a50e7c195", + "sha256:6c1de77ded79fef664d5098a66810d4d27ca0224e9051906e634b3f7ead134c2", + "sha256:713cb46d266514db773de52af677aa931cc896a4f5e52f494449c4ff53ce6051", + "sha256:76368c788ccb4f4782cf9c842b316140142b4cbf22ff8db82724e82fe1205dce", + "sha256:77fa9826cbc7273e4bc3b7aa289b86936c942fe2c91bc35617c2417e14421592", + "sha256:7a894c51fd8c4e834f00ac742abad73fc485df1062f1b875661a3c1e1fb1c2f6", + "sha256:7bcb4f360dc9e29b4f5f9fb36b35b429e731373843ccf39a22105bd809ef3138", + "sha256:7dc90da0081f7e1da49ec4e398ede6a8e9cc4f5ebe5f9e06b443ed889ee9aaa2", + "sha256:848c6b5cad9898e4b9ef251b6f934fa34630371f2e916261070a4eb9092ffd33", + "sha256:86cc61c5479ed3b324011aa69484cae8f491b7f58dc0e54acf0894bdb4fae879", + "sha256:899da829b362ade41e1e7eccad2cf274035e1cb36ba73034946fccd4afd8606b", + "sha256:8ab81ccd753859ab89e67199b9da62c543850f819993761c1e94a75a814ed667", + "sha256:8ee3ab33c02a0bd7d219a184c9bc43811de373551529981035673ca2a1ba7b93", + "sha256:8fb49a0ba4d8f41198ae2d52118b050fd34dace4b8f3fb0ee34e23eb4ae775b1", + "sha256:9156ca1f79fc4acc226696e95bfcc2b486f165a6a59ebe22b2c1f82ab190384a", + "sha256:9523f8b46485db6939bd069b28b642fec86c30909cea90ef550373787f79530e", + "sha256:98a1486861fa3c603a5a3ccd56fc45b9756372bb30f6fb559b898fc2fad82e0d", + "sha256:9a6bdc19830703eee91e7eb2d671b165febefbf5eec6a4f163d1833d23be17af", + "sha256:a0756a179afa766ad7cb6f036de622e8a8f16ffdd55aa31f296c870b5679d745", + "sha256:a0cdef204199278f5c461a0bed6ed2e052998276e6d8ab2963d5b5c39a0500bc", + "sha256:ab83adc099ec62e044b1fbb3a05499fa1e99f6d53a1dde102b2d85eff66ed324", + "sha256:b34fa5e3b5d6dc7e0a4243fa0f81367027cb6f4a7215a17852979634b5544ee0", + "sha256:b47c551c6724960479cefd7353656498b86e7232429e3a41ab83be4da1b109e8", + "sha256:be3ddd26a22d032914cfca5ef7db74f31adbd6c9d88a6f4e21ebd8e057d9474c", + "sha256:c4cd94dfefbefec3f8b544f61286584292d740e6e9d4677769bc76b8f41deb02", + "sha256:c4f982715e65036c34897eb598d64aef15150c447be2cfc6643ec7a11af06574", + "sha256:c8458becc562ee35b30b5e53173933414cf42e56b3f4f3d80997bf0dda7308d1", + "sha256:ca195cd9d1d84b3498532968237774a6e06e2a4afe706b87172f1d033b95e230", + "sha256:ccc68ee27362f8d3516deecffa124d1488ae20347628e357264e7e66dbdaba08", + "sha256:d3d59479b98cc364b8a08ddd854c7817b5c578a521b56af5a96b3a9db18cc9b7", + "sha256:d8f699a709120b220dfe173f79c73cb2a2cab2c0b88dd59d7b49407d032b8ebd", + "sha256:dc2af0135139bbb26b1ea5bdc430e049edb745ae643cb898afb32549ce4801de", + "sha256:dc7ce867d277aa74555c67b93ef2a6f78bd7bd73e6c2bbafeb96f8bccd05b9d9", + "sha256:dd94ce596bda40a9618324547cfaaf6650b1a24f5390350142499aa4e34e53d1", + "sha256:de844aaa4815b78f6023832590d77da0e3b6805c644c33ce94a1e449f16d6ab5", + "sha256:e5a64ac6016839fd906b3d7cc1f7ecb145c7d44a310234b6843f3b23b8ec0651", + "sha256:e5f0642cdf4636198a4990de7a71b693d824c56a757862230454629cf62e323d", + "sha256:e74dc488a27b90f31ab307b4cf3f07a45bb78a0e91cfb36d69c6eced4f36089b", + "sha256:e82b8e0b88d493d4e882f18de30f679bf1197c82d8c799acb5fdb4068cadb945", + "sha256:f07fa2f15dabe91259828ce7d71b5ca9e2eb7c8c26baa822c825ce43552f4883", + "sha256:f15976718c004466406342789f31b6673776360f3b1e3c575f25302d7e789575", + "sha256:f358ea9e47eb3c2d6eba121ab512dfff38a88db719c38d1e67349af210bc7529", + "sha256:f38fabd7b8d14fb7d63fbb2d07971d6edd518d2a43542c63c29164c901d2a758", + "sha256:f412923d4ce1ec29aa3cf7752598e5eb154f549cfbf62d7c6f3cc76cb25b32e0", + "sha256:f4e07df8476545da7cf23f75811f4fc334b06fc50d8e945e897cfc00c8f89690", + "sha256:f505264735ee074250a9c78247ee8618292091d9d1fcc023290e9ac67e8f1afa", + "sha256:f5ebbf9fbdabed208d4ecd2e1dfd2c0741af2f876e7ae522c2537d404ca895c3", + "sha256:f6b26e6c3b98adb648243670fddc8cab6ae17473f9dc58c51574af3e64d61211", + "sha256:f73e4fcf7455d3b734e6ecbafdbc12d3c1dd8f2146fd186e003ae1c8f00e5eed", + "sha256:f8e93a01a35be08d31ae33021e5268f157a2d60ebd643cfc15de6ab8e4722eb1", + "sha256:fe76d75b345dc045acdbc006adcb197cc680754afd6c259de60d358d60c93736", + "sha256:fea6d6939d9bf098d96c6d22bb3e4ff39f8eb3f0f26b52c8c69ba06845490095", + "sha256:ffbd6faeb190aaf2b5e9024bac9622d2ee549b7ec89ef3a9373fa35313d44e0e" + ], + "markers": "python_version < '3.11'", + "version": "==2.1.0" + }, + "openpyxl": { + "hashes": [ + "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", + "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==3.1.5" + }, + "paho-mqtt": { + "hashes": [ + "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", + "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.7'", + "version": "==2.1.0" + }, + "pandas": { + "hashes": [ + "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863", + "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2", + "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", + "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", + "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", + "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", + "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51", + "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", + "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08", + "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", + "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4", + "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921", + "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", + "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", + "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0", + "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", + "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99", + "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", + "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd", + "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce", + "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57", + "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", + "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", + "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a", + "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238", + "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", + "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772", + "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", + "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.9'", + "version": "==2.2.2" + }, + "pillow": { + "hashes": [ + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==10.4.0" + }, + "psutil": { + "hashes": [ + "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35", + "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0", + "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c", + "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1", + "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3", + "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c", + "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd", + "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3", + "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0", + "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2", + "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6", + "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d", + "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c", + "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0", + "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132", + "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14", + "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==6.0.0" + }, + "pydantic": { + "hashes": [ + "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a", + "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8" + ], + "markers": "python_version >= '3.8'", + "version": "==2.8.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d", + "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f", + "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686", + "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482", + "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006", + "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83", + "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6", + "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88", + "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86", + "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a", + "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6", + "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a", + "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6", + "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6", + "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43", + "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c", + "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4", + "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e", + "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203", + "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd", + "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1", + "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24", + "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc", + "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc", + "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3", + "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598", + "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98", + "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331", + "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2", + "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a", + "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6", + "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688", + "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91", + "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa", + "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b", + "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0", + "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840", + "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c", + "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd", + "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3", + "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231", + "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1", + "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953", + "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250", + "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a", + "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2", + "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20", + "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434", + "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab", + "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703", + "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a", + "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2", + "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac", + "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611", + "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121", + "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e", + "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b", + "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09", + "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906", + "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9", + "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7", + "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b", + "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987", + "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c", + "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b", + "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e", + "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237", + "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1", + "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19", + "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b", + "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad", + "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0", + "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94", + "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312", + "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f", + "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669", + "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1", + "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe", + "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99", + "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a", + "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a", + "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52", + "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c", + "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad", + "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1", + "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a", + "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f", + "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a", + "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27" + ], + "markers": "python_version >= '3.8'", + "version": "==2.20.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "python-multipart": { + "hashes": [ + "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026", + "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==0.0.9" + }, + "pytz": { + "hashes": [ + "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", + "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + ], + "version": "==2024.1" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sniffio": { + "hashes": [ + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "starlette": { + "hashes": [ + "sha256:526f53a77f0e43b85f583438aee1a940fd84f8fd610353e8b0c1a77ad8a87e76", + "sha256:53a7439060304a208fea17ed407e998f46da5e5d9b1addfea3040094512a6379" + ], + "markers": "python_version >= '3.8'", + "version": "==0.38.4" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version < '3.11'", + "version": "==4.12.2" + }, + "tzdata": { + "hashes": [ + "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", + "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + ], + "markers": "python_version >= '2'", + "version": "==2024.1" + }, + "urllib3": { + "hashes": [ + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.2" + }, + "uvicorn": { + "hashes": [ + "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", + "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '3.8'", + "version": "==0.30.6" + }, + "xlrd": { + "hashes": [ + "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", + "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88" + ], + "index": "pip_conf_index_global", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.0.1" + } + }, + "develop": {} +} diff --git a/SCLP/app.py b/SCLP/app.py new file mode 100644 index 0000000..c38b381 --- /dev/null +++ b/SCLP/app.py @@ -0,0 +1,99 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : api后端服务程序启动入口 +""" +import argparse + +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +import uvicorn + +from config import VERSION, PREFIX_PATH +from utils import logger, database, misc +from models import sessions, houses, devices, products, householders, subsystem, parkinglots +from routers import (login, houses_manage, devices_manage, space_manage, householder_manage, + access_devices_0, access_devices_1, + parkinglot_0, parkinglot_1) + + +class MainServer: + def __init__(self, app_args): + self.port = app_args.port + self.app = None + self.prepare() + self.server_main() + + def prepare(self): + """准备处理,一些适配和配置加载工作""" + # 数据库初始化 + logger.Logger.init("数据库执行初始化校验 ...") + th = database.get_table_handler() + sessions.SessionsTable.check(th) + houses.HousesTable.check(th) + products.ProductsTable.check(th) + devices.DevicesTable.check(th) + subsystem.SubsystemTable.check(th) + householders.HouseholdersTable.check(th) + parkinglots.ParkinglotsTable.check(th) + logger.Logger.init("数据库完成初始化校验 ✅") + + self.app = FastAPI( + title="Smart Community Local Platform", + description="智慧社区本地化平台后端服务", + version=VERSION, + docs_url=f"{PREFIX_PATH}/docs" # 设为None则会关闭 + ) + + # 注册路由模块中的路由 + self.app.include_router(login.router, prefix=PREFIX_PATH, tags=["登录界面"]) + self.app.include_router(houses_manage.router, prefix=PREFIX_PATH, tags=["内置信息"]) + self.app.include_router(householder_manage.router, prefix=PREFIX_PATH, tags=["住户管理界面"]) + self.app.include_router(access_devices_0.router, prefix=PREFIX_PATH, tags=["通行设备界面"]) + self.app.include_router(access_devices_1.router, prefix=PREFIX_PATH, tags=["通行授权界面"]) + self.app.include_router(parkinglot_0.router, prefix=PREFIX_PATH, tags=["车场管理界面"]) + self.app.include_router(parkinglot_1.router, prefix=PREFIX_PATH, tags=["车场授权界面"]) + self.app.include_router(devices_manage.router, prefix=PREFIX_PATH, tags=["设备管理界面"]) + self.app.include_router(space_manage.router, prefix=PREFIX_PATH, tags=["通行空间映射界面"]) + + @self.app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError): + logger.Logger.error(f"{request.url.path} 请求参数错误 {exc.errors()} - {request.client.host}") + return JSONResponse( + status_code=422, + content={"status": False, + "message": f"请求参数错误,请检查输入" + f"{',错误类型:' + exc.errors()[0]['type'] if len(exc.errors()) > 0 else ''}"} + ) + + @self.app.exception_handler(misc.InvalidException) + async def invalid_target_exception_handler(request: Request, exc: misc.InvalidException): + logger.Logger.error(f"{request.url.path} {exc.message} - {request.client.host}") + return JSONResponse( + status_code=400, + content={"status": False, "message": f"{exc.message}"} + ) + + def server_main(self): + """主服务程序""" + logger.Logger.debug(f"当前已开启 DEBUG 模式") + uvicorn.run(self.app, host="0.0.0.0", port=self.port, access_log=False) + + +def main(port: str = 9680): + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument( + "-p", + "--port", + type=int, + default=port, + help=f"Port of server, default {logger.new_dc(port)}", + ) + app_args = parser.parse_args() + MainServer(app_args) + + +if __name__ == "__main__": + main() diff --git a/SCLP/auto_callback.py b/SCLP/auto_callback.py new file mode 100644 index 0000000..981f5bb --- /dev/null +++ b/SCLP/auto_callback.py @@ -0,0 +1,51 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 用于自动回复mqtt,用于测试模拟设备下发请求成功 +""" +import json +import time + +import paho.mqtt.client as mqtt +from utils import logger +from config import BROKER_HOST, BROKER_PORT, BROKER_USERNAME, BROKER_PASSWD + + +def on_connect(client, userdata, flags, rc): + logger.Logger.debug(f"\033[1;32mAUTO CALLBACK\033[0m 🔗 Mqtt connection! {{rc: {rc}}} 🔗") + client.subscribe("/jmlink/+/tml/service/call") + + +def on_disconnect(client, userdata, rc): + logger.Logger.debug(f"\033[1;31mAUTO CALLBACK\033[0m 🔌 Break mqtt connection! {{rc: {rc}}} 🔌") + + +def on_message(client, userdata, message): + logger.Logger.debug("\033[1;36mAUTO CALLBACK\033[0m <- 💡 接收到新数据,执行 on_message 中 ...") + msg = json.loads(message.payload.decode().replace("\x00", "")) + print(f"msg: {msg}") + if "messageId" in msg.keys(): + topic = f"{message.topic}_resp" + print(topic) + auto_callback = { + "messageId": msg["messageId"], + "code": 0, + "data": { + "code": 0 + } + } + client.publish(topic, json.dumps(auto_callback)) + print(auto_callback) + + +client = mqtt.Client(client_id="auto_callback@python") +client.on_connect = on_connect +client.on_disconnect = on_disconnect +client.on_message = on_message +client.username_pw_set(BROKER_USERNAME, BROKER_PASSWD) +client.connect(BROKER_HOST, BROKER_PORT) +client.loop_start() + +while True: + time.sleep(86400) diff --git a/SCLP/backend.py b/SCLP/backend.py new file mode 100644 index 0000000..cfcbc40 --- /dev/null +++ b/SCLP/backend.py @@ -0,0 +1,38 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : mqtt后端服务程序启动入口 +""" +import time + +from config import BROKER_HOST, BROKER_PORT, BROKER_USERNAME, BROKER_PASSWD +from utils.misc import create_mqtt_client, UserData, on_connect, on_disconnect, on_publish +from device.services import on_message + + +def main_mqtt(): + client_dict = {} + userdata = UserData() + userdata.set_topics([ + "/jmlink/+/comm/register", + "/jmlink/+/comm/sub/register", + "/jmlink/+/comm/online", + "/jmlink/+/comm/offline", + "/jmlink/+/comm/post", + "/jmlink/+/tml/event/post", + "/jmlink/+/tml/property/post" + ]) + userdata.set_clients(client_dict) + client = create_mqtt_client(BROKER_HOST, BROKER_PORT, + userdata, on_message, on_publish, on_connect, on_disconnect, + "backend@python", username=BROKER_USERNAME, password=BROKER_PASSWD) + client.loop_start() + client_dict["center"] = [client, userdata] + + while True: + time.sleep(86400) + + +if __name__ == "__main__": + main_mqtt() diff --git a/SCLP/config.py b/SCLP/config.py new file mode 100644 index 0000000..760cec5 --- /dev/null +++ b/SCLP/config.py @@ -0,0 +1,50 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 根据实际情况动态变化的配置 +""" +import os +from utils import logger +from utils.misc import get_ip_address + +VERSION = "0.0.0.1" +PREFIX_PATH = "/api/v1" +DB_PATH = "./data/SCLP.db" +FACES_DIR_PATH = "data/FaceImages" +SUB_PATH = "faces" + +ENV_FLAG = os.getenv("ENV_FLAG", "DEPLOY") +if_name = "WLAN" if os.name != "posix" else "enp1s0" if ENV_FLAG != "WJ14P" else "wlp2s0" +LAN_IP, _ = get_ip_address(if_name) +if ENV_FLAG == "DEPLOY": + if "10.59.246" in LAN_IP: + ENV_FLAG = "LAB" + if "192.168.1" in LAN_IP: + ENV_FLAG = "X13" +SLEEP_TIME = 0.03 # mqtt publish后的循环等待返回时间,30ms检测一次 +TIMEOUT_SECOND = 30 # 等待超时时间,3s + +LOGGER_DEBUG = os.environ.get("LOGGER_DEBUG", "True") +logger.DEBUG = False if LOGGER_DEBUG.lower() == "false" else True +logger.LOGGER_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "run.log") + +LOCAL_IP = "127.0.0.1" +ENV_CONFIG = { + "B760M": {"HOST": "b760m.langzihan.top", "PORT": 9680, "BROKER_HOST": LOCAL_IP, "BROKER_PORT": 1883}, + "M5": {"HOST": "sclp.langzihan.top", "PORT": 80, "BROKER_HOST": LOCAL_IP, "BROKER_PORT": 1883}, + "X13": {"HOST": LOCAL_IP, "PORT": 9680, "BROKER_HOST": "intranet.b760m.langzihan.top", "BROKER_PORT": 1883}, + "WJ14P": {"HOST": LOCAL_IP, "PORT": 9680, "BROKER_HOST": "intranet.b760m.langzihan.top", "BROKER_PORT": 1883}, + "LAB": {"HOST": "10.59.246.170", "PORT": 9680, "BROKER_HOST": "10.59.246.161", "BROKER_PORT": 31883}, + "DEPLOY": {"HOST": "10.59.246.170", "PORT": 9680, "BROKER_HOST": "10.59.246.161", "BROKER_PORT": 31883}, +} +APP_HOST = ENV_CONFIG[ENV_FLAG]["HOST"] +APP_PORT = ENV_CONFIG[ENV_FLAG]["PORT"] +IMAGE_SERVER_PORT = APP_PORT + 1 + +BROKER_HOST = ENV_CONFIG[ENV_FLAG]["BROKER_HOST"] +BROKER_PORT = ENV_CONFIG[ENV_FLAG]["BROKER_PORT"] +BROKER_USERNAME = "test@sclp" +BROKER_PASSWD = "test@sclp" + +AUTO_CALLBACK = False if ENV_FLAG == "LAB" else True diff --git a/SCLP/data/mvboli.ttf b/SCLP/data/mvboli.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b81d80c7f36fd4a9790b98b8c7a36594c12f1fc2 GIT binary patch literal 78272 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¥Mx>so3#qIfY1Rfu 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

5^uXgF(ND}jJr2p%>gK9RVBM;R1-ofg zEt9-;ZH>N+o&rc;tR>oNhHPcXwE-=0js{CP;|x&+3#Oy7#hh^wXapMUY+yCt!BDaE z9rwe+C)R&jGR@#U^FiCVkcWh8BoMo|G4UCNZb?aDry$2n7OQ9(a;Y>tL}NnP+zhWbUwdW#Psbkkv0ua|H~PGFIPUu<$Bxf-x~{%;{zu8q4WaAS7fMov zKk$~k7jz<8cj?+G&l}~Bbmb)pP77W*y?w`Ldp`VFy?Ag=Sz`1=mtpqqpIpCv;!d{K zuirC#NnKO(mgm||WUx6;65*j~^U{@twZdb23xB@&z-&{qdS=d+$)9C}*F1KjqIm4W z6XvS;bqgAN>)!gVqN?xZa_+`OX`CsXw>i7pgH%T|z^CQ0ex`JEp?X}glf4=kjjb+~m z=X_n?XXg#yReHev7cR6nR;^5Z`{B}ogO1J_7P4uoJUV##_k9Js)BkwtwZ|)tZ(97& zu!*Gub90VmzWmVSa>1P32{)%bdi-!>zb&|! zMUFeCgtr%7I_IAB0rT-M(D11!QIFmZ3o)>a5zyjELKr0h-ShUb8chB@H_}mHrQY;p z&R3nLt?x`(pOZ7`c>oFT`+1PBy4NkDs8U}^XZV~@@bW?47=QL+DuIE92HiL~UkQ74 ztZAE^bL@XRvP-75G>>boY^j?T*?S9@YD z(}sM`w2DD5)+f=9DChb7qQi?GrH%>o1n^m6rdvnLul-J%{e$R#rF=Nta+}IHE;u>qJtWr2V`}d9`UtQf8Ik{uJ zu~F0%vErEr$K*cS5qfISg>t#fG40fsuWj!7+%@n2Uo+1B^}<`PK3U<@-L5qi1(^#13y1{t$iM-g9VNw!WQo4Gs6<~< zY|MzedgP}cJ#pryOg{bI+=k#U&H4J|w3W)dw;i_7_|PE&D}5ck9|Xa zi8FG(clNoRO*2Ni($A(pTplyqHS_K1-kfE`!dK~YTUVXB{6dM%HfYUvxq~IEcRcw0 zr6;$%{8*b}P{X00YYw%)fBB`$bo!M=Tas1xwk}Nl?ef)_bH6>KnY?z!xKVr8ZAe_G zKK1edcY4)B?bTP}Ltnl0sl9R04#SY&d7HlpFZguc=T}RfntAm|YF^IDXV1*t7JY-o F`aidzD;59% literal 0 HcmV?d00001 diff --git a/SCLP/device/call.py b/SCLP/device/call.py new file mode 100644 index 0000000..c90bd03 --- /dev/null +++ b/SCLP/device/call.py @@ -0,0 +1,323 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 由服务主动调用的一些方法,实现调用后自动连接mqtt进行任务下发和回馈处理,解耦服务端与mqtt +""" +import json +import time +import traceback +from typing import Optional + +from pydantic import BaseModel, field_validator + +from config import (BROKER_HOST, BROKER_PORT, BROKER_USERNAME, BROKER_PASSWD, PREFIX_PATH, SUB_PATH, + SLEEP_TIME, TIMEOUT_SECOND) +from config import APP_HOST as HOST_IP +from config import IMAGE_SERVER_PORT as PORT +from utils import logger +from utils.misc import generate_captcha_text, UserData, create_mqtt_client, on_publish, on_disconnect, now_tz_datetime + + +class CallMessage: + def __init__(self, service_code, params): + self.messageId = generate_captcha_text(16) + self.version = "1.0" + self.time = int(time.time() * 1000) + self.fromUid = "system" + self.serviceCode = service_code + self.params = params + + def dict(self): + _dict = self.__dict__.copy() + for key, value in self.__dict__.items(): + if value is None: + _dict.pop(key) + _dict["from"] = "backend" + return _dict + + def __call__(self, *args, **kwargs): + return json.dumps(self.dict()) + + +class AddFaceItem(BaseModel): + name: str + user_id: str | int + device_ids: str + start_date: str = now_tz_datetime() + phone_number: str = "" + face_url: str + expire_date: Optional[str] = "2099-01-01T01:01:01.001Z" + + @field_validator("user_id") + def check_user_id(cls, value): + if isinstance(value, int): + return str(value) + return value + + +class DelFaceItem(BaseModel): + user_id: str | int + device_ids: str + + @field_validator("user_id") + def check_user_id(cls, value): + if isinstance(value, int): + return str(value) + return value + + +ACTION = { + "insert": "新增", + "update": "更新", + "delete": "删除" +} + + +class VehicleRegistrationItem(BaseModel): + car_plate_no: str # 车牌 + registration_id: str + owner_id: str + owner_name: str + registration_time: str + begin_time: str + end_time: str = "2099-01-01T01:01:01.001Z" + action: str # insert, update, delete + registration_type: str # 0: 业主,1: 访客 + + +class UpdateFixedCardItem(BaseModel): + vehicle_owner_id: str + vehicle_owner_name: str + vehicle_owner_phone: str + plate_number: str # 车牌 + effective_date_begin: str + effective_date_end: str + action: str # insert, update, delete + card_id: str # 在新增时进行生成 + + +def on_connect(client, userdata, _flags, rc): + logger.Logger.debug(f"🔗 Mqtt connection! {{rc: {rc}}} 🔗") + if userdata.topics: + _topics = [(topic, 0) for topic in userdata.topics] + client.subscribe(_topics) + logger.Logger.debug(f"subscribe topics: {userdata.topics}") + + +def on_message(_client, userdata: UserData, message): + try: + logger.Logger.debug("💡 接收到新数据,执行 on_message 中 ...", log_path=None) + msg = json.loads(message.payload.decode().replace("\x00", "")) + logger.Logger.info(f"{message.topic}: {msg}") + userdata.set_topic(message.topic) + if userdata.token: + if "messageId" in msg.keys() and userdata.token == msg["messageId"]: # 兼容aiot平台 + userdata.set_status_add("status", True) + userdata.set_status_add("response", msg) + except Exception as e: + logger.Logger.error(f"{type(e).__name__}, {e}") + if logger.DEBUG: + traceback.print_exc() + + +class ServicesCall: + def __init__(self): + # 创建mqtt连接 + self.userdata = UserData() + self.client = create_mqtt_client(BROKER_HOST, BROKER_PORT, + self.userdata, on_message, on_publish, on_connect, on_disconnect, + username=BROKER_USERNAME, password=BROKER_PASSWD) + self.client.loop_start() + + def __del__(self): + self.client.loop_stop() + + def add_face(self, device_id, obj: AddFaceItem) -> tuple[bool, int, str]: + if obj.face_url.startswith("http"): + face_filename = obj.face_url.split('/')[-1] + else: + face_filename = obj.face_url + obj.face_url = f"http://{HOST_IP}:{PORT}{PREFIX_PATH}/{SUB_PATH}/{face_filename}" + topic = f"/jmlink/{device_id}/tml/service/call" + self.userdata.set_topic(topic) + logger.Logger.debug(f"{'=' * 10} 🚩 AddFace 🚩 {'=' * 10}") + logger.Logger.debug(f"{obj.device_ids} 添加用户 {obj.user_id} 的人脸授权") + cm = CallMessage("add_face", obj.__dict__) + self.userdata.set_message(cm.dict()) + # 等待回传 + self.userdata.set_token(cm.messageId) + self.userdata.set_status_add("status", False) + self.userdata.set_status_add("start_timestamp", time.time()) + topic_resp = f"/jmlink/{device_id}/tml/service/call_resp" + self.client.subscribe(topic_resp) + self.client.publish(topic, cm()) + try: + while True: + if self.userdata.status["status"]: + if "code" in self.userdata.status["response"].keys(): + if self.userdata.status["response"]["code"] != 0: + error_code = self.userdata.status['response']['code'] + logger.Logger.error( + f"用户 - {obj.user_id} 在设备 - {device_id} 上人脸下置失败,错误码 {error_code}") + return False, error_code, self.userdata.status['response']['message'] + else: + logger.Logger.info(f"设备 - {obj.device_ids} 用户({obj.user_id}) 人脸完成授权") + return True, 0, "" + if time.time() - self.userdata.status["start_timestamp"] > TIMEOUT_SECOND: # 超时跳出 + logger.Logger.error("等待回复超时") + return False, -1, "等待回复超时" + time.sleep(SLEEP_TIME) + finally: + self.client.unsubscribe(topic_resp) + logger.Logger.debug(f"移除订阅: {topic_resp}") + self.userdata.set_token(None) + self.userdata.set_status_remove("response") + logger.Logger.debug(f"{'=' * 10} 🏁 AddFace 🏁 {'=' * 10}") + + def del_face(self, device_id, obj: DelFaceItem) -> tuple[bool, int, str]: + topic = f"/jmlink/{device_id}/tml/service/call" + self.userdata.set_topic(topic) + logger.Logger.debug(f"{'=' * 10} 🚩 DelFace 🚩 {'=' * 10}") + logger.Logger.debug(f"从 {obj.device_ids} 移除用户 {obj.user_id} 的人脸授权") + cm = CallMessage("del_face", obj.__dict__) + self.userdata.set_message(cm.dict()) + # 等待回传 + self.userdata.set_token(cm.messageId) + self.userdata.set_status_add("status", False) + self.userdata.set_status_add("start_timestamp", time.time()) + topic_resp = f"/jmlink/{device_id}/tml/service/call_resp" + self.client.subscribe(topic_resp) + self.client.publish(topic, cm()) + try: + while True: + if self.userdata.status["status"]: + if "code" in self.userdata.status["response"].keys(): + if self.userdata.status["response"]["code"] != 0: + error_code = self.userdata.status['response']['code'] + logger.Logger.error( + f"用户 - {obj.user_id} 在设备 - {device_id} 上人脸移除失败,错误码 {error_code}") + return False, error_code, self.userdata.status['response']['message'] + else: + logger.Logger.info(f"设备 - {obj.device_ids} 用户({obj.user_id}) 人脸完成移除") + return True, 0, "" + if time.time() - self.userdata.status["start_timestamp"] > TIMEOUT_SECOND: # 超时跳出 + logger.Logger.error("等待回复超时") + return False, -1, "等待回复超时" + time.sleep(SLEEP_TIME) + finally: + self.client.unsubscribe(topic_resp) + logger.Logger.debug(f"移除订阅: {topic_resp}") + self.userdata.set_token(None) + self.userdata.set_status_remove("response") + logger.Logger.debug(f"{'=' * 10} 🏁 DelFace 🏁 {'=' * 10}") + + def vehicle_registration(self, device_id, obj: VehicleRegistrationItem) -> tuple[bool, int, str]: + topic = f"/jmlink/{device_id}/tml/service/call" + self.userdata.set_topic(topic) + logger.Logger.debug(f"{'=' * 10} 🚩 VehicleRegistration 🚩 {'=' * 10}") + logger.Logger.debug(f"{device_id} {ACTION[obj.action]}车辆 {obj.car_plate_no} 信息") + cm = CallMessage("vehicle_registration", obj.__dict__) + self.userdata.set_message(cm.dict()) + # 等待回传 + self.userdata.set_token(cm.messageId) + self.userdata.set_status_add("status", False) + self.userdata.set_status_add("start_timestamp", time.time()) + topic_resp = f"/jmlink/{device_id}/tml/service/call_resp" + self.client.subscribe(topic_resp) + self.client.publish(topic, cm()) + try: + while True: + if self.userdata.status["status"]: + if "code" in self.userdata.status["response"].keys(): + if self.userdata.status["response"]["code"] != 0: + error_code = self.userdata.status['response']['code'] + logger.Logger.error(f"{topic_resp} 返回错误码: {error_code}") + return False, error_code, self.userdata.status['response']['message'] + else: + logger.Logger.info(f"车辆({obj.car_plate_no}) 信息完成{ACTION[obj.action]}") + return True, 0, "" + if time.time() - self.userdata.status["start_timestamp"] > TIMEOUT_SECOND: # 超时跳出 + logger.Logger.error("等待回复超时") + return False, -1, "等待回复超时" + time.sleep(SLEEP_TIME) + finally: + self.client.unsubscribe(topic_resp) + logger.Logger.debug(f"移除订阅: {topic_resp}") + self.userdata.set_token(None) + self.userdata.set_status_remove("response") + logger.Logger.debug(f"{'=' * 10} 🏁 VehicleRegistration 🏁 {'=' * 10}") + + def update_fixed_card(self, device_id, obj: UpdateFixedCardItem) -> tuple[bool, int, str]: + logger.Logger.debug(f"{'=' * 10} 🚩 UpdateFixedCard 🚩 {'=' * 10}") + topic = f"/jmlink/{device_id}/tml/service/call" + self.userdata.set_topic(topic) + logger.Logger.debug(f"{device_id} {ACTION[obj.action]}用户 {obj.plate_number} 的月卡信息") + cm = CallMessage("update_fixed_card", obj.__dict__) + self.userdata.set_message(cm.dict()) + # 等待回传 + self.userdata.set_token(cm.messageId) + self.userdata.set_status_add("status", False) + self.userdata.set_status_add("start_timestamp", time.time()) + topic_resp = f"/jmlink/{device_id}/tml/service/call_resp" + self.client.subscribe(topic_resp) + self.client.publish(topic, cm()) + try: + while True: + if self.userdata.status["status"]: + if "code" in self.userdata.status["response"].keys(): + if self.userdata.status["response"]["code"] != 0: + error_code = self.userdata.status['response']['code'] + logger.Logger.error(f"{topic_resp} 返回错误码: {error_code}") + return False, error_code, self.userdata.status['response']['message'] + else: + logger.Logger.info(f"车辆({obj.plate_number}) 月卡信息完成修改") + return True, 0, "" + if time.time() - self.userdata.status["start_timestamp"] > TIMEOUT_SECOND: # 超时跳出 + logger.Logger.error("等待回复超时") + return False, -1, "等待回复超时" + time.sleep(SLEEP_TIME) + finally: + self.client.unsubscribe(topic_resp) + logger.Logger.debug(f"移除订阅: {topic_resp}") + self.userdata.set_token(None) + self.userdata.set_status_remove("response") + logger.Logger.debug(f"{'=' * 10} 🏁 UpdateFixedCard 🏁 {'=' * 10}") + + +if __name__ == '__main__': + sc = ServicesCall() + sc.del_face(2, DelFaceItem( + user_id="1234", + device_ids=str([2]) + )) + sc.add_face(2, AddFaceItem( + name="测试", + user_id="ceshi", + face_url="http://ip:port/api/v1/faces/person_demo.jpg", + device_ids=str([2, 321]), + start_date="2024-01-01T18:00:00.000Z" + )) + + sc.vehicle_registration(2, VehicleRegistrationItem( + car_plate_no="鲁G95339", + registration_id="5a12265cf73f261148770c12328bb195", + owner_id="1244668310399733760", + owner_name="测试业主", + registration_time="2024-05-27T16:12:26.895Z", + begin_time="2024-05-27T16:12:26.895Z", + end_time="2034-05-27T16:12:26.895Z", + action="insert", + registration_type='0' + )) + + sc.update_fixed_card(2, UpdateFixedCardItem( + vehicle_owner_id="1244668310399733760", + vehicle_owner_name="测试业主", + vehicle_owner_phone="1244668310399733760", + action="insert", + card_id=str(int(time.time() * 1000)), + effective_date_begin=now_tz_datetime(), + effective_date_end=now_tz_datetime(365 * 10), + plate_number="鲁G95339" + )) diff --git a/SCLP/device/message.py b/SCLP/device/message.py new file mode 100644 index 0000000..163e47e --- /dev/null +++ b/SCLP/device/message.py @@ -0,0 +1,100 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : None +""" +import json +import time + + +class BaseResp: + def __init__(self, message_id, data): + self.message_id = message_id + self.time = int(time.time() * 1000) + self.data = data + + def dict(self): + if not isinstance(self.data, list): + if isinstance(self.data, dict): + data = self.data + else: + data = self.data() + else: + data = [i if isinstance(i, dict) else i() for i in self.data] + return { + "messageId": self.message_id, + "time": self.time, + "data": data + } + + def __call__(self): + return json.dumps(self.dict()) + + +class SimpleResp: + def __init__(self, message_id, code): + self.message_id = message_id + self.time = int(time.time() * 1000) + self.code = code + + def dict(self): + return { + "messageId": self.message_id, + "time": self.time, + "code": self.code + } + + def __call__(self, *args, **kwargs): + return json.dumps(self.dict()) + + +class ErrorResp: + def __init__(self, message_id, code): + self.message_id = message_id + self.time = int(time.time() * 1000) + self.code = code + + def dict(self): + return { + "messageId": self.message_id, + "time": self.time, + "data": {"code": self.code} + } + + def __call__(self, *args, **kwargs): + return json.dumps(self.dict()) + + +class DRRespItem: + """设备注册返回数据体""" + + def __init__(self, device_id: str, device_name: str, device_secret: str, code: int): + self.device_id = device_id + self.device_name = device_name + self.device_secret = device_secret + self.code = code + + def __call__(self, *args, **kwargs): + return { + "deviceId": self.device_id, + "deviceName": self.device_name, + "deviceSecret": self.device_secret, + "code": self.code + } + + +class SDRRespItem: + """设备注册返回数据体""" + + def __init__(self, device_id: str, device_name: str, code: int): + self.device_id = device_id + self.device_name = device_name + self.code = code + + def __call__(self, *args, **kwargs): + return { + "deviceId": self.device_id, + "deviceName": self.device_name, + "code": self.code + } diff --git a/SCLP/device/services.py b/SCLP/device/services.py new file mode 100644 index 0000000..29eec53 --- /dev/null +++ b/SCLP/device/services.py @@ -0,0 +1,520 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : None +""" +import json +import traceback +import time + +from config import DB_PATH +from device.message import ErrorResp, DRRespItem, BaseResp, SDRRespItem, SimpleResp +from models.devices import DevicesTable, DeviceRegister +from models.products import ProductsTable +from utils.misc import generate_captcha_text, now_datetime_second +from utils import logger +from utils.database import SQLiteDatabaseEngine, BaseTable +from utils.misc import UserData + + +def on_message(_client, userdata: UserData, message): + try: + logger.Logger.debug("💡 接收到新数据,执行 on_message 中 ...", log_path=None) + msg = json.loads(message.payload.decode().replace("\x00", "")) + logger.Logger.info(f"{message.topic}: {msg}") + userdata.set_topic(message.topic) + if userdata.table_handler is None: + _db = SQLiteDatabaseEngine(db_path=DB_PATH) + userdata.set_table_handler(BaseTable(_db.connection, _db.cursor)) + if userdata.token: + if "messageId" in msg.keys() and userdata.token == msg["messageId"]: # 兼容aiot平台 + userdata.set_status_add("status", True) + userdata.set_status_add("response", msg) + else: + auto_service(msg, userdata) + except Exception as e: + logger.Logger.error(f"{type(e).__name__}, {e}") + if logger.DEBUG: + traceback.print_exc() + + +class BaseService: + def handle(self, **kwargs): + """must be `override`""" + pass + + +class Services: + """业务逻辑入口""" + + class DeviceRegisterService(BaseService): + """直连设备、网关设备注册""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("设备注册请求已接收,正在执行注册 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + # 获取产品信息 + params = msg["params"] + product = ProductsTable.get_product_info(userdata.table_handler, params["productId"]) + if product is None: + logger.Logger.error("DeviceRegisterService -> 产品ID不存在") + message = ErrorResp(message_id, 601) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if product.node_type not in ["网关设备", "直连设备"]: + logger.Logger.error("DeviceRegisterService -> 节点类型错误") + message = ErrorResp(message_id, 503) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + sct = generate_captcha_text(32) + dr = DeviceRegister( + device_mac=params["deviceName"], + device_name=params["displayName"] if params["displayName"] else "", + device_desc=params["desc"] if params["desc"] else "", + third_local_device_id=params["thirdLocalDeviceId"] if params["thirdLocalDeviceId"] else "", + device_sct=sct, + gateway_id=None, + gateway_name=None, + product_name=product.product_name, + node_type=product.node_type + ) + device_id = DevicesTable.insert_by_device_register(userdata.table_handler, dr) + resp = BaseResp(message_id, DRRespItem( + str(device_id), + params["displayName"], + sct, + 0 + )) + userdata.set_message(resp.dict()) + client.publish(topic, resp()) + except (KeyError, TypeError): + logger.Logger.error("DeviceRegisterService -> 关键参数缺失") + message = ErrorResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class SubDeviceRegisterService(BaseService): + """子设备注册""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("子设备注册请求已接收,正在执行注册 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + gateway_id = int(userdata.topic.split("/")[2]) + gateway_info = DevicesTable.get_device_info(userdata.table_handler, gateway_id) + if gateway_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.error("SubDeviceRegisterService -> 并无对应上级设备") + message = ErrorResp(message_id, 601) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if gateway_info.node_type != "网关设备": # 确保当前主题是网关设备 + logger.Logger.error("SubDeviceRegisterService -> 节点类型错误") + message = ErrorResp(message_id, 503) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + data = [] + for params in msg["params"]: + product_info = ProductsTable.get_product_info(userdata.table_handler, params["productId"]) + if product_info is None: + logger.Logger.error("SubDeviceRegisterService -> 产品ID不存在") + data.append({"code": 601}) + continue + if product_info.node_type != "网关子设备": # 确保注册的产品是网关子设备 + logger.Logger.error("SubDeviceRegisterService -> 产品ID节点类型错误") + data.append({"code": 503}) + continue + dr = DeviceRegister( + device_mac=params["deviceName"], + device_name=params["displayName"] if params["displayName"] else "", + device_desc=params["desc"] if params["desc"] else "", + third_local_device_id=params["thirdLocalDeviceId"] if params["thirdLocalDeviceId"] else "", + device_sct=None, + gateway_id=gateway_id, + gateway_name=gateway_info.device_name, + product_name=product_info.product_name, + node_type=product_info.node_type + ) + device_id = DevicesTable.insert_by_device_register(userdata.table_handler, dr) + data.append(SDRRespItem( + str(device_id), + params["displayName"] if params["displayName"] else "", + 0 + )) + message = BaseResp(message_id, data) + userdata.set_message(message.dict()) + client.publish(topic, message()) + except (KeyError, TypeError) as e: + logger.Logger.error("DeviceRegisterService -> 关键参数缺失") + logger.Logger.error(f"{type(e).__name__}, {e}") + if logger.DEBUG: + traceback.print_exc() + message = ErrorResp(message_id, 401) + userdata.set_message(message) + client.publish(topic, + {"messageId": message_id, "time": int(time.time() * 1000), "data": [{"code": 401}]}) + + class UpdateDeviceStatusService(BaseService): + """设备在线和离线状态的变更""" + + def handle(self, msg: dict, userdata: UserData): + message_type = userdata.topic.split("/")[-1] + if message_type == "online": + device_status = "在线" + else: + device_status = "离线" + logger.Logger.debug(f"设备状态更新请求已接收,正在执行{device_status} ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + device_id = int(userdata.topic.split("/")[2]) + device_info = DevicesTable.get_device_info(userdata.table_handler, device_id) + if device_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.error("UpdateDeviceStatusService -> 并无对应设备") + message = ErrorResp(message_id, 40014) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if message_type == "offline" and device_info.node_type == "网关设备": + DevicesTable.offline_gateway(userdata.table_handler, device_id) + else: + DevicesTable.update_device_status(userdata.table_handler, device_id, device_status) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + except (KeyError, TypeError): + logger.Logger.error("UpdateDeviceStatusService -> 关键参数缺失") + message = SimpleResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class PropertyService(BaseService): + """设备属性上报事件""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("设备属性上报事件已接收,正在处理 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + device_id = int(userdata.topic.split("/")[2]) + device_info = DevicesTable.get_device_info(userdata.table_handler, device_id) + if device_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.info("PropertyService -> 并无对应设备") + logger.Logger.debug("忽略的事件上报请求已接收,即将给予成功回馈 ...", log_path=None) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if "停车" not in device_info.product_name: # 只处理停车场设备的信息上报 + logger.Logger.debug("忽略的事件上报请求已接收,即将给予成功回馈 ...", log_path=None) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + data = msg["values"] + nt = now_datetime_second() + # 判断是否存在对应停车场,若不存在进行新建,若存在则更新除ID之外的信息 + userdata.table_handler.execute( + """ + INSERT INTO parkinglots + (id, number, name, type, update_datetime) + VALUES (?, ?, ?, 'parkinglot', ?) + ON CONFLICT (id, type) + DO UPDATE SET number = ?, name = ?, update_datetime = ? + """, (data["parkinglot_no"], device_id, data["parkinglot_name"], nt, + device_id, data["parkinglot_name"], nt) + ) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + except (KeyError, TypeError): + logger.Logger.error("PropertyService -> 关键参数缺失") + message = SimpleResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class AreaService(BaseService): + """车场区域信息变化同步事件""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("车场区域信息变化同步事件已接收,正在处理 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + device_id = int(userdata.topic.split("/")[2]) + device_info = DevicesTable.get_device_info(userdata.table_handler, device_id) + if device_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.error("AreaService -> 并无对应设备") + message = SimpleResp(message_id, 40014) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if "停车" not in device_info.product_name: # 确保当前主题是停车场设备 + logger.Logger.error("AreaService -> 节点类型错误") + message = SimpleResp(message_id, 503) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + params = msg["params"] + # ["区域ID","父区域ID","区域编码","区域名称","车位总数"] + # {"area_name":"地面","area_no":"1","place_numbers":"60","area_id":"1","parent_area_id":"1"} + if params["parent_area_id"] == params["area_id"]: + params["parent_area_id"] = None + nt = now_datetime_second() + userdata.table_handler.execute( + "SELECT id FROM parkinglots WHERE number = ? AND type = 'parkinglot'", (device_id,)) + res = userdata.table_handler.cursor.fetchall() + if res: + parkinglot_number = res[0][0] + # 判断是否存在对应区域,若不存在进行新建,若存在则更新除ID之外的信息 + userdata.table_handler.execute( + """ + INSERT OR REPLACE INTO parkinglots + (id, number, name, type, area_id, parkinglot_id, update_datetime) + VALUES (?, ?, ?, 'area', ?, ?, ?) + """, (params["area_id"], params["area_no"], params["area_name"], params["parent_area_id"], + parkinglot_number, nt) + ) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + else: + message = SimpleResp(message_id, 40014) # 对应设备属性没有上报,数据库中未记载设备编码 + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + except (KeyError, TypeError): + logger.Logger.error("AreaService -> 关键参数缺失") + message = SimpleResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class ChannelService(BaseService): + """车场通道信息变化同步事件""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("车场通道信息变化同步事件已接收,正在执行处理 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + device_id = int(userdata.topic.split("/")[2]) + device_info = DevicesTable.get_device_info(userdata.table_handler, device_id) + if device_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.error("ChannelService -> 并无对应设备") + message = SimpleResp(message_id, 40014) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if "停车" not in device_info.product_name: # 确保当前主题是停车场设备 + logger.Logger.error("ChannelService -> 节点类型错误") + message = SimpleResp(message_id, 503) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + params = msg["params"] + # ["区域ID","通道ID","通道编码","通道名称","通道类型"] + # {"channel_name":"测试出口","channel_no":"13","area_id":"1","channel_type":"1","channel_id":"13"} + channel_type = int(params["channel_type"]) + if channel_type == 0: + params["channel_type"] = "入口" + elif channel_type == 1: + params["channel_type"] = "出口" + else: + params["channel_type"] = "出入口" + nt = now_datetime_second() + userdata.table_handler.execute( + "SELECT id FROM parkinglots WHERE number = ? AND type = 'parkinglot'", (device_id,)) + res = userdata.table_handler.cursor.fetchall() + if res: + parkinglot_number = res[0][0] + # 判断是否存在对应区域,若不存在进行新建,若存在则更新除ID之外的信息 + userdata.table_handler.execute( + """ + INSERT OR REPLACE INTO parkinglots + (id, number, name, type, area_id, parkinglot_id, channel_type, update_datetime) + VALUES (?, ?, ?, 'channel', ?, ?, ?, ?) + """, (params["channel_id"], params["channel_no"], params["channel_name"], + params["area_id"], parkinglot_number, params["channel_type"], nt) + ) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + else: + message = SimpleResp(message_id, 40014) # 对应设备属性没有上报,数据库中未记载设备编码 + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + except (KeyError, TypeError): + logger.Logger.error("ChannelService -> 关键参数缺失") + message = SimpleResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class GateService(BaseService): + """道闸信息同步""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("道闸信息同步事件已接收,正在执行处理 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + device_id = int(userdata.topic.split("/")[2]) + device_info = DevicesTable.get_device_info(userdata.table_handler, device_id) + if device_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.error("ChanGateService -> 并无对应设备") + message = SimpleResp(message_id, 40014) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + if "停车" not in device_info.product_name: # 确保当前主题是停车场设备 + logger.Logger.error("GateService -> 节点类型错误") + message = SimpleResp(message_id, 503) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + params = msg["params"] + # ["区域ID","通道ID","道闸编码","道闸ID","道闸名称","道闸品牌","道闸状态","运行状态","是否在线"] + # {"gate_id":"13","gate_name":"测试出口","is_online":"0","gate_no":"13","area_id":"1","channel_id":"13"} + if "is_online" in params.keys(): + is_online = int(params["is_online"]) + if is_online == 0: + params["is_online"] = "下线" + else: + params["is_online"] = "上线" + else: + params["is_online"] = "" + if "gate_status" in params.keys(): + gate_status = int(params["gate_status"]) + if gate_status == 0: + params["gate_status"] = "关闭" + elif gate_status == 1: + params["gate_status"] = "开启" + elif gate_status == 2: + params["gate_status"] = "常开" + else: + params["gate_status"] = "常闭" + else: + params["gate_status"] = "" + if "running_status" in params.keys(): + running_status = int(params["running_status"]) + if running_status == 0: + params["running_status"] = "正常" + elif running_status == 1: + params["running_status"] = "故障" + else: + params["running_status"] = "" + nt = now_datetime_second() + userdata.table_handler.execute( + "SELECT id FROM parkinglots WHERE number = ? AND type = 'parkinglot'", (device_id,)) + res = userdata.table_handler.cursor.fetchall() + if res: + parkinglot_number = res[0][0] + # 判断是否存在对应区域,若不存在进行新建,若存在则更新除ID之外的信息 + userdata.table_handler.execute( + """ + INSERT OR REPLACE INTO parkinglots + (id, number, name, type, area_id, parkinglot_id, channel_id, + is_online, gate_status, running_status, update_datetime) + VALUES (?, ?, ?, 'gate', ?, ?, ?, ?, ?, ?, ?) + """, (params["gate_id"], params["gate_no"], params["gate_name"], + params.get("area_id", ""), parkinglot_number, params.get("channel_id", ""), + params["is_online"], params["gate_status"], params["running_status"], nt) + ) + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + else: + message = SimpleResp(message_id, 40014) # 对应设备属性没有上报,数据库中未记载设备编码 + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + except (KeyError, TypeError): + logger.Logger.error("GateService -> 关键参数缺失") + message = SimpleResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class NoHandlePostService(BaseService): + """忽略的事件上报,直接返回成功的反馈""" + + def handle(self, msg: dict, userdata: UserData): + logger.Logger.debug("忽略的事件上报请求已接收,即将给予成功回馈 ...", log_path=None) + client = userdata.clients["center"][0] + topic = f"{userdata.topic}_resp" + userdata.set_topic(topic) + message_id = msg["messageId"] if "messageId" in msg.keys() else "1" + try: + device_id = int(userdata.topic.split("/")[2]) + device_info = DevicesTable.get_device_info(userdata.table_handler, device_id) + if device_info is None: # 确保当前主题中的设备ID存在 + logger.Logger.error("SubDeviceRegisterService -> 并无对应设备") + message = SimpleResp(message_id, 601) + userdata.set_message(message.dict()) + client.publish(topic, message()) + return + message = SimpleResp(message_id, 0) + userdata.set_message(message.dict()) + client.publish(topic, message()) + except (KeyError, TypeError): + logger.Logger.error("UpdateDeviceStatusService -> 关键参数缺失") + message = SimpleResp(message_id, 401) + userdata.set_message(message.dict()) + client.publish(topic, message()) + + class UpdateFixedCardService(BaseService): + def handle(self, **kwargs): + pass + + class VehicleRegistration(BaseService): + def handle(self, **kwargs): + pass + + +def auto_service(msg: dict, userdata: UserData): + """自动化加载不同的服务层""" + message_type = userdata.topic.split("/")[-1] + servicer = None + if message_type == "register": # 设备注册 + if userdata.topic.split("/")[-2] == "sub": + servicer = Services.SubDeviceRegisterService() + else: + servicer = Services.DeviceRegisterService() + elif message_type in ["online", "offline"]: # 设备上下线 + servicer = Services.UpdateDeviceStatusService() + elif message_type == "post": # 事件上报,一般是通行事件,不做处理,直接返回成功 + if userdata.topic.split("/")[-2] == "property": + servicer = Services.PropertyService() + elif "eventCode" in msg.keys() and msg["eventCode"] in ["area", "channel", "gate"]: + if msg["eventCode"] == "area": + servicer = Services.AreaService() + elif msg["eventCode"] == "channel": + servicer = Services.ChannelService() + elif msg["eventCode"] == "gate": + servicer = Services.GateService() + else: + servicer = Services.NoHandlePostService() + + # 构建服务层实体并进行调用 + if servicer is not None: + servicer.handle(msg, userdata) diff --git a/SCLP/image_server.py b/SCLP/image_server.py new file mode 100644 index 0000000..6bbfa20 --- /dev/null +++ b/SCLP/image_server.py @@ -0,0 +1,80 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : api后端服务程序启动入口 +""" +import argparse +import os + +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import StreamingResponse +import aiofiles +import uvicorn + +from config import VERSION, PREFIX_PATH, FACES_DIR_PATH, IMAGE_SERVER_PORT +from utils import logger +from routers import edge_simulation_api + + +class ImageServer: + def __init__(self, app_args): + self.port = app_args.port + self.app = None + self.prepare() + self.server_main() + + def prepare(self): + """准备处理,一些适配和配置加载工作""" + # 清理图片库,移除不存在数据库中的图片 + logger.Logger.init("人脸图片库执行初始化校验 ...") + for filename in os.listdir(FACES_DIR_PATH): + if filename.startswith("_"): + os.remove(os.path.join(FACES_DIR_PATH, filename)) + logger.Logger.init("人脸图片库完成初始化校验 ✅") + + self.app = FastAPI( + title="Image Server", + description="图片后端服务", + version=VERSION, + docs_url=f"{PREFIX_PATH}/docs" # 设为None则会关闭 + ) + + # self.app.mount(f"{PREFIX_PATH}/faces", StaticFiles(directory=FACES_DIR_PATH), name="static") + self.app.include_router(edge_simulation_api.router, tags=["边缘端请求接口"]) + + @self.app.get(f"{PREFIX_PATH}/faces/{{image_filename}}", summary="返回指定图片") + async def get_image_data(request: Request, image_filename: str): + """返回指定图片""" + image_path = f"{FACES_DIR_PATH}/{image_filename}" + if not os.path.exists(image_path): + raise HTTPException(status_code=404, detail="Image not found") + + async def image_streamer(file_path: str): + async with aiofiles.open(file_path, mode='rb') as f: + while chunk := await f.read(1024): # 每次读取 1024 字节 + yield chunk + + logger.Logger.info(f"{PREFIX_PATH}/faces -> {image_path}") + return StreamingResponse(image_streamer(image_path), media_type="image/jpeg") + + def server_main(self): + """主服务程序""" + uvicorn.run(self.app, host="0.0.0.0", port=self.port, access_log=False) + + +def main(port: str = IMAGE_SERVER_PORT): + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument( + "-p", + "--port", + type=int, + default=port, + help=f"Port of server, default {logger.new_dc(port)}", + ) + app_args = parser.parse_args() + ImageServer(app_args) + + +if __name__ == "__main__": + main() diff --git a/SCLP/main.py b/SCLP/main.py new file mode 100644 index 0000000..49010ef --- /dev/null +++ b/SCLP/main.py @@ -0,0 +1,41 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 主程序运行入口 +""" + +import multiprocessing +import subprocess + +from config import AUTO_CALLBACK, ENV_FLAG, LAN_IP +from utils import logger + + +def run_script(script_path): + # 使用 subprocess 运行脚本 + subprocess.run(['python', script_path]) + + +if __name__ == "__main__": + logger.Logger.init(f"当前程序环境 ENV_FLAG: {ENV_FLAG}") + logger.Logger.init(f"当前环境IP: {LAN_IP}") + # 创建两个进程 + p1 = multiprocessing.Process(target=run_script, args=('backend.py',)) + p2 = multiprocessing.Process(target=run_script, args=('app.py',)) + p3 = multiprocessing.Process(target=run_script, args=('image_server.py',)) + p4 = multiprocessing.Process(target=run_script, args=('auto_callback.py',)) + + # 启动进程 + p1.start() + p2.start() + p3.start() + if AUTO_CALLBACK: + p4.start() + + # 等待进程完成 + p1.join() + p2.join() + p3.join() + if AUTO_CALLBACK: + p4.join() diff --git a/SCLP/models/brands.py b/SCLP/models/brands.py new file mode 100644 index 0000000..732f3c8 --- /dev/null +++ b/SCLP/models/brands.py @@ -0,0 +1,54 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 可视对讲厂家信息表 改&查 +""" +from pydantic import BaseModel, field_validator + +from utils.database import BaseTable, get_table_handler +from utils.misc import InvalidException + + +class UpdateBrand(BaseModel): + name: str + + @field_validator("name") + def check_name(cls, value): + th = get_table_handler() + if BrandsTable.exists(th, value): + return value + else: + raise InvalidException("请提供正确的可视对讲厂家名") + + +class BrandsTable(BaseTable): + @staticmethod + def get_checked_factory(table_handler: BaseTable): + """获取当前启用的可视对讲厂家""" + table_handler.query("SELECT brand_name FROM brands WHERE status = '开启'") + res = table_handler.cursor.fetchall() + if res: + return {"name": res[0][0]} + else: + return {"name": None} + + @staticmethod + def update_checked_factory(table_handler: BaseTable, brand_name: str): + """更新启用的可视对讲厂家""" + sqls = [ + "UPDATE brands SET status = '关闭' WHERE status = '开启'", + "UPDATE brands SET status = '开启' WHERE brand_name = ?" + ] + table_handler.execute(sqls, [(), (brand_name,)]) + return {"status": True} + + @staticmethod + def exists(table_handler: BaseTable, brand_name: str): + """可视对讲厂家是否存在""" + table_handler.query("SELECT brand_name FROM brands WHERE brand_name = ?", (brand_name,)) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False diff --git a/SCLP/models/devices.py b/SCLP/models/devices.py new file mode 100644 index 0000000..9112679 --- /dev/null +++ b/SCLP/models/devices.py @@ -0,0 +1,793 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 设备信息表 增&改&查 +""" +import csv +import os +from datetime import datetime +from typing import List, Optional +from pydantic import BaseModel, field_validator + +from device.call import ServicesCall, DelFaceItem, AddFaceItem +from models.houses import HousesTable +from utils import logger +from utils.database import BaseTable, get_table_handler +from utils.misc import InvalidException, now_datetime_second, decrypt_number + + +class DeviceRegister(BaseModel): + device_mac: str + device_name: str + device_desc: str + third_local_device_id: str + device_status: str = "离线" + device_sct: Optional[str] = None + # 以下需要查询已注册设备信息获取 + gateway_id: Optional[int] = None + gateway_name: Optional[str] = None + # 以下需要查询产品信息表获取 + product_name: str + node_type: str + + +class Device(BaseModel): + device_id: int + device_name: str + node_type: str + product_name: str + + +class AccessDevice(BaseModel): + device_id: int + building_ids: list[str] + + @field_validator("device_id") + def check_device_id(cls, value): + th = get_table_handler() + if DevicesTable.bind_exits(th, value): + raise InvalidException(f"设备:{value} 已被添加过关联") + if not DevicesTable.exits(th, value): + raise InvalidException(f"设备:{value} 不存在") + return value + + @field_validator("building_ids") + def check_authorized_scope(cls, value): + th = get_table_handler() + for i in value: + if not HousesTable.building_exists(th, i): + raise InvalidException(f"楼栋:{i} 不存在") + return value + + +class AccessDevicesScope(BaseModel): + device_type: str + info: List[AccessDevice] + + @field_validator("device_type") + def check_device_type(cls, value): + if value in ["大门闸机", "大门门禁"]: + return value + else: + raise InvalidException("请提供正确的设备类型:[大门闸机, 大门门禁]") + + +class BuildingDevice(BaseModel): + device_id: int + bind_unit_id: str + + @field_validator("device_id") + def check_device_id(cls, value): + th = get_table_handler() + if DevicesTable.bind_exits(th, value): + raise InvalidException(f"设备:{value} 已被添加过关联") + if not DevicesTable.exits(th, value): + raise InvalidException(f"设备:{value} 不存在") + return value + + @field_validator("bind_unit_id") + def check_bind_unit_id(cls, value): + th = get_table_handler() + if not HousesTable.unit_exists(th, value): + raise InvalidException(f"单元:{value} 不存在") + return value + + +class BuildingDevicesScope(BaseModel): + info: List[BuildingDevice] + + +class SearchDevicesInfo(BaseModel): + search_type: Optional[str] = None + search_key: Optional[str] = None + + @field_validator("search_type") + def check_search_type(cls, value): + types = { + "设备名称": "device_name", + "MAC地址": "device_mac", + "设备ID": "device_id" + } + if value in types: + return types[value] + else: + raise InvalidException(f"请提供正确的类型:{list(types.keys())}") + + +class DevicesTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='devices'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE devices ( + device_id INTEGER PRIMARY KEY AUTOINCREMENT, + device_mac TEXT UNIQUE, + device_name TEXT, + device_desc TEXT, + third_local_device_id TEXT, + device_status TEXT, + device_sct TEXT, + gateway_id INTEGER, + gateway_name TEXT, + product_name TEXT, + node_type TEXT, + last_online_datetime TEXT, + register_datetime TEXT + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/devices.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 12: + for row in csvreader: + device_mac = row[0].strip() + device_name = row[1].strip() + device_desc = row[2].strip() if row[2].strip() else None + third_local_device_id = row[3].strip() if row[3].strip() else None + device_status = row[4].strip() if row[4].strip() else "离线" + device_sct = row[5].strip() if row[5].strip() else None + gateway_id = int(row[6].strip()) if row[6].strip() else None + gateway_name = row[7].strip() if row[7].strip() else None + product_name = row[8].strip() + node_type = row[9].strip() + last_online_datetime = row[10].strip() if row[10].strip() else None + register_datetime = row[11].strip() if row[11].strip() else None + data.append((device_mac, device_name, device_desc, third_local_device_id, device_status, + device_sct, gateway_id, gateway_name, product_name, node_type, + last_online_datetime, register_datetime)) + table_handler.executemany( + f""" + INSERT INTO devices + (device_mac, device_name, device_desc, third_local_device_id, device_status, + device_sct, gateway_id, gateway_name, product_name, node_type, + last_online_datetime, register_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (device_mac) DO NOTHING + """, + data + ) + + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='devices_scope'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE devices_scope ( + device_id INT, + device_type TEXT, + bind_unit_id TEXT, + UNIQUE (device_id, bind_unit_id) + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), + "data/InitialData/devices_scope.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 3: + for row in csvreader: + device_id = row[0].strip() + device_type = row[1].strip() + bind_unit_id = row[2].strip() + data.append((device_id, device_type, bind_unit_id)) + table_handler.executemany( + f""" + INSERT INTO devices_scope + (device_id, device_type, bind_unit_id) + VALUES (?, ?, ?) + ON CONFLICT (device_id, bind_unit_id) DO NOTHING + """, data + ) + + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='devices_auth'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE devices_auth ( + device_id INT, + _id TEXT, + record_type TEXT, + start_date TEXT, + expire_date TEXT, + add_datetime TEXT, + update_datetime TEXT, + UNIQUE (device_id, _id, record_type) + ) + """ + ) + + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), + "data/InitialData/devices_auth.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 5: + for row in csvreader: + device_id = row[0].strip() + _id = row[1].strip() + record_type = row[2].strip() + start_date = row[3].strip() + expire_date = row[4].strip() + data.append((device_id, _id, record_type, start_date, expire_date, + now_datetime_second(), now_datetime_second())) + table_handler.executemany( + f""" + INSERT INTO devices_auth + (device_id, _id, record_type, start_date, expire_date, add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (device_id, _id, record_type) DO NOTHING + """, data + ) + + @staticmethod + def insert_by_device_register(table_handler: BaseTable, obj: DeviceRegister): + register_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + table_handler.execute( + """ + INSERT INTO devices + (device_mac, device_name, device_desc, third_local_device_id, device_status, device_sct, + gateway_id, gateway_name, product_name, node_type, register_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (device_mac) DO UPDATE SET + device_name=?, device_desc=?, third_local_device_id=?, + device_status=?, device_sct=?, gateway_id=?, gateway_name=?, + product_name=?, node_type=?, register_datetime=? + """, + (obj.device_mac, obj.device_name, obj.device_desc, obj.third_local_device_id, + obj.device_status, obj.device_sct, obj.gateway_id, obj.gateway_name, + obj.product_name, obj.node_type, register_datetime, + obj.device_name, obj.device_desc, obj.third_local_device_id, + obj.device_status, obj.device_sct, obj.gateway_id, obj.gateway_name, + obj.product_name, obj.node_type, register_datetime) + ) + table_handler.query( + """ + SELECT device_id + FROM devices + WHERE device_mac = ? + """, + (obj.device_mac,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + return None + + @staticmethod + def insert_device_auth_record(table_handler: BaseTable, device_id: int, _id: str, + start_date: str, expire_date: str, record_type: str = '人行'): + table_handler.execute( + """ + INSERT INTO devices_auth + (device_id, _id, record_type, start_date, expire_date, add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (device_id, _id, record_type) DO UPDATE SET + start_date=?, expire_date=?, update_datetime=? + """, + (device_id, _id, record_type, start_date, expire_date, + now_datetime_second(), now_datetime_second(), start_date, expire_date, now_datetime_second()) + ) + + @staticmethod + def get_devices_info(table_handler: BaseTable): + """获取对应设备的基本信息""" + table_handler.query( + """ + SELECT device_name, third_local_device_id, device_id, device_status, + gateway_name, product_name, node_type, last_online_datetime, register_datetime + FROM devices + WHERE node_type != '网关设备' + """ + ) + res = table_handler.cursor.fetchall() + if res: + devices_info = [] + for item in res: + devices_info.append({ + "device_name": item[0], + "third_local_device_id": item[1], + "device_id": item[2], + "device_status": item[3], + "gateway_name": item[4] if item[4] else "", + "product_name": item[5], + "node_type": item[6], + "last_online_datetime": item[7] if item[7] else "", + "register_datetime": item[8] + }) + return devices_info + return None + + @staticmethod + def get_device_info(table_handler: BaseTable, device_id: int): + """获取对应设备的基本信息""" + table_handler.query( + """ + SELECT device_name, node_type, product_name + FROM devices + WHERE device_id = ? + """, + (device_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return Device(device_id=device_id, device_name=res[0][0], node_type=res[0][1], product_name=res[0][2]) + return None + + @staticmethod + def get_device_name(table_handler: BaseTable, device_id: int): + """获取对应设备名""" + table_handler.query( + """ + SELECT device_name + FROM devices + WHERE device_id = ? + """, + (device_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + return "" + + @staticmethod + def get_device_scope_type(table_handler: BaseTable, device_id: int): + """获取对应设备门禁类型""" + table_handler.query( + """ + SELECT device_type + FROM devices_scope + WHERE device_id = ? + """, + (device_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + return "" + + @staticmethod + def get_device_ids_by_unit_id(table_handler: BaseTable, unit_id: str) -> list: + """查询对应单元权限内的设备ID""" + table_handler.query( + """ + SELECT GROUP_CONCAT(device_id) AS device_ids + FROM devices_scope + WHERE bind_unit_id = ? + """, (unit_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0].split(',') + else: + return [] + + @staticmethod + def get_device_ids(table_handler: BaseTable, filter_name: Optional[str] = None, filter_online: bool = True): + """获取相关的设备ID""" + sub_sql_list = [] + if filter_online: + sub_sql_list.append("device_status = '在线'") + if filter_name is not None: + sub_sql_list.append(f"product_name like '%{filter_name}%'") + if len(sub_sql_list) > 0: + sub_sql = "WHERE " + " AND ".join(sub_sql_list) + else: + sub_sql = "" + table_handler.query(f"SELECT GROUP_CONCAT(device_id) device_ids FROM devices {sub_sql}") + res = table_handler.cursor.fetchall() + if res: + return [i for i in res[0][0].split(',')] + else: + return [] + + @staticmethod + def get_auth_interval(table_handler: BaseTable, _id: str, device_id: int, record_type: str = '人行'): + """获取对应ID授权在设备上的有效期""" + table_handler.query( + """ + SELECT start_date, expire_date + FROM devices_auth + WHERE device_id = ? and _id = ? and record_type = ? + """, + (device_id, _id, record_type) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0], res[0][1] + return "", "" + + @staticmethod + def update_device_status(table_handler: BaseTable, device_id: int, device_status: str): + """更新设备在线状态""" + if device_status == "在线": + last_online_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + set_sql = f"device_status='在线', last_online_datetime='{last_online_datetime}' " + else: + set_sql = f"device_status='离线' " + table_handler.execute( + f""" + UPDATE devices SET {set_sql} + WHERE device_id={device_id} + """ + ) + + @staticmethod + def offline_gateway(table_handler: BaseTable, device_id: int): + """用于在网关下线时同时下线网关下的所有设备""" + table_handler.execute( + f""" + UPDATE devices SET device_status='离线' + WHERE device_id={device_id} or gateway_id={device_id} + """ + ) + + @staticmethod + def get_access_devices_info(table_handler: BaseTable, + is_associated: bool, + device_name: str = None, + product_name: str = None, + device_mac: str = None, + device_id: int = None): + if is_associated: + sub_sql = "WHERE d.node_type != '网关设备' AND product_name not like '%停车场%' AND device_type is not null " + else: + sub_sql = "WHERE d.node_type != '网关设备' AND product_name not like '%停车场%' AND device_type is null " + if device_name: + sub_sql += f"AND device_name = '{device_name}'" + elif product_name: + sub_sql += f"AND product_name = '{product_name}'" + elif device_mac: + sub_sql += f"AND device_mac = '{device_mac}'" + elif device_id: + sub_sql += f"AND d.device_id = {device_id}" + table_handler.query( + f""" + SELECT + d.device_id, + device_name, + device_mac, + device_status, + ds.device_type, + GROUP_CONCAT(t.building_name || '-' || t.unit_name) as scopes, + product_name + FROM devices d + LEFT JOIN devices_scope ds ON ds.device_id = d.device_id + LEFT JOIN (SELECT DISTINCT unit_id, unit_name, building_id, building_name FROM houses) t + ON t.unit_id = ds.bind_unit_id + {sub_sql} + GROUP BY d.device_id, device_name, device_mac, device_status, ds.device_type, product_name + """ + ) + res = table_handler.cursor.fetchall() + if res: + devices_info = [] + for i in res: + devices_info.append({ + "device_id": i[0], + "device_name": i[1], + "device_mac": i[2], + "device_status": i[3], + "device_type": i[4] if i[4] else "", + "authorized_scope": i[5].split(",") if i[5] else [], + "product_name": i[6] + }) + return {"devices": devices_info} + return {"devices": []} + + @staticmethod + def get_associated_access_devices_info(table_handler: BaseTable, + search_type: Optional[str] = None, + search_key: Optional[str] = None): + if search_type: + if search_type == "device_id": + sub_sql = f"WHERE ds.device_id = {search_key}" + elif search_type == "device_name": + sub_sql = f"WHERE d.device_name like '%{search_key}%'" + else: + sub_sql = f"WHERE d.{search_type} = '{search_key}'" + else: + sub_sql = "" + table_handler.query( + f""" + SELECT ds.device_id as _id, + d.device_name as _name, + d.device_mac as _mac, + d.device_status as _status + FROM (SELECT DISTINCT device_id FROM devices_scope) ds + LEFT JOIN devices d ON ds.device_id = d.device_id + {sub_sql} + """ + ) + res = table_handler.cursor.fetchall() + if res: + devices_info = [] + for i in res: + devices_info.append({ + "device_id": i[0], + "device_name": i[1], + "device_mac": i[2], + "device_status": i[3] + }) + return {"devices": devices_info} + return {"devices": []} + + @staticmethod + def auto_auth_by_unit_id(table_handler: BaseTable, device_id: int, unit_id: str): + """查询对应单元下相关住户,对具备人脸的住户授权""" + table_handler.query( + """ + SELECT h.householder_id, name, phone, face_url + FROM householders h + LEFT JOIN householders_type ht ON ht.householder_id=h.householder_id + LEFT JOIN houses ON ht.room_id=houses.room_id + WHERE h.type = '住户' AND (face_url != '' or face_url is not null ) AND houses.unit_id = ? + """, (unit_id,) + ) + res = table_handler.cursor.fetchall() + if res: + # 人员人脸下放 + for i in res: + sc = ServicesCall() + face_item = AddFaceItem( + user_id=i[0], + name=i[1], + phone_number=decrypt_number(i[2]), + face_url=i[3], + device_ids=str([device_id]) + ) + _callback, code, msg = sc.add_face(device_id, face_item) + if _callback: + DevicesTable.insert_device_auth_record(table_handler, device_id, face_item.user_id, + face_item.start_date, face_item.expire_date) + + @staticmethod + def add_access_devices(table_handler: BaseTable, device_type: str, objs: List[AccessDevice]): + data = [] + for obj in objs: + for bind_building_id in obj.building_ids: + unit_ids = HousesTable.get_unit_ids(table_handler, bind_building_id) + for unit_id in unit_ids: + data.append((obj.device_id, device_type, unit_id, device_type)) + DevicesTable.auto_auth_by_unit_id(table_handler, obj.device_id, unit_id) + try: + table_handler.executemany( + """ + INSERT INTO devices_scope + (device_id, device_type, bind_unit_id) + VALUES (?, ?, ?) + ON CONFLICT (device_id, bind_unit_id) DO UPDATE + SET device_type = ? + """, data + ) + return True + except Exception as e: + logger.Logger.error(f"DevicesTable.add_access_devices: {type(e).__name__}, {e}") + raise InvalidException(f"DevicesTable.add_access_devices: {type(e).__name__}, {e}") + + @staticmethod + def add_building_devices(table_handler: BaseTable, device_type: str, objs: List[BuildingDevice]): + data = [] + for obj in objs: + data.append((obj.device_id, device_type, obj.bind_unit_id)) + DevicesTable.auto_auth_by_unit_id(table_handler, obj.device_id, obj.bind_unit_id) + try: + table_handler.executemany( + """ + INSERT INTO devices_scope + (device_id, device_type, bind_unit_id) + VALUES (?, ?, ?) + ON CONFLICT (device_id, bind_unit_id) DO NOTHING + """, data + ) + return True + except Exception as e: + logger.Logger.error(f"DevicesTable.add_building_devices: {type(e).__name__}, {e}") + return False + + @staticmethod + def add_update_device_auth(table_handler: BaseTable, device_id, _ids: list[str], record_type: str, + start_date: str, expire_date: str): + """对应设备批量授权记录增加""" + data = [] + for _id in _ids: + _datetime = now_datetime_second() + data.append((device_id, _id, record_type, start_date, expire_date, _datetime, _datetime, + start_date, expire_date, _datetime)) + try: + table_handler.executemany( + """ + INSERT INTO devices_auth + (device_id, _id, record_type, start_date, expire_date, add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (device_id, _id, record_type) + DO UPDATE SET start_date = ?, expire_date = ?, update_datetime = ? + """, data + ) + return True + except Exception as e: + logger.Logger.error(f"DevicesTable.add_device_auth: {type(e).__name__}, {e}") + return False + + @staticmethod + def get_auth_device_ids(table_handler: BaseTable, _id: str, record_type: str = '人行'): + """获取ID的所有授权设备""" + table_handler.query( + """ + SELECT device_id + FROM devices_auth + WHERE _id = ? AND record_type = ? + """, (_id, record_type) + ) + res = table_handler.cursor.fetchall() + if res: + return [i[0] for i in res] + else: + return [] + + @staticmethod + def get_auth_device_info(table_handler: BaseTable, _id: str, record_type: str = '人行'): + """获取ID当前所有授权设备基础信息""" + table_handler.query( + """ + SELECT devices_auth.device_id, device_name, device_mac, device_status + FROM devices_auth + LEFT JOIN devices ON devices.device_id = devices_auth.device_id + WHERE _id = ? AND record_type = ? + """, (_id, record_type) + ) + res = table_handler.cursor.fetchall() + if res: + return [{"device_id": i[0], "device_name": i[1], "device_mac": i[2], "device_status": i[3]} for i in res] + else: + return [] + + @staticmethod + def get_auth_householders_info(table_handler: BaseTable, device_id: int): + """获取关联授权下的所有完成下放的人员信息""" + table_handler.query( + """ + SELECT da._id, name, phone, face_url, da.start_date, da.expire_date + FROM devices_auth da + LEFT JOIN householders h ON da._id = h.householder_id + WHERE device_id = ? AND record_type = '人行' + """, (device_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return [{"id": i[0], "name": i[1], "phone": i[2], "url": i[3], "sdate": i[4], "edate": i[5]} for i in res] + else: + return [] + + @staticmethod + def delete_householder_all_auth(table_handler: BaseTable, _id: str): + table_handler.execute( + """ + DELETE FROM devices_auth + WHERE _id = ? AND record_type = '人行' + """, (_id,) + ) + + @staticmethod + def delete_invalid_auth_record(table_handler: BaseTable, device_id: int, _id: str, record_type: str = '人行'): + table_handler.execute( + """ + DELETE FROM devices_auth + WHERE device_id = ? AND _id = ? AND record_type = ? + """, + (device_id, _id, record_type) + ) + + @staticmethod + def delete_access_device_info(table_handler: BaseTable, device_id: int): + # 1. 确认设备ID有效 + if not DevicesTable.exits(table_handler, device_id): + raise InvalidException(f"设备:{device_id} 不存在") + # 2. 获取关联授权下的所有完成下放的人员信息 + householder_infos = DevicesTable.get_auth_householders_info(table_handler, device_id) + if len(householder_infos) > 0: + # 3. 移除对应设备中所有的人员信息 + sc = ServicesCall() + success_ids = [] + for index, householder_info in enumerate(householder_infos): + _callback, code, _ = sc.del_face(device_id, + DelFaceItem(user_id=str(householder_info["id"]), + device_ids=str([device_id]))) + if not _callback: + # 存在异常时,回滚已删除掉的人脸 + failed = [] + msgs = [] + for success_id in success_ids: + _back, code, msg = sc.add_face(device_id, AddFaceItem( + name=householder_infos[success_id]["name"], + user_id=str(householder_infos[success_id]["id"]), + phone_number=householder_infos[success_id]["phone"], + face_url=householder_infos[success_id]["url"], + device_ids=str([device_id]), + start_date=householder_infos[success_id]["sdate"], + expire_date=householder_infos[success_id]["edate"] + )) + if not _back: + failed.append(householder_infos[success_id]["id"]) + msgs.append(msg) + if len(failed) == 0: + raise InvalidException( + f"住户 - {householder_info['id']} 人脸授权移除失败,错误码{code}, {msgs}, 已完成回滚") + else: + raise InvalidException( + f"住户 - {householder_info['id']} 人脸授权移除失败,错误码{code}, {msgs}, 回滚失败") + success_ids.append(index) + # 4. 移除设备的关联记录 + table_handler.execute( + """ + DELETE FROM devices_auth + WHERE device_id = ? AND record_type = '人行' + """, (device_id,) + ) + table_handler.execute( + """ + DELETE FROM devices_scope + WHERE device_id = ? + """, (device_id,) + ) + return {"status": True} + + @staticmethod + def exits(table_handler: BaseTable, device_id: int): + table_handler.query( + """ + SELECT device_id FROM devices + WHERE device_id = ? + """, (device_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def bind_exits(table_handler: BaseTable, device_id: int): + table_handler.query( + """ + SELECT bind_unit_id + FROM devices_scope + WHERE device_id = ? + """, (device_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False diff --git a/SCLP/models/householders.py b/SCLP/models/householders.py new file mode 100644 index 0000000..857fdf3 --- /dev/null +++ b/SCLP/models/householders.py @@ -0,0 +1,1369 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 住户信息表 & 住户-房产关联表 增&删&改&查 +""" +import csv +import os +import re +import shutil +from typing import Union, Optional, List + +from pydantic import BaseModel, field_validator + +from config import PREFIX_PATH, FACES_DIR_PATH, SUB_PATH +from config import APP_HOST as HOST_IP +from config import IMAGE_SERVER_PORT as PORT +from device.call import ServicesCall, DelFaceItem, AddFaceItem +from models.devices import DevicesTable +from models.houses import HousesTable +from utils import logger +from utils.database import BaseTable, get_table_handler +from utils.misc import now_datetime_second, InvalidException, is_image_valid, get_file_md5, encrypt_number, \ + decrypt_number + +ROLE_TYPES = ["业主", "亲属", "租户"] + + +class HouseholdersInfo(BaseModel): + householder_id: int + name: str + sex: str + phone: str + rooms: Union[str, list] + add_datetime: str + + @field_validator("rooms") + def convert_rooms(cls, value): + return [i.strip() for i in value.split(",")] + + +class HouseholderDetailInfo(BaseModel): + householder_id: int + name: str + phone: str + face_url: str + rooms: list + authorized_devices: list + + +class AuthHouseholdersInfo(BaseModel): + householder_id: int + name: str + phone: str + rooms: Union[str, list] + + @field_validator("rooms") + def convert_rooms(cls, value): + return [i.strip() for i in value.split(",")] + + +class AuthUsersInfo(BaseModel): + user_id: int + name: str + sex: str + phone: str + card_type: str + card_id: str + user_type: str + start_datetime: str + expire_datetime: str + + +class AuthUsersDetailInfo(AuthUsersInfo): + face_url: str + authorized_devices: List[dict] + + +class AddAuthUserInfo(BaseModel): + name: str + sex: str + phone: str + card_type: str + card_id: str + user_type: str + start_datetime: str + expire_datetime: str + face_url: str + authorized_devices: List[int] + + @field_validator("sex") + def check_sex(cls, value): + types = ["男", "女"] + if value not in types: + raise InvalidException(f"请提供正确的sex类型 {types}") + else: + return value + + @field_validator('phone') + def check_phone(cls, value): + pattern = re.compile(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$') + if pattern.search(value) is None: + raise InvalidException("请提供正确的手机号码") + th = get_table_handler() + if HouseholdersTable.exists_user_phone(th, value): + raise InvalidException("手机号码已存在,请勿重复提交") + return value + + @field_validator("user_type") + def check_user_type(cls, value): + types = ["员工", "访客", "其他"] + if value not in types: + raise InvalidException(f"请提供正确的user_type类型 {types}") + else: + return value + + @field_validator("face_url") + def check_face_url(cls, value, values): + filename = value.split('/')[-1] + file_path = str(os.path.join(FACES_DIR_PATH, filename)) + if is_image_valid(file_path): + if filename.startswith('_'): + new_filename = \ + f"{values.data.get('name')}{encrypt_number(values.data.get('phone'))}.{filename.split('.')[-1]}" + shutil.copy(file_path, f"{FACES_DIR_PATH}/{new_filename}") + os.remove(file_path) + return new_filename + return filename + else: + raise InvalidException(f"地址或图片无效,请提供有效的图片地址 {value}") + + @field_validator("authorized_devices") + def check_authorized_devices(cls, value): + if len(value) > 0: + return value + else: + raise InvalidException("请提供至少一个门禁设备权限") + + +class UpdateAuthUserInfo(AddAuthUserInfo): + user_id: int + phone: str + + @field_validator("user_id") + def check_user_id(cls, value): + th = get_table_handler() + if HouseholdersTable.exists_user_id(th, value): + return value + else: + raise InvalidException("user_id 不存在") + + @field_validator('phone') + def check_phone(cls, value): + pattern = re.compile(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$') + if pattern.search(value) is None: + raise InvalidException("请提供正确的手机号码") + return value + + +class AddHouseholderInfo(BaseModel): + name: str + sex: str + phone: str + property_info: List[tuple[str, str]] + + @field_validator('sex') + def check_sex(cls, value): + if value not in ["男", "女"]: + raise InvalidException("请提供正确的性别类型") + return value + + @field_validator('phone') + def check_phone(cls, value): + pattern = re.compile(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$') + if pattern.search(value) is None: + raise InvalidException("请提供正确的手机号码") + th = get_table_handler() + if HouseholdersTable.exists_householder_phone(th, value): + raise InvalidException("住户手机号码已存在,请勿重复提交") + return value + + @field_validator('property_info') + def check_property_info(cls, value): + th = get_table_handler() + room_ids = [] + for item in value: + if not HousesTable.exists(th, item[0]): + raise InvalidException("对应房产不存在") + if item[1] not in ROLE_TYPES: + raise InvalidException("请提供正确的身份类型") + room_ids.append(item[0]) + if len(set(room_ids)) != len(room_ids): + raise InvalidException("不能存在重复房产") + return value + + +class GetHouseholdersInfo(BaseModel): + search_type: Optional[str] = None + search_key: Optional[str] = None + role_type: str = "全部" + limit: Optional[int] = None + page: Optional[int] = None + + @field_validator("search_type") + def check_search_type(cls, value): + types = { + "姓名": "name", + "电话号码": "phone" + } + if value not in ["姓名", "电话号码"]: + raise InvalidException("请提供正确的关键字类型") + else: + return types[value] + + @field_validator("search_key") + def check_search_key(cls, value, values): + if value and values.data.get("search_type", None): + return value + else: + raise InvalidException("search 参数缺失") + + @field_validator("page") + def check_page(cls, value, values): + if (value is not None and value > 0 + and values.data.get("limit", None) is not None and values.data.get("limit") != 0): + return value + else: + raise InvalidException("page 参数缺失或异常") + + @field_validator("role_type") + def check_role_type(cls, value): + if value not in ROLE_TYPES + ["全部"]: + raise InvalidException("请提供正确的身份类型") + else: + return value + + +class GetAuthHouseholdersInfo(BaseModel): + search_type: Optional[str] = None + search_key: Optional[str] = None + space_type: Optional[str] = None + space_id: Optional[str] = None + limit: Optional[int] = None + page: Optional[int] = None + + @field_validator("search_type") + def check_search_type(cls, value): + types = { + "住户姓名": "name", + "手机号码": "phone" + } + if value not in types.keys(): + raise InvalidException(f"请提供正确的关键字类型 {list(types.keys())}") + else: + return types[value] + + @field_validator("search_key") + def check_search_key(cls, value, values): + if (value or value.strip() == "") and values.data.get("search_type", None): + if value.strip() == "": + value = None + return value + else: + raise InvalidException("search 参数缺失") + + @field_validator("space_type") + def check_space_type(cls, value): + types = { + "区域": "name_id", + "楼栋": "building_id", + "单元": "unit_id", + "楼层": "floor_id", + "房间号": "room_id" + } + if value not in types.keys(): + raise InvalidException(f"请提供正确的关联房产类型 {list(types.keys())}") + else: + return types[value] + + @field_validator("space_id") + def check_space_id(cls, value, values): + if value and values.data.get("space_type", None): + return value + else: + raise InvalidException("space 参数缺失") + + @field_validator("page") + def check_page(cls, value, values): + if (value is not None and value > 0 + and values.data.get("limit", None) is not None and values.data.get("limit") != 0): + return value + else: + raise InvalidException("page 参数缺失或异常") + + +class GetAuthUsersInfo(BaseModel): + search_type: Optional[str] = None + search_key: Optional[str] = None + user_type: Optional[str] = None + limit: Optional[int] = None + page: Optional[int] = None + + @field_validator("search_type") + def check_search_type(cls, value): + types = { + "姓名": "name", + "手机号码": "phone" + } + if value not in types.keys(): + raise InvalidException(f"请提供正确的关键字类型 {list(types.keys())}") + else: + return types[value] + + @field_validator("search_key") + def check_search_key(cls, value, values): + if value and values.data.get("search_type", None): + return value + else: + raise InvalidException("search 参数缺失") + + @field_validator("user_type") + def check_space_type(cls, value): + types = { + "员工": "员工", + "访客": "访客", + "其他": "其他" + } + if value not in types.keys(): + raise InvalidException(f"请提供正确的人员类型 {list(types.keys())}") + else: + return types[value] + + @field_validator("page") + def check_page(cls, value, values): + if (value is not None and value > 0 + and values.data.get("limit", None) is not None and values.data.get("limit") != 0): + return value + else: + raise InvalidException("page 参数缺失或异常") + + +class UpdateHouseholderInfo(BaseModel): + householder_id: int + property_info: List[tuple[str, str]] + + @field_validator('property_info') + def check_property_info(cls, value): + th = get_table_handler() + for item in value: + if not HousesTable.exists(th, item[0]): + raise InvalidException("对应房产不存在") + if item[1] not in ROLE_TYPES: + raise InvalidException("请提供正确的身份类型") + return value + + +class UpdateHouseholderFace(BaseModel): + id: int + face_url: str + + @field_validator("id") + def check_id(cls, value): + th = get_table_handler() + if HouseholdersTable.exists_id(th, value): + return value + else: + raise InvalidException(f"无效ID {value}") + + @field_validator("face_url") + def check_face_url(cls, value, values): + filename = value.split('/')[-1] + file_path = str(os.path.join(FACES_DIR_PATH, filename)) + if is_image_valid(file_path): + # 若为临时图片,则进行规则命名 + if filename.startswith('_'): + th = get_table_handler() + name_phone = HouseholdersTable.get_householder_np_by_id(th, values.data.get('id')) + new_filename = f"{name_phone['name']}{name_phone['phone']}.{filename.split('.')[-1]}" + shutil.copy(file_path, f"{FACES_DIR_PATH}/{new_filename}") + os.remove(file_path) + return new_filename + return filename + else: + raise InvalidException(f"地址或图片无效,请提供有效的图片地址 {value}") + + +class HouseholdersTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + # householders 住户信息表 + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='householders'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + """ + CREATE TABLE householders ( + householder_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + sex TEXT, + phone TEXT, + face_url TEXT DEFAULT '', + face_md5 TEXT, + type TEXT, + user_type TEXT, + card_type TEXT, + card_id TEXT, + auth_start TEXT, + auth_expire TEXT, + add_datetime TEXT, + update_datetime TEXT, + UNIQUE (phone, type) + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), + "data/InitialData/householders.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 11: + for row in csvreader: + householder_id = row[0].strip() + name = row[1].strip() + sex = row[2].strip() + phone = encrypt_number(row[3].strip()) + face_url = row[4].strip() + if face_url and os.path.exists(f"{FACES_DIR_PATH}/{face_url}"): + face_md5 = get_file_md5(f"{FACES_DIR_PATH}/{face_url}") + else: + face_md5 = None + _type = row[5].strip() + if _type != "住户": + user_type = row[6].strip() + card_type = row[7].strip() + card_id = row[8].strip() + auth_start = row[9].strip() + auth_expire = row[10].strip() + else: + user_type, card_type, card_id, auth_start, auth_expire = None, None, None, None, None + data.append((householder_id, name, sex, phone, face_url, face_md5, + _type, user_type, card_type, card_id, auth_start, auth_expire, + now_datetime_second(), now_datetime_second())) + table_handler.executemany( + """ + INSERT OR IGNORE INTO householders + (householder_id, name, sex, phone, face_url, face_md5, + type, user_type, card_type, card_id, auth_start, auth_expire, + add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, data + ) + + # householders_type 住户-房产关联表 + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='householders_type'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + """ + CREATE TABLE householders_type ( + householder_id INT, + room_id TEXT, + type TEXT, + update_datetime TEXT, + UNIQUE (householder_id, room_id) + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), + "data/InitialData/householders_type.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + count_info = {} + if len(head) == 3: + for row in csvreader: + householder_id = row[0].strip() + room_id = row[1].strip() + _type = row[2].strip() + if not HouseholdersTable.exists_householder_type(table_handler, householder_id, room_id): + if room_id in count_info.keys(): + count_info[room_id] += 1 + else: + count_info[room_id] = 1 + data.append((int(householder_id), room_id, _type, now_datetime_second())) + table_handler.executemany( + """ + INSERT INTO householders_type + (householder_id, room_id, type, update_datetime) + VALUES (?, ?, ?, ?) + ON CONFLICT (householder_id, room_id) DO NOTHING + """, data + ) + for _id, _count in count_info.items(): + old_count = HousesTable.get_householder_count(table_handler, _id) + HousesTable.update_householder_count(table_handler, _id, old_count + _count) + + @staticmethod + def insert(table_handler: BaseTable, name: str, sex: str, phone: str, _type: str): + try: + table_handler.execute( + """ + INSERT OR IGNORE INTO householders + (name, sex, phone, type, add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?) + """, (name, sex, encrypt_number(phone), _type, now_datetime_second(), now_datetime_second()) + ) + finally: + table_handler.query( + """ + SELECT householder_id + FROM householders + WHERE phone = ? + """, (encrypt_number(phone),) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return None + + @staticmethod + def insert_type(table_handler: BaseTable, room_id: str, householder_id: int, _type: str): + table_handler.execute( + """ + INSERT INTO householders_type + (householder_id, room_id, type, update_datetime) + VALUES (?, ?, ?, ?) + ON CONFLICT (householder_id, room_id) DO UPDATE + SET type = ?, update_datetime = ? + """, (householder_id, room_id, _type, now_datetime_second(), _type, now_datetime_second()) + ) + + @staticmethod + def add_auth_user_info(table_handler: BaseTable, obj: AddAuthUserInfo): + """插入通用人员信息 & 进行通用人员设备授权""" + now_datetime = now_datetime_second() + face_md5 = get_file_md5(f"{FACES_DIR_PATH}/{obj.face_url}") + table_handler.execute( + """ + INSERT OR IGNORE INTO householders + (name, sex, phone, face_url, face_md5, type, + user_type, card_type, card_id, + auth_start, auth_expire, add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (obj.name, obj.sex, encrypt_number(obj.phone), obj.face_url, face_md5, "非住户", + obj.user_type, obj.card_type, obj.card_id, + obj.start_datetime, obj.expire_datetime, now_datetime, now_datetime) + ) + user_id = HouseholdersTable.get_householder_id(table_handler, obj.phone, '非住户') + failed_ids = [] + codes = [] + msgs = [] + if user_id: + try: + # 人脸下置,设备授权 + sc = ServicesCall() + face_item = AddFaceItem( + user_id=user_id, + name=obj.name, + phone_number=obj.phone, + face_url=obj.face_url, + start_date=obj.start_datetime.replace(' ', 'T') + ':00.000Z', + expire_date=obj.expire_datetime.replace(' ', 'T') + ':00.000Z', + device_ids="" + ) + for device_id in obj.authorized_devices: + face_item.device_ids = f"[{device_id}]" + _callback, code, msg = sc.add_face(device_id, face_item) + if _callback: + DevicesTable.insert_device_auth_record(table_handler, device_id, face_item.user_id, + face_item.start_date, face_item.expire_date) + else: + failed_ids.append(device_id) + codes.append(code) + msgs.append(msg) + finally: + if 0 < len(failed_ids): + table_handler.execute( + """ + DELETE FROM householders + WHERE householder_id = ? + """, (user_id,) + ) + return {"status": False, "message": f"部分设备 - {failed_ids} 人脸授权失败,错误码:{codes},{msgs}"} + return {"status": True} + + @staticmethod + def get_householder_id(table_handler: BaseTable, phone: str, _type: str) -> str: + table_handler.query("SELECT householder_id FROM householders WHERE phone=? AND type=?", + (encrypt_number(phone), _type)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + raise None + + @staticmethod + def get_householder_info(table_handler: BaseTable, search_type, search_key, role_type, + page: Optional[int] = None, limit: Optional[int] = None): + sub_sql_list = [] + if role_type != "全部": + sub_sql_list.append(f"types like '%{role_type}%'") + if search_type is not None: + if search_type == "name": + sub_sql_list.append(f"name like '%{search_key}%'") + else: + sub_sql_list.append(f"{search_type} = '{search_key}'") + + if sub_sql_list: + sub_sql = "WHERE " + " AND ".join(sub_sql_list) + else: + sub_sql = "" + if page is not None and limit is not None: + page_sql = f"LIMIT {limit} OFFSET {(page - 1) * limit}" + else: + page_sql = "" + query_sql = f""" + SELECT * FROM ( + SELECT h.householder_id, + h.name, + h.sex, + h.phone, + GROUP_CONCAT(houses.house_name) AS rooms, + GROUP_CONCAT(hh.type) AS types, + h.add_datetime + FROM householders h + LEFT JOIN householders_type hh ON h.householder_id = hh.householder_id + LEFT JOIN houses ON hh.room_id = houses.room_id + WHERE h.type = '住户' + GROUP BY h.householder_id, h.name, h.sex, h.phone, h.add_datetime + ) {sub_sql} + """ + table_handler.query(f"SELECT COUNT(*) FROM ({query_sql})") + total = table_handler.cursor.fetchall()[0][0] + table_handler.query(f"{query_sql} {page_sql}") + res = table_handler.cursor.fetchall() + if res: + householder_info = [] + for i in res: + householder_info.append(HouseholdersInfo( + householder_id=i[0], + name=i[1], + sex=i[2], + phone=decrypt_number(i[3]), + rooms=i[4], + add_datetime=i[6] + ).__dict__) + return {"info": householder_info, "total": total} + else: + return {"info": [], "total": total} + + @staticmethod + def get_auth_householder_info(table_handler: BaseTable, + search_type: Optional[str], search_key: Optional[str], + space_type: Optional[str], space_id: Optional[str], + page: Optional[int] = None, limit: Optional[int] = None): + sub_sql_list = [] + if search_type is not None and search_key is not None: + if search_type == "name": + sub_sql_list.append(f"{search_type} like '%{search_key}%'") + else: + sub_sql_list.append(f"{search_type} = '{search_key}'") + if space_type is not None and space_id is not None: + space_sub_sql = f""" + AND h.householder_id IN ( + SELECT DISTINCT h1.householder_id + FROM householders h1 + LEFT JOIN householders_type hh1 ON h1.householder_id = hh1.householder_id + LEFT JOIN houses houses1 ON hh1.room_id = houses1.room_id + WHERE houses1.{space_type} = '{space_id}' + ) + """ + else: + space_sub_sql = "" + if sub_sql_list: + sub_sql = "WHERE " + " AND ".join(sub_sql_list) + else: + sub_sql = "" + if page is not None and limit is not None: + page_sql = f"LIMIT {limit} OFFSET {(page - 1) * limit}" + else: + page_sql = "" + query_sql = f""" + SELECT * FROM ( + SELECT h.householder_id, h.name, h.phone, + GROUP_CONCAT(houses.house_name || '(' || hh.type || ')') AS rooms + FROM householders h + LEFT JOIN householders_type hh ON h.householder_id = hh.householder_id + LEFT JOIN houses ON hh.room_id = houses.room_id + WHERE h.type = '住户' {space_sub_sql} + GROUP BY h.householder_id, h.name, h.phone + ) {sub_sql} + """ + table_handler.query(f"SELECT COUNT(*) FROM ({query_sql})") + total = table_handler.cursor.fetchall()[0][0] + table_handler.query(f"{query_sql} {page_sql}") + res = table_handler.cursor.fetchall() + if res: + householder_info = [] + for i in res: + householder_info.append(AuthHouseholdersInfo( + householder_id=i[0], + name=i[1], + phone=decrypt_number(i[2]), + rooms=i[3] + ).__dict__) + return {"info": householder_info, "total": total} + else: + return {"info": [], "total": total} + + @staticmethod + def get_auth_users_info(table_handler: BaseTable, + search_type: Optional[str], search_key: Optional[str], user_type: Optional[str], + page: Optional[int] = None, limit: Optional[int] = None): + sub_sql_list = [] + if search_type is not None: + if search_type == "name": + sub_sql_list.append(f"{search_type} like '%{search_key}%'") + else: + sub_sql_list.append(f"{search_type} = '{search_key}'") + if user_type is not None: + sub_sql_list.append(f"user_type = '{user_type}'") + if sub_sql_list: + sub_sql = "AND " + " AND ".join(sub_sql_list) + else: + sub_sql = "" + if page is not None and limit is not None: + page_sql = f"LIMIT {limit} OFFSET {(page - 1) * limit}" + else: + page_sql = "" + query_sql = f""" + SELECT householder_id, name, sex, phone, card_type, card_id, user_type, auth_start, auth_expire + FROM householders + WHERE type != '住户' {sub_sql} + """ + table_handler.query(f"SELECT COUNT(*) FROM ({query_sql})") + total = table_handler.cursor.fetchall()[0][0] + table_handler.query(f"{query_sql} {page_sql}") + res = table_handler.cursor.fetchall() + if res: + householder_info = [] + for i in res: + householder_info.append(AuthUsersInfo( + user_id=i[0], name=i[1], sex=i[2], phone=decrypt_number(i[3]), + card_type=i[4], card_id=i[5], user_type=i[6], + start_datetime=i[7], expire_datetime=i[8] + ).__dict__) + return {"info": householder_info, "total": total} + else: + return {"info": [], "total": total} + + @staticmethod + def get_auth_user_info(table_handler: BaseTable, householder_id: int): + query_sql = """ + SELECT householder_id, name, sex, phone, face_url, card_type, card_id, user_type, auth_start, auth_expire + FROM householders + WHERE type != '住户' AND householder_id = ? + """ + table_handler.query(query_sql, (householder_id,)) + res = table_handler.cursor.fetchall() + if res: + householder_info = AuthUsersDetailInfo( + user_id=res[0][0], name=res[0][1], sex=res[0][2], phone=decrypt_number(res[0][3]), + face_url=f"http://{HOST_IP}:{PORT}{PREFIX_PATH}/{SUB_PATH}/{res[0][4]}" if res[0][4] else '', + card_type=res[0][5], card_id=res[0][6], user_type=res[0][7], + start_datetime=res[0][8], expire_datetime=res[0][9], + authorized_devices=DevicesTable.get_auth_device_info(table_handler, str(res[0][0])) + ).__dict__ + return householder_info + else: + raise InvalidException(f"无效ID {householder_id}") + + @staticmethod + def get_room_ids(table_handler: BaseTable, householder_id): + """获取住户的所有房产ID""" + table_handler.query( + """ + SELECT room_id + FROM householders_type + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return [i[0] for i in res] + else: + return [] + + @staticmethod + def get_rooms_by_householder_id(table_handler: BaseTable, householder_id): + """获取住户的所有房产ID""" + table_handler.query( + """ + SELECT h.house_name, type, ht.room_id + FROM householders_type ht + LEFT JOIN houses h ON h.room_id = ht.room_id + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return [list(i) for i in res] + else: + return [] + + @staticmethod + def get_householder_np_by_id(table_handler: BaseTable, householder_id: int): + table_handler.query( + """ + SELECT name, phone + FROM householders + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return {"name": res[0][0], "phone": decrypt_number(res[0][1])} + else: + raise InvalidException(f"无效ID {householder_id}") + + @staticmethod + def get_face_url(table_handler: BaseTable, householder_id: int): + table_handler.query( + """ + SELECT DISTINCT face_url + FROM householders + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return "" + + @staticmethod + def get_face_md5(table_handler: BaseTable, householder_id: int): + table_handler.query( + """ + SELECT DISTINCT face_md5 + FROM householders + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return "" + + @staticmethod + def get_householder_info_by_id(table_handler: BaseTable, householder_id: int): + table_handler.query( + """ + SELECT name, sex, phone + FROM householders + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + householder_info = { + "householder_id": householder_id, + "name": res[0][0], + "sex": res[0][1], + "phone": decrypt_number(res[0][2]), + "property_info": [] + } + table_handler.query( + """ + SELECT householders_type.room_id, house_name, area_name, building_name, unit_name, + householders_type.type, householder_count + FROM householders_type + LEFT JOIN houses ON houses.room_id = householders_type.room_id + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + for i in res: + householder_info["property_info"].append({ + "room_id": i[0], + "room_name": i[1], + "area_name": i[2] if i[2] else "", + "building_name": i[3] if i[3] else "", + "unit_name": i[4] if i[4] else "", + "type": i[5], + "household_count": i[6] + }) + return householder_info + return {} + + @staticmethod + def get_auth_householder_detail_info(table_handler: BaseTable, householder_id: int): + info = HouseholdersTable.get_householder_info_by_id(table_handler, householder_id) + rooms = HouseholdersTable.get_rooms_by_householder_id(table_handler, info["householder_id"]) + face_url = HouseholdersTable.get_face_url(table_handler, householder_id) + device_ids = DevicesTable.get_auth_device_ids(table_handler, str(householder_id)) + authorized_devices = [] + for device_id in device_ids: + start_date, expire_date = DevicesTable.get_auth_interval(table_handler, str(householder_id), device_id) + authorized_devices.append({ + "device_name": DevicesTable.get_device_name(table_handler, device_id), + "device_type": DevicesTable.get_device_scope_type(table_handler, device_id), + "open_type": ["人脸"], + "start_date": start_date, + "expire_date": expire_date, + "method": "自动授权", + "from": "系统" + }) + + resp = HouseholderDetailInfo( + householder_id=info["householder_id"], + name=info["name"], + phone=info["phone"], + face_url=f"http://{HOST_IP}:{PORT}{PREFIX_PATH}/{SUB_PATH}/{face_url}", + rooms=rooms, + authorized_devices=authorized_devices + ) + return resp.__dict__ + + @staticmethod + def update_householder_info(table_handler: BaseTable, + householder_id: int, + property_info: List[tuple[str, str]]): + # 查询对应住户关联的房产信息,比对更新数据中的房产信息 + update_room_ids = [i[0] for i in property_info] + update_types = [i[1] for i in property_info] + table_handler.query( + """ + SELECT room_id, type + FROM householders_type + WHERE householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + update_data = [] + delete_data = [] + insert_data = [[v, update_types[i]] for i, v in enumerate(update_room_ids)] + sqls = [] + params = [] + update_count = [] + for i in res: + try: + index = update_room_ids.index(i[0]) + if update_types[index] != i[1]: + update_data.append([i[0], update_types[index]]) + insert_data.remove([i[0], update_types[index]]) + except ValueError: + delete_data.append(i[0]) + table_handler.query( + """ + SELECT name, phone, face_url, auth_start, auth_expire + FROM householders + WHERE householder_id = ? + """, (householder_id,) + ) + householder_info = table_handler.cursor.fetchall()[0] + sc = ServicesCall() + face_item = AddFaceItem( + user_id=householder_id, + name=householder_info[0], + phone_number=householder_info[1], + face_url=householder_info[2], + start_date=householder_info[3].replace(' ', 'T') + ':00.000Z', + expire_date=householder_info[4].replace(' ', 'T') + ':00.000Z', + device_ids="" + ) + if update_data: + for data in update_data: + sqls.append( + """ + UPDATE householders_type + SET type = ?, update_datetime = ? + WHERE householder_id = ? AND room_id = ? + """ + ) + params.append((data[1], now_datetime_second(), householder_id, data[0])) + if insert_data: + for data in insert_data: + sqls.append( + """ + INSERT INTO householders_type + (room_id, householder_id, type, update_datetime) + VALUES (?, ?, ?, ?) + """ + ) + params.append((data[0], householder_id, data[1], now_datetime_second())) + old_count = HousesTable.get_householder_count(table_handler, data[0]) + update_count.append([data[0], old_count + 1]) + # TODO 同步增加对应设备中的人脸信息 + unit_id = HousesTable.get_unit_id_by_room_id(table_handler, data[0]) + device_ids = DevicesTable.get_device_ids_by_unit_id(table_handler, unit_id) + for device_id in device_ids: + face_item.device_ids = f"[{device_id}]" + _callback, code, msg = sc.add_face(device_id, face_item) + if _callback: + DevicesTable.insert_device_auth_record(table_handler, device_id, face_item.user_id, + face_item.start_date, face_item.expire_date) + logger.Logger.error(f"[update_householder_info] " + f"住户[{householder_id}]人脸增加失败,失败代码:{code},提示信息:{msg}") + if delete_data: + for room_id in delete_data: + sqls.append( + """ + DELETE FROM householders_type + WHERE householder_id = ? AND room_id = ? + """ + ) + params.append((householder_id, room_id)) + old_count = HousesTable.get_householder_count(table_handler, room_id) + update_count.append([room_id, old_count - 1]) + # TODO 获取房产-单元 同步移除对应设备中的人脸 + unit_id = HousesTable.get_unit_id_by_room_id(table_handler, room_id) + device_ids = DevicesTable.get_device_ids_by_unit_id(table_handler, unit_id) + for device_id in device_ids: + _callback, code, msg = sc.del_face(device_id, + DelFaceItem(user_id=householder_id, device_ids=f"[{device_id}]")) + if _callback: + DevicesTable.delete_invalid_auth_record(table_handler, device_id, str(householder_id)) + logger.Logger.error(f"[update_householder_info] " + f"住户[{householder_id}]人脸移除失败,失败代码:{code},提示信息:{msg}") + if len(update_room_ids) == 0: + sqls.append( + """ + DELETE FROM householders + WHERE householder_id = ? + """ + ) + params.append((householder_id,)) + table_handler.execute(sqls, params) + for i in update_count: + HousesTable.update_householder_count(table_handler, i[0], i[1]) + return {"status": True} + else: + return {"status": False, "message": "不存在对应住户或住户名下无关联房产"} + + @staticmethod + def update_auth_user_info(table_handler: BaseTable, obj: UpdateAuthUserInfo): + # 通用人员授权信息更新 + nd = now_datetime_second().replace(' ', 'T') + '.000Z' + sd = obj.start_datetime.replace(' ', 'T') + ':00.000Z' + ed = obj.expire_datetime.replace(' ', 'T') + ':00.000Z' + sc = ServicesCall() + face_item = AddFaceItem( + user_id=obj.user_id, + name=obj.name, + phone_number=obj.phone, + face_url=obj.face_url, + start_date=sd, + expire_date=ed, + device_ids="" + ) + old_face_url = HouseholdersTable.get_face_url(table_handler, obj.user_id) + # 查询当前人员授权情况 + table_handler.query( + """ + SELECT device_id, start_date, expire_date + FROM devices_auth + WHERE _id = ? + """, (obj.user_id,) + ) + res = table_handler.cursor.fetchall() + if res: + delete_data = [] # 移除权限 + insert_data = [[device_id, sd, ed] for device_id in obj.authorized_devices] # 新增权限 + for i in res: + if old_face_url == obj.face_url: # 对比人脸图片是否变更 + # 人脸无实际变更,根据权限变更重新赋权 + logger.Logger.debug("新旧人脸一致,按需赋权") + if [i[0], sd, ed] in insert_data and ed == i[2] and (sd < nd or sd == i[1]): + insert_data.remove([i[0], sd, ed]) + else: + delete_data.append(i[0]) + else: + # 若人脸变更直接移除所有人脸授权,重新赋权 + delete_data.append(i[0]) + delete_failed_ids = [] + delete_failed_codes = [] + delete_failed_msgs = [] + message = "" + if delete_data: + for device_id in delete_data: + _callback, code, msg = sc.del_face(device_id, DelFaceItem( + user_id=obj.user_id, device_ids=f"[{device_id}]")) + if _callback: + DevicesTable.delete_invalid_auth_record(table_handler, device_id, face_item.user_id) + else: + delete_failed_ids.append(device_id) + delete_failed_codes.append(code) + delete_failed_msgs.append(msg) + if 0 < len(delete_failed_ids) < len(delete_data): + message += f"部分设备 - {delete_failed_ids} 人脸移除失败,错误码:{delete_failed_codes}, {delete_failed_msgs}" + elif len(delete_failed_ids) == len(delete_data) != 0: + raise InvalidException("设备授权无法更新:人脸移除失败") + insert_failed_ids = [] + insert_failed_codes = [] + if insert_data: + for data in insert_data: + face_item.device_ids = f"[{data[0]}]" + _callback, code, msg = sc.add_face(data[0], face_item) + if _callback: + DevicesTable.insert_device_auth_record(table_handler, data[0], face_item.user_id, + face_item.start_date, face_item.expire_date) + else: + insert_failed_ids.append(data[0]) + insert_failed_codes.append(code) + if 0 < len(insert_failed_ids) < len(insert_data): + message += f"部分设备 - {delete_failed_ids} 人脸授权失败,错误码:{delete_failed_codes}" + elif len(delete_failed_ids) == len(delete_data) != 0: + raise InvalidException("设备授权无法更新:人脸下发失败") + if message != '': + return InvalidException(message) + else: + table_handler.execute( + """ + UPDATE householders + SET name = ?, sex = ?, phone= ?, + face_url = ?, face_md5= ?, + user_type = ?, card_type = ?, card_id = ?, + auth_start = ?, auth_expire = ?, update_datetime = ? + WHERE householder_id = ? + """, (obj.name, obj.sex, encrypt_number(obj.phone), + obj.face_url, get_file_md5(f"{FACES_DIR_PATH}/{obj.face_url}"), + obj.user_type, obj.card_type, obj.card_id, + sd, ed, now_datetime_second(), obj.user_id) + ) + return {"status": True} + else: + return {"status": False, "message": "不存在对应ID的授权"} + + @staticmethod + def update_householder_face(table_handler: BaseTable, householder_id: int, face_url: str): + """更新住户信息中的人脸地址 & 人脸地址同步到权限内的设备""" + new_md5 = get_file_md5(f"{FACES_DIR_PATH}/{face_url}") + if new_md5 != HouseholdersTable.get_face_md5(table_handler, householder_id): + table_handler.execute( + """ + UPDATE householders + SET face_url = ?, update_datetime = ? WHERE householder_id = ? + """, (face_url, now_datetime_second(), householder_id) + ) + # 查询住户关联单元,获取单元权限内的设备,下发人脸 + table_handler.query( + """ + SELECT h.householder_id, name, phone, GROUP_CONCAT(unit_id) as unit_ids + FROM householders h + LEFT JOIN householders_type ht ON ht.householder_id = h.householder_id + LEFT JOIN houses ON ht.room_id = houses.room_id + WHERE h.type = '住户' AND h.householder_id = ? + """, (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + sc = ServicesCall() + face_item = AddFaceItem( + user_id=res[0][0], + name=res[0][1], + phone_number=decrypt_number(res[0][2]), + face_url=face_url, + device_ids="" + ) + for unit_id in res[0][3].split(','): + device_ids = DevicesTable.get_device_ids_by_unit_id(table_handler, unit_id) + for device_id in device_ids: + face_item.device_ids = f"[{device_id}]" + _callback, code, msg = sc.add_face(device_id, face_item) + if _callback: + DevicesTable.insert_device_auth_record(table_handler, device_id, face_item.user_id, + face_item.start_date, face_item.expire_date) + return {"status": True} + else: + logger.Logger.debug("新旧人脸一致,不做任何变更") + return {"status": True} + + @staticmethod + def update_user_info(table_handler: BaseTable, householder_id: int, face_url: str): + """更新通用人员信息中的人脸地址 & 人脸地址同步到下发过的设备中""" + table_handler.execute( + """ + UPDATE householders + SET face_url = ? WHERE householder_id = ? + """, (face_url, householder_id) + ) + # 查询人员授权过的设备,下发人脸 + device_ids = DevicesTable.get_auth_device_ids(table_handler, str(householder_id)) + name_phone = HouseholdersTable.get_householder_np_by_id(table_handler, householder_id) + sc = ServicesCall() + face_item = AddFaceItem( + user_id=householder_id, + name=name_phone["name"], + phone_number=name_phone["phone"], + face_url=face_url, + device_ids="" + ) + for device_id in device_ids: + face_item.device_ids = f"[{device_id}]" + _callback, code, _ = sc.add_face(device_id, face_item) + if _callback: + DevicesTable.insert_device_auth_record(table_handler, device_id, face_item.user_id, + face_item.start_date, face_item.expire_date) + return {"status": True} + + @staticmethod + def delete_householder_info(table_handler: BaseTable, householder_id: int): + # 确认住户ID有效 + if not HouseholdersTable.exists_householder_id(table_handler, householder_id): + raise InvalidException("不存在对应住户ID") + # 1. 查询当前住户授权的授权的设备ID + device_ids = DevicesTable.get_auth_device_ids(table_handler, str(householder_id)) + # 2. 对所有授权设备进行授权移除操作 + if device_ids: + sc = ServicesCall() + _callback, code, msg = sc.del_face(device_ids[0], DelFaceItem( + user_id=str(householder_id), + device_ids=str(device_ids) + )) + if not _callback: + raise InvalidException(f"住户人脸授权移除失败,错误码{code},{msg}") + DevicesTable.delete_householder_all_auth(table_handler, str(householder_id)) + # 3. 同步更新住户名下房产入住人数,并移除住户记录 + room_ids = HouseholdersTable.get_room_ids(table_handler, householder_id) + for room_id in room_ids: + old_count = HousesTable.get_householder_count(table_handler, room_id) + HousesTable.update_householder_count(table_handler, room_id, old_count - 1) + sqls = [ + "DELETE FROM householders WHERE householder_id = ?", + "DELETE FROM householders_type WHERE householder_id = ?" + ] + table_handler.execute(sqls, [(householder_id,), (householder_id,)]) + return {"status": True} + + @staticmethod + def delete_auth_user_info(table_handler: BaseTable, user_id: int): + # 确认通用用户ID有效 + if not HouseholdersTable.exists_user_id(table_handler, user_id): + raise InvalidException("不存在对应通用用户ID") + # 1. 查询当前住户授权的授权的设备ID + device_ids = DevicesTable.get_auth_device_ids(table_handler, str(user_id)) + # 2. 对所有授权设备进行授权移除操作 + if device_ids: + sc = ServicesCall() + _callback, code, msg = sc.del_face(device_ids[0], DelFaceItem( + user_id=str(user_id), + device_ids=str(device_ids) + )) + if not _callback: + raise InvalidException(f"住户人脸授权移除失败,错误码{code},{msg}") + DevicesTable.delete_householder_all_auth(table_handler, str(user_id)) + sqls = [ + "DELETE FROM householders WHERE householder_id = ?", + "DELETE FROM householders_type WHERE householder_id = ?" + ] + table_handler.execute(sqls, [(user_id,), (user_id,)]) + return {"status": True} + + @staticmethod + def delete_type(table_handler: BaseTable, householder_id: int): + table_handler.execute( + """ + DELETE FROM householders_type + WHERE householder_id = ? + """, (householder_id,) + ) + + @staticmethod + def exists_householder_phone(table_handler: BaseTable, phone: str) -> bool: + """查询住户手机号码是否存在""" + table_handler.query( + """ + SELECT householder_id + FROM householders + WHERE type = '住户' AND phone = ? + """, (encrypt_number(phone),) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_user_phone(table_handler: BaseTable, phone: str): + """查询非住户手机号码是否存在""" + table_handler.query( + """ + SELECT householder_id + FROM householders + WHERE type != '住户' AND phone = ? + """, (encrypt_number(phone),) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_id(table_handler: BaseTable, householder_id: int) -> bool: + """查询ID是否存在""" + table_handler.query( + """ + SELECT householder_id + FROM householders + WHERE householder_id = ? + """, + (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_householder_id(table_handler: BaseTable, householder_id: int) -> bool: + """查询住户ID是否存在""" + table_handler.query( + """ + SELECT householder_id + FROM householders + WHERE type = '住户' AND householder_id = ? + """, + (householder_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_user_id(table_handler: BaseTable, user_id: int) -> bool: + """查询非住户ID是否存在""" + table_handler.query( + """ + SELECT householder_id + FROM householders + WHERE type != '住户' AND householder_id = ? + """, + (user_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_householder_type(table_handler: BaseTable, householder_id: str, room_id: str) -> bool: + """查询住户-房产关联信息是否存在""" + table_handler.query( + """ + SELECT householder_id + FROM householders_type + WHERE householder_id = ? AND room_id = ? + """, + (householder_id, room_id) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False diff --git a/SCLP/models/houses.py b/SCLP/models/houses.py new file mode 100644 index 0000000..05487d3 --- /dev/null +++ b/SCLP/models/houses.py @@ -0,0 +1,303 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 房产信息表查&改 +""" +import csv +import json + +import os +import traceback + +from utils import logger +from utils.database import BaseTable +from utils.misc import now_datetime_second, extract_fixed_length_number + + +class HousesTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='houses'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE houses ( + room_id TEXT UNIQUE, + house_name TEXT UNIQUE, + project_id TEXT, + project_name TEXT, + area_id TEXT, + area_name TEXT, + building_id TEXT, + building_name TEXT, + unit_id TEXT, + unit_name TEXT, + floor_id TEXT, + floor_name TEXT, + room_name TEXT, + householder_count INTEGER, + update_datetime TEXT + ) + """ + ) + # 根据导出数据生成内置数据 + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/houses.csv") + csv_file = open(init_config_path, "w", encoding="utf8") + csv_file.write( + "房屋ID, 房屋编号, 项目ID, 所属项目, 区域ID, 所属区域, 楼栋ID, 所属楼栋, 单元ID, 所属单元, 楼层ID, 所属楼层, 房间号, 住户数量\n") + room_info_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/ExportData/room.txt") + if os.path.exists(room_info_path): + with open(room_info_path, "r", encoding="utf8") as f: + file_content = f.read() + file_content = file_content.replace("ISODate(", "").replace(")", "").replace("'", "\"") + room_info = json.loads(file_content) + for item in room_info: + room_id = item.get("_id", "") + project_id = item["project_id"] + project = item["project_name"] + area_id = item.get("area_id", "") + area = item.get("area_name", "") + building_id = item.get("build_id", "") + building = item.get("building_name", "") + unit_id = item.get("unit_id", "") + unit = item.get("unit_name", "") + floor_id = item.get("floor_id", "") + floor = item.get("floor_name", "") + room = item.get("num", "") + wait_write = [room_id, + '-'.join([i for i in [project, area, building, unit, floor, room] if i != '']), + project_id, project, area_id, area, building_id, building, + unit_id, unit, floor_id, floor, room, str(len(item.get("households", [])))] + csv_file.write(', '.join(wait_write) + "\n") + csv_file.flush() + csv_file.close() + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 14: + for row in csvreader: + room_id = row[0].strip() + house_name = row[1].strip() + project_id = row[2].strip() + project_name = row[3].strip() + area_id = row[4].strip() if row[4].strip() else None + area_name = row[5].strip() if row[5].strip() else None + building_id = row[6].strip() if row[6].strip() else None + building_name = row[7].strip() if row[7].strip() else None + unit_id = row[8].strip() if row[8].strip() else None + unit_name = row[9].strip() if row[9].strip() else None + floor_id = row[10].strip() if row[10].strip() else None + floor_name = row[11].strip() if row[11].strip() else None + room_name = row[12].strip() + householder_count = int(row[13].strip()) + update_datetime = now_datetime_second() + data.append((room_id, house_name, project_id, project_name, area_id, area_name, + building_id, building_name, unit_id, unit_name, floor_id, floor_name, + room_name, householder_count, update_datetime)) + table_handler.executemany( + f""" + INSERT INTO houses + (room_id, house_name, project_id, project_name, area_id, area_name, + building_id, building_name, unit_id, unit_name, floor_id, floor_name, + room_name, householder_count, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (room_id) DO NOTHING + """, + data + ) + + @staticmethod + def get_house_detail_info(table_handler: BaseTable, room_id): + """根据房屋ID获取房产详细信息""" + table_handler.query( + """ + SELECT room_id, project_id, room_name, project_name + FROM houses + WHERE room_id = ? + """, (room_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return { + "id": res[0][0], + "project_id": res[0][1], + "name": res[0][2], + "type": 1, + "subtype": 0, + "project_name": res[0][3] + } + else: + return None + + @staticmethod + def get_unit_id_by_room_id(table_handler: BaseTable, room_id): + """根据房屋ID获取房产详细信息""" + table_handler.query( + """ + SELECT unit_id + FROM houses + WHERE room_id = ? + """, (room_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return None + + @staticmethod + def get_house_ty_info(table_handler: BaseTable, project_id: str, room_id: str) -> dict | None: + """查询房产真实信息""" + table_handler.query( + """ + SELECT houses.room_id, house_name, project_id, corp_id, + building_name, unit_name, floor_name, room_name + FROM houses + LEFT JOIN subsystem ON houses.room_id = subsystem.room_id + WHERE project_id = ? AND houses.room_id = ? + """, (project_id, room_id) + ) + res = table_handler.cursor.fetchall() + if res: + building = extract_fixed_length_number(res[0][4]) + unit = extract_fixed_length_number(res[0][5]) + floor = extract_fixed_length_number(res[0][6]) + room = extract_fixed_length_number(res[0][7]) + ty_house_id = f"{building}{unit}{floor}{room}" + return { + "id": res[0][0], + "house_name": res[0][1], + "ty_house_id": ty_house_id, + "project_id": res[0][2], + "corp_id": res[0][3] + } + else: + return None + + @staticmethod + def get_items_by_dynamic_item(table_handler: BaseTable, query_target: str, query_item: dict): + sub_sql_list = [] + for key, value in query_item.items(): + if isinstance(value, int): + sub_sql_list.append(f"{key}_id = {value}") + else: + sub_sql_list.append(f"{key}_id = '{value}'") + sub_sql = "" if len(sub_sql_list) == 0 else "WHERE " + " and ".join(sub_sql_list) + try: + table_handler.query( + f""" + SELECT DISTINCT {query_target}_id, {query_target}_name + FROM houses {sub_sql} + """ + ) + res = table_handler.cursor.fetchall() + if res: + item_list = [{"_id": i[0], "_name": i[1]} for i in res if i[0] is not None] + return item_list + else: + return [] + except Exception as e: + logger.Logger.error(f"{type(e).__name__}, {e}") + if logger.DEBUG: + traceback.print_exc() + return [] + + @staticmethod + def get_householder_count(table_handler: BaseTable, room_id: str): + table_handler.query( + """ + SELECT householder_count + FROM houses + WHERE room_id = ? + """, + (room_id,), + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return None + + @staticmethod + def update_householder_count(table_handler: BaseTable, room_id: str, householder_count: int): + table_handler.execute( + """ + UPDATE houses SET + householder_count=?, + update_datetime=? + WHERE room_id=? + """, + ( + householder_count, + now_datetime_second(), + room_id + ), + ) + return True + + @staticmethod + def get_unit_ids(table_handler: BaseTable, building_id: str): + table_handler.execute( + """ + SELECT DISTINCT unit_id + FROM houses + WHERE building_id = ? + """, (building_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return [i[0] for i in res] + else: + return [] + + @staticmethod + def exists(table_handler: BaseTable, room_id: str): + table_handler.query( + """ + SELECT house_name + FROM houses + WHERE room_id = ? + """, + (room_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def building_exists(table_handler: BaseTable, building_id: str): + table_handler.query( + """ + SELECT DISTINCT building_id + FROM houses + WHERE building_id = ? + """, + (building_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def unit_exists(table_handler: BaseTable, unit_id: str): + table_handler.query( + """ + SELECT DISTINCT unit_id + FROM houses + WHERE unit_id = ? + """, + (unit_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False diff --git a/SCLP/models/parkinglots.py b/SCLP/models/parkinglots.py new file mode 100644 index 0000000..da89052 --- /dev/null +++ b/SCLP/models/parkinglots.py @@ -0,0 +1,1019 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 记录车场信息,在初始化时会初始化csv进行覆盖 +""" +import csv +import os +import time +from typing import Optional, List +import re + +from pydantic import BaseModel, field_validator, Field + +from device.call import ServicesCall, UpdateFixedCardItem, VehicleRegistrationItem +from models.devices import DevicesTable +from utils.database import BaseTable, get_table_handler +from utils.misc import (now_datetime_second, InvalidException, now_tz_datetime, millisecond_timestamp2tz, + encrypt_number, decrypt_number) + + +class AddParkingLot(BaseModel): + parkinglot_number: str = Field(description="车场编号") + parkinglot_name: str = Field(description="车场名称") + factory_name: Optional[str] = Field(None, description="供应商") + + @field_validator("parkinglot_number") + def check_parkinglot_number(cls, value): + th = get_table_handler() + if not ParkinglotsTable.exists(th, value, 'parkinglot'): + raise InvalidException("停车场编号无效") + if ParkinglotsTable.is_number_added(th, value, 'parkinglot'): + raise InvalidException("停车场编号已添加") + return value + + +class AddCarsCard(BaseModel): + car_type: str + owner_name: str + owner_phone: str + start_datetime: str + expire_datetime: str + car_ids: List[str] + associated_parking_space: str = "" + + @field_validator("car_type") + def check_car_type(cls, value): + types = ["业主车辆", "员工车辆", "访客车辆", "其他车辆"] + if value not in types: + raise InvalidException(f"请提供正确的车辆类型:{types}") + return value + + @field_validator("owner_phone") + def check_owner_phone(cls, value): + pattern = re.compile(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$') + if pattern.search(value) is None: + raise InvalidException("请提供正确的手机号码") + th = get_table_handler() + if not ParkinglotsTable.exists_phone(th, value): + return value + else: + raise InvalidException(f"车辆所有人电话号码已存在") + + @field_validator("car_ids") + def check_car_ids(cls, value): + th = get_table_handler() + for car_id in value: + if ParkinglotsTable.get_car_id_auth_status(th, car_id)["auth_status"]: + raise InvalidException(f"车牌号 {car_id} 已授权,请关联未授权的车牌号") + return value + + +class UpdateCarsCard(BaseModel): + auth_id: int + car_type: str + owner_name: str + owner_phone: str + start_datetime: str + expire_datetime: str + car_ids: List[str] + associated_parking_space: str + + @field_validator("auth_id") + def check_auth_id(cls, value): + th = get_table_handler() + if not ParkinglotsTable.exists_auth_id(th, value): + raise InvalidException(f"授权ID {value} 不存在") + else: + return value + + @field_validator("car_type") + def check_car_type(cls, value): + types = ["业主车辆", "员工车辆", "访客车辆", "其他车辆"] + if value not in types: + raise InvalidException(f"请提供正确的车辆类型:{types}") + return value + + @field_validator("start_datetime") + def check_datetime(cls, value): + if "T" in value and "Z" in value: + return value + pattern = r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$' + if re.match(pattern, value): + return value + else: + return InvalidException(f"请提供正确的时间格式:YYYY-MM-DD HH:MM") + + @field_validator("owner_phone") + def check_owner_phone(cls, value): + pattern = re.compile(r'^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$') + if pattern.search(value) is None: + raise InvalidException("请提供正确的手机号码") + return value + + +class GetCarsInfo(BaseModel): + search_type: str + search_key: str + page: Optional[int] = None + limit: Optional[int] = None + + @field_validator("search_type") + def check_search_type(cls, value): + types = { + "车牌号码": "car_id", + "车辆所有人": "owner", + "联系电话": "phone" + } + if value not in types.keys(): + raise InvalidException(f"请提供正确的搜索类型 {list(types.keys())}") + else: + return types[value] + + +class ParkinglotsTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + # parkinglots 车场信息表 + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), + "data/InitialData/parkinglots.csv") + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='parkinglots'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE parkinglots ( + id TEXT, + number TEXT, + name TEXT, + type TEXT, + parkinglot_id TEXT, + area_id TEXT, + channel_id TEXT, + factory_name TEXT, + channel_type TEXT, + is_online TEXT, + gate_status TEXT, + running_status TEXT, + update_datetime TEXT, + PRIMARY KEY (id, type) + ) + """ + ) + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 12: + for row in csvreader: + # id,number,name,type,area_id,channel_id,factory_name,channel_type,is_online,gate_status,running_status + _id = row[0].strip() + number = row[1].strip() if row[1].strip() else None + name = row[2].strip() + _type = row[3].strip() + parkinglot_id = row[4].strip() + area_id = row[5].strip() if row[5].strip() else None + channel_id = row[6].strip() if row[6].strip() else None + factory_name = row[7].strip() if row[7].strip() else None + channel_type = row[8].strip() if row[8].strip() else None + is_online = row[9].strip() if row[9].strip() else None + gate_status = row[10].strip() if row[10].strip() else None + running_status = row[11].strip() if row[11].strip() else None + data.append((_id, number, name, _type, parkinglot_id, area_id, channel_id, factory_name, + channel_type, is_online, gate_status, running_status, now_datetime_second())) + table_handler.executemany( + f""" + INSERT OR IGNORE INTO parkinglots + (id, number, name, type, parkinglot_id, area_id, channel_id, factory_name, channel_type, + is_online, gate_status, running_status, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, data + ) + + # cars_card 车辆月卡信息表 + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='cars_card'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE cars_card ( + auth_id INTEGER PRIMARY KEY AUTOINCREMENT, + card_id TEXT UNIQUE, + car_type TEXT, + owner TEXT, + phone TEXT UNIQUE, + start_datetime TEXT, + expire_datetime TEXT, + associated_parking_space TEXT, + update_datetime TEXT + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/cars_card.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 7: + for row in csvreader: + # auth_id,car_type,owner,phone,start_datetime,expire_datetime + auth_id = row[0].strip() + card_id = row[1].strip() + car_type = row[2].strip() + owner = row[3].strip() + phone = encrypt_number(row[4].strip()) + start_datetime = row[5].strip() + expire_datetime = row[6].strip() + data.append((auth_id, card_id, car_type, owner, phone, + start_datetime, expire_datetime, now_datetime_second())) + table_handler.executemany( + f""" + INSERT OR IGNORE INTO cars_card + (auth_id, card_id, car_type, owner, phone, start_datetime, expire_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, data + ) + + # cars_auth 授权关联信息表 + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='cars_auth'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE cars_auth ( + auth_id INTEGER, + car_id TEXT, + car_type TEXT, + registration_id TEXT, + update_datetime TEXT, + PRIMARY KEY (auth_id, car_id) + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/cars_auth.csv") + if os.path.exists(init_config_path): + with (open(init_config_path, newline='', encoding='utf8') as csvfile): + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 3: + for row in csvreader: + # auth_id,car_id,car_type + auth_id = row[0].strip() + car_id = row[1].strip() + car_type = row[2].strip() + registration_id = str(int(time.time() * (10 ** 7))) + data.append((auth_id, car_id, car_type, registration_id, now_datetime_second())) + table_handler.executemany( + f""" + INSERT OR IGNORE INTO cars_auth + (auth_id, car_id, car_type, registration_id, update_datetime) + VALUES (?, ?, ?, ?, ?) + """, data + ) + # 移除 cars_card & cars_auth 表中过期的授权记录 + now_datetime = now_datetime_second()[:-3] + table_handler.query( + """ + SELECT auth_id FROM cars_card + WHERE expire_datetime < ? + """, (now_datetime,) + ) + res = table_handler.cursor.fetchall() + if res: + for i in res: + table_handler.execute("DELETE FROM cars_card WHERE auth_id = ?", (i[0],)) + table_handler.execute("DELETE FROM cars_auth WHERE auth_id = ?", (i[0],)) + + @staticmethod + def get_parkinglots_id(table_handler: BaseTable, number: str, _type: str): + """根据编码和_type查询停车场信息中的空间ID""" + table_handler.query("SELECT id from parkinglots WHERE number = ? AND type = ?", (number, _type)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + raise InvalidException(f"{_type} 中不存在编码 {number} ") + + @staticmethod + def get_auth_id_by_card_id(table_handler: BaseTable, card_id: str): + """根据月卡ID查询车辆授权ID""" + table_handler.query("SELECT auth_id from cars_card WHERE card_id = ?", (card_id,)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + raise InvalidException(f"月卡ID {card_id} 不存在 ") + + @staticmethod + def get_parkinglots_info(table_handler: BaseTable): + table_handler.query( + """ + SELECT id, name, factory_name + FROM parkinglots + WHERE type = 'parkinglot' AND running_status is not null + """ + ) + res = table_handler.cursor.fetchall() + if res: + info = [] + for i in res: + info.append({ + "number": i[0], + "name": i[1], + "factory_name": i[2] if i[2] else "" + }) + return {"info": info} + else: + return {"info": []} + + @staticmethod + def get_showed_parkinglot_area_info(table_handler: BaseTable): + table_handler.query( + """ + SELECT area_number, name, parkinglot_name, '逻辑区域' as area_type, pc.channel_count, pc.channel_name + FROM (SELECT id area_id, number area_number, name, parkinglot_name + FROM parkinglots pa + LEFT JOIN (SELECT id parkinglot_number, name parkinglot_name + FROM parkinglots + WHERE type = 'parkinglot' AND running_status IS NOT NULL) + ON parkinglot_number = pa.parkinglot_id + WHERE type = 'area' AND parkinglot_number IS NOT NULL) pa + LEFT JOIN (SELECT area_id, count(id) channel_count, GROUP_CONCAT(name) channel_name + FROM parkinglots + WHERE type = 'channel' + GROUP BY area_id) pc ON pc.area_id = pa.area_id + """ + ) + res = table_handler.cursor.fetchall() + if res: + info = [] + for i in res: + info.append({ + "area_number": i[0], + "area_name": i[1], + "parkinglot_name": i[2], + "area_type": i[3], + "channel_count": i[4], + "channel_name": i[5] + }) + return {"info": info} + else: + return {"info": []} + + @staticmethod + def get_showed_parkinglot_channel_info(table_handler: BaseTable): + table_handler.query( + """ + SELECT number, name, channel_type, parkinglot_name + FROM parkinglots pa + LEFT JOIN (SELECT id parkinglot_number, name parkinglot_name + FROM parkinglots + WHERE type = 'parkinglot' AND running_status IS NOT NULL) + ON parkinglot_number = pa.parkinglot_id + WHERE type = 'channel' AND parkinglot_number IS NOT NULL + """ + ) + res = table_handler.cursor.fetchall() + if res: + info = [] + for i in res: + info.append({ + "channel_number": i[0], + "channel_name": i[1], + "channel_type": i[2], + "parkinglot_name": i[3] + }) + return {"info": info} + else: + return {"info": []} + + @staticmethod + def get_showed_parkinglot_gate_info(table_handler: BaseTable): + table_handler.query( + """ + SELECT number, name, is_online, gate_status, running_status, parkinglot_name, channel_name + FROM parkinglots pa + LEFT JOIN (SELECT id parkinglot_number, name parkinglot_name + FROM parkinglots + WHERE type = 'parkinglot' AND running_status IS NOT NULL) + ON parkinglot_number = pa.parkinglot_id + LEFT JOIN (SELECT id channel_id, name channel_name + FROM parkinglots + WHERE type = 'channel') pc + ON pc.channel_id = pa.channel_id + WHERE type = 'gate' AND parkinglot_number IS NOT NULL + """ + ) + res = table_handler.cursor.fetchall() + info = [] + if res: + for i in res: + info.append({ + "gate_number": i[0], + "gate_name": i[1], + "is_online": i[2], + "gate_status": i[3], + "running_status": i[4], + "parkinglot_name": i[5], + "channel_name": i[6] if i[6] else "" + }) + return {"info": info} + + @staticmethod + def get_cars_info(table_handler: BaseTable, search_type: Optional[str], search_key: Optional[str], + page: Optional[int] = None, limit: Optional[int] = None): + sub_sql_list = [] + if search_type is not None: + if search_type == "owner": + sub_sql_list.append(f"owner like '%{search_key}%'") + elif search_type == "car_id": + sub_sql_list.append(f"car_ids like '%{search_key}%'") + else: + sub_sql_list.append(f"{search_type} = '{search_key}'") + if len(sub_sql_list) > 0: + sub_sql = "WHERE " + " AND ".join(sub_sql_list) + else: + sub_sql = "" + if page is not None and limit is not None: + page_sql = f"LIMIT {limit} OFFSET {(page - 1) * limit}" + else: + page_sql = "" + query_sql = f""" + SELECT cars_card.auth_id, car_ids, car_type, owner, phone, start_datetime, expire_datetime + FROM cars_card + LEFT JOIN (SELECT auth_id, GROUP_CONCAT(car_id) car_ids + FROM cars_auth GROUP BY auth_id) ca + ON ca.auth_id = cars_card.auth_id {sub_sql} + """ + table_handler.query(f"SELECT COUNT(*) FROM ({query_sql})") + total = table_handler.cursor.fetchall()[0][0] + table_handler.query(f"{query_sql} {page_sql}") + res = table_handler.cursor.fetchall() + info = [] + if res: + for i in res: + info.append({ + "auth_id": i[0], + "car_ids": i[1], + "car_type": i[2], + "owner_name": i[3], + "owner_phone": decrypt_number(i[4]), + "start_datetime": i[5], + "expire_datetime": i[6] + }) + return {"info": info, "total": total} + + @staticmethod + def get_auth_info(table_handler: BaseTable, auth_id: int): + table_handler.query( + """ + SELECT cars_card.auth_id, car_type, owner, phone, start_datetime, expire_datetime, + car_ids, associated_parking_space + FROM cars_card + LEFT JOIN (SELECT auth_id, GROUP_CONCAT(car_id) car_ids FROM cars_auth GROUP BY auth_id) ca + ON ca.auth_id = cars_card.auth_id + WHERE cars_card.auth_id = ? + """, (auth_id,) + ) + res = table_handler.cursor.fetchall() + if res: + car_ids = [i for i in res[0][6].split(',')] + return { + "auth_id": res[0][0], + "car_type": res[0][1], + "owner_name": res[0][2], + "owner_phone": decrypt_number(res[0][3]), + "start_datetime": res[0][4], + "expire_datetime": res[0][5], + "car_ids": car_ids, + "car_count": len(car_ids), + "associated_parking_space": res[0][7] if res[0][7] else '' + } + else: + raise InvalidException("授权ID不存在") + + @staticmethod + def get_car_id_auth_status(table_handler: BaseTable, car_id: str): + now_datetime = now_datetime_second()[:-3] + table_handler.query( + """ + SELECT cars_auth.auth_id, car_id, start_datetime, expire_datetime + FROM cars_auth + LEFT JOIN cars_card ON cars_card.auth_id = cars_auth.auth_id + WHERE car_id = ? AND expire_datetime > ? + """, (car_id, now_datetime) + ) + res = table_handler.cursor.fetchall() + if res: + return {"auth_status": True} + else: + return {"auth_status": False} + + @staticmethod + def get_auth_info_by_auth_id(table_handler: BaseTable, auth_id): + table_handler.query( + """ + SELECT + (SELECT card_id FROM cars_card WHERE auth_id = ?) AS card_id, + (SELECT owner FROM cars_card WHERE auth_id = ?) AS owner_name, + (SELECT phone FROM cars_card WHERE auth_id = ?) AS owner_phone, + (SELECT car_type FROM cars_card WHERE auth_id = ?) AS car_type, + (SELECT GROUP_CONCAT(device_id) FROM devices_auth WHERE _id = ? AND record_type = '车行') AS device_ids, + (SELECT GROUP_CONCAT(car_id) FROM cars_auth WHERE auth_id = ?) AS car_ids, + (SELECT GROUP_CONCAT(cars_auth.registration_id) FROM cars_auth WHERE auth_id = ?) AS reg_ids + """, (auth_id, auth_id, auth_id, auth_id, auth_id, auth_id, auth_id) + ) + res = table_handler.cursor.fetchall() + card_id, owner_name, owner_phone, car_type = None, None, None, None + device_ids = [] + car_ids = [] + reg_ids = [] + if res: + if res[0][0] is not None: + card_id = res[0][0] + if res[0][1] is not None: + owner_name = res[0][1] + if res[0][2] is not None: + owner_phone = decrypt_number(res[0][2]) + if res[0][3] is not None: + car_type = res[0][3] + if res[0][4] is not None: + device_ids = [i for i in res[0][4].split(',')] + if res[0][5] is not None: + car_ids = [i for i in res[0][5].split(',')] + if res[0][6] is not None: + reg_ids = [i for i in res[0][6].split(',')] + return card_id, owner_name, owner_phone, car_type, device_ids, car_ids, reg_ids + + @staticmethod + def get_card_id_by_auth_id(table_handler: BaseTable, auth_id: int): + table_handler.query("SELECT card_id FROM cars_card WHERE auth_id = ?", (auth_id,)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + raise InvalidException(f"授权ID {auth_id} 不存在") + + @staticmethod + def get_car_card_interval(table_handler: BaseTable, card_id: str): + """获取对应月卡的有效期间""" + table_handler.query("SELECT start_datetime, expire_datetime FROM cars_card WHERE card_id = ?", (card_id,)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0], res[0][1] + else: + raise InvalidException(f"月卡ID {card_id} 未查询到对应有效期记录") + + @staticmethod + def get_auth_id_interval(table_handler: BaseTable, auth_id: int): + """获取对应月卡的有效期间""" + table_handler.query("SELECT start_datetime, expire_datetime FROM cars_card WHERE auth_id = ?", (auth_id,)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0], res[0][1] + else: + raise InvalidException(f"授权ID {auth_id} 未查询到对应有效期记录") + + @staticmethod + def get_car_reg_id(table_handler: BaseTable, car_id: str): + table_handler.query("SELECT registration_id FROM cars_auth WHERE car_id = ?", (car_id,)) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + raise InvalidException(f"Car ID {car_id} 未查询到对应注册ID") + + @staticmethod + def add_update_cars_auth_record(table_handler: BaseTable, auth_id: int, car_ids: list, reg_ids: list): + cars_auth_data = [] + for i, car_id in enumerate(car_ids): + car_type = "新能源" if len(car_id) == 7 else "普通" + nt = now_datetime_second() + cars_auth_data.append((auth_id, car_id, car_type, reg_ids[i], nt, reg_ids[i], nt)) + table_handler.executemany( + """ + INSERT INTO cars_auth + (auth_id, car_id, car_type, registration_id, update_datetime) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT (auth_id, car_id) + DO UPDATE SET registration_id = ?, update_datetime = ? + """, cars_auth_data + ) + + @staticmethod + def add_car_card(table_handler: BaseTable, card_id: str, device_ids: list, reg_ids: list, + obj: AddCarsCard | UpdateCarsCard): + """增加车辆授权的月卡信息,插入到 cars_card & cars_auth & devices_auth""" + table_handler.execute( + """ + INSERT INTO cars_card + (card_id, car_type, owner, phone, associated_parking_space, + start_datetime, expire_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (card_id, obj.car_type, obj.owner_name, encrypt_number(obj.owner_phone), obj.associated_parking_space, + obj.start_datetime, obj.expire_datetime, now_datetime_second()) + ) + auth_id = ParkinglotsTable.get_auth_id_by_card_id(table_handler, card_id) + # cars_auth + ParkinglotsTable.add_update_cars_auth_record(table_handler, auth_id, obj.car_ids, reg_ids) + # devices_auth + st = obj.start_datetime + et = obj.expire_datetime + for device_id in device_ids: + DevicesTable.add_update_device_auth(table_handler, device_id, [auth_id], "车行", st, et) + + @staticmethod + def add_car_card_auth(table_handler: BaseTable, obj: AddCarsCard): + """注册车辆 & 新建月卡 & 保留记录""" + # 1. 获取车场系统 device_id + device_ids = DevicesTable.get_device_ids(table_handler, filter_name="停车") + if len(device_ids) > 0: + sc = ServicesCall() + vri = VehicleRegistrationItem( + car_plate_no="", + registration_id="", + owner_id=obj.owner_phone, + owner_name=obj.owner_name, + registration_time=now_tz_datetime(), + begin_time=obj.start_datetime, + action="insert", + registration_type='0' if "业主" in obj.car_type else '1' + ) + base_reg_id = int(time.time() * (10 ** 7)) + reg_ids = [str(base_reg_id + 1) for _ in obj.car_ids] # 不同设备、相同车牌 相同注册ID + success_device_ids_0 = [] + for device_index, device_id in enumerate(device_ids): + # 2. 注册车辆,只要存在失败的设备,就对所有之前成功的设备进行回滚 + for car_id_index, car_id in enumerate(obj.car_ids): + vri.car_plate_no = car_id + vri.registration_id = reg_ids[car_id_index] + _callback, code, msg = sc.vehicle_registration(device_id, vri) + if not _callback and "UNIQUE" not in msg: + # 执行回滚 + if len(success_device_ids_0) > 1: + for success_device_id in success_device_ids_0: + for j, _id in enumerate(obj.car_ids): + vri.car_plate_no = _id + vri.registration_id = reg_ids[j] + vri.action = "delete" + sc.vehicle_registration(success_device_id, vri) + raise InvalidException(f"车辆注册失败,设备:{device_id},错误码:{code},{msg},已执行回滚") + else: + if "UNIQUE" in msg: + vri.action = "update" + _callback, code, msg = sc.vehicle_registration(device_id, vri) + success_device_ids_0.append(device_id) + + # 3. 新建月卡 + multi_car_id = '|'.join(obj.car_ids) + t = str(int(time.time() * 1000)) + card_id = t[:8] + '-' + t[8:12] + '-' + t[12:] + "000-0000-000000000000" # 不同设备、相同车牌 相同月卡ID + ufci = UpdateFixedCardItem( + vehicle_owner_id=obj.owner_phone, + vehicle_owner_name=obj.owner_name, + vehicle_owner_phone=obj.owner_phone, + plate_number=multi_car_id, + effective_date_begin=obj.start_datetime, + effective_date_end=obj.expire_datetime, + action="insert", + card_id=card_id + ) + success_device_ids_1 = [] + for device_index, device_id in enumerate(device_ids): + _callback, code, msg = sc.update_fixed_card(device_id, ufci) + if not _callback: + # 月卡新建回滚 + if len(success_device_ids_1) > 1: + for success_device_id in success_device_ids_1: + ufci.action = "delete" + sc.update_fixed_card(success_device_id, ufci) + # 车辆注册回滚 + if len(success_device_ids_0) > 1: + for success_device_id in success_device_ids_0: + for j, _id in enumerate(obj.car_ids): + vri.car_plate_no = _id + vri.registration_id = reg_ids[j] + vri.action = "delete" + sc.vehicle_registration(success_device_id, vri) + raise InvalidException(f"车辆新建月卡失败,设备:{device_id},错误码:{code},已执行回滚") + else: + success_device_ids_1.append(device_id) + + # 4. 记录授权 cars_card & cars_auth & devices_auth + ParkinglotsTable.add_car_card(table_handler, card_id, device_ids, reg_ids, obj) + return {"status": True} + + @staticmethod + def delete_car_auth_id(table_handler: BaseTable, auth_id: int): + """移除授权ID下的基本信息及关联车辆授权""" + # 1. 确认查询授权ID有效 + if ParkinglotsTable.exists_auth_id(table_handler, auth_id): + # 2. 获取相关授权设备ID & 车辆列表,在设备上进行 月卡删除 & 车辆删除(→_→暂不处理) + card_id, owner_name, owner_phone, car_type, device_ids, car_ids, _ = \ + ParkinglotsTable.get_auth_info_by_auth_id(table_handler, auth_id) + start_date, expire_date = ParkinglotsTable.get_car_card_interval(table_handler, card_id) + if len(device_ids) > 0: + success_ids = [] + fail_ids = [] + fail_code = [] + fail_msg = [] + sc = ServicesCall() + update_card_item = UpdateFixedCardItem( + vehicle_owner_id=owner_phone, + vehicle_owner_name=owner_name, + vehicle_owner_phone=owner_phone, + plate_number="", + effective_date_begin=start_date, + effective_date_end=expire_date, + action="delete", + card_id=card_id + ) + for device_id in device_ids: + update_card_item.plate_number = '|'.join(car_ids) + # 3. 设备移除授权 + _callback, code, msg = sc.update_fixed_card(device_id, update_card_item) + if not _callback: + fail_ids.append(device_id) + fail_code.append(code) + fail_msg.append(msg) + else: + success_ids.append(device_id) + + # 4. 删除授权ID cars_card & cars_auth & devices_auth 记录 + if len(success_ids) == len(device_ids): # 全部设备执行成功 + sqls = [ + "DELETE FROM cars_card WHERE auth_id = ?", + "DELETE FROM cars_auth WHERE auth_id = ?", + "DELETE FROM devices_auth WHERE _id = ? AND record_type = '车行'", + ] + params = [(auth_id,), (auth_id,), (auth_id,)] + table_handler.execute(sqls, params) + return {"status": True} + elif len(success_ids) > 0: + sqls = [ + "DELETE FROM cars_card WHERE auth_id = ?", + "DELETE FROM cars_auth WHERE auth_id = ?" + ] + _sql = "DELETE FROM devices_auth WHERE _id = ? AND device_id = ? AND record_type = '车行'" + sqls += [_sql for _ in range(len(success_ids))] + params = [(auth_id,), (auth_id,)] + params += [(auth_id, success_id) for success_id in success_ids] + table_handler.execute(sqls, params) + raise InvalidException(f"部分授权移除失败:设备ID {fail_ids}, 错误码: {fail_code}, {fail_msg}") + else: + raise InvalidException(f"授权移除失败:设备ID {fail_ids}, 错误码: {fail_code}, {fail_msg}") + else: + raise InvalidException(f"授权ID {auth_id} 无效") + + @staticmethod + def delete_car_auth(table_handler: BaseTable, delete_car_list): + """删除车辆授权的月卡信息,cars_auth""" + params = [] + for car_id in delete_car_list: + params.append((car_id,)) + table_handler.executemany("DELETE FROM cars_auth WHERE car_id = ?", params) + + @staticmethod + def update_car_card(table_handler: BaseTable, auth_id: int, car_type: str, owner_name: str, owner_phone: str, + associated_parking_space: str, start_datetime: str, expire_datetime: str): + """更新车辆授权的月卡信息,cars_card & cars_auth & devices_auth""" + table_handler.execute( + """ + UPDATE cars_card SET + car_type = ?, owner = ?, phone = ?, associated_parking_space = ?, + start_datetime = ?, expire_datetime = ?, update_datetime = ? + WHERE auth_id = ? + """, (car_type, owner_name, encrypt_number(owner_phone), associated_parking_space, + start_datetime, expire_datetime, now_datetime_second(), auth_id) + ) + + @staticmethod + def update_car_auth(table_handler: BaseTable, device_ids: list, reg_ids: list, obj: UpdateCarsCard): + """更新车辆授权的车辆信息和设备授权日期,cars_auth & devices_auth""" + # cars_auth + ParkinglotsTable.add_update_cars_auth_record(table_handler, obj.auth_id, obj.car_ids, reg_ids) + # devices_auth + st = obj.start_datetime + et = obj.expire_datetime + for device_id in device_ids: + DevicesTable.add_update_device_auth(table_handler, device_id, [str(obj.auth_id)], "车行", st, et) + + @staticmethod + def update_parkinglot_show(table_handler: BaseTable, + _id: str, name: Optional[str], factory_name: Optional[str], is_show: bool): + """更新停车场显示状态""" + if not ParkinglotsTable.exists_number(table_handler, _id, 'parkinglot'): + raise InvalidException(f"不存在编码 {_id} ") + status = 'show' if is_show else None + sub_sql_list = [] + if name is not None: + sub_sql_list.append(f"name = '{name}'") + if factory_name is not None: + sub_sql_list.append(f"factory_name = '{factory_name}'") + if len(sub_sql_list) > 0: + sub_sql = ', '.join(sub_sql_list) + ', ' + else: + sub_sql = '' + table_handler.execute( + f""" + UPDATE parkinglots + SET {sub_sql}running_status = ?, update_datetime = ? + WHERE type = 'parkinglot' AND id = ? + """, (status, now_datetime_second(), _id) + ) + return {"status": True} + + @staticmethod + def update_car_card_auth(table_handler: BaseTable, obj: UpdateCarsCard): + """更新车辆 & 更新月卡 & 保留记录""" + # 1. 获取车场系统 device_id + device_ids = DevicesTable.get_device_ids(table_handler, filter_name="停车") + if len(device_ids) > 0: + old_st, old_et = ParkinglotsTable.get_auth_id_interval(table_handler, obj.auth_id) + registration_time = millisecond_timestamp2tz( + ParkinglotsTable.get_car_reg_id(table_handler, str(obj.car_ids[0]))[:13]) + sc = ServicesCall() + vri = VehicleRegistrationItem( + car_plate_no="", + registration_id="", + owner_id=obj.owner_phone, + owner_name=obj.owner_name, + registration_time=registration_time, + begin_time=obj.start_datetime, + action="", + registration_type='0' if "业主" in obj.car_type else '1' + ) + # 2. 查询当前授权情况,获取 已授权车辆列表、待授权车辆列表、待移除车辆列表、待更新车辆列表 + card_id, owner_name, owner_phone, car_type, device_ids, authed_car_list, authed_reg_ids = \ + ParkinglotsTable.get_auth_info_by_auth_id(table_handler, obj.auth_id) + insert_car_list = obj.car_ids.copy() + delete_car_list = [] + update_car_list = [] + if (obj.start_datetime < now_datetime_second()[:-3] or obj.start_datetime == old_st) and \ + obj.expire_datetime == old_et and obj.owner_phone == owner_phone and \ + obj.owner_name == owner_name: + # 若有效时间+人名+手机号码未变更则不变的车辆不需更新,只考虑新增的车辆、移除的车辆 + for car_id in obj.car_ids: + if car_id in authed_car_list: + insert_car_list.remove(car_id) + for car_id in authed_car_list: + if car_id not in obj.car_ids: + delete_car_list.append(car_id) + else: + for car_id in obj.car_ids: + if car_id in authed_car_list: + insert_car_list.remove(car_id) + update_car_list.append(car_id) + for car_id in authed_car_list: + if car_id not in obj.car_ids: + delete_car_list.append(car_id) + + base_reg_id = int(time.time() * (10 ** 7)) + reg_ids = [str(base_reg_id + 1) for _ in insert_car_list] # 不同设备、相同车牌 相同注册ID + + success_insert_vri = [] + success_delete_vri = [] + success_update_origin_vri = [] + fail_callback = False + for device_index, device_id in enumerate(device_ids): + # 3. 注册车辆 & 删除车辆 & 更新车辆 + for car_id_index, car_id in enumerate(insert_car_list): + vri.car_plate_no = car_id + vri.registration_id = reg_ids[car_id_index] + vri.action = "insert" + _callback, code, msg = sc.vehicle_registration(device_id, vri) + if _callback or "UNIQUE" not in msg: + if not _callback and "UNIQUE" not in msg: + update_car_list.append(car_id) + else: + if "UNIQUE" in msg: + vri.action = "update" + _callback, _, _ = sc.vehicle_registration(device_id, vri) + success_insert_vri.append([device_id, vri.copy()]) + else: + fail_callback = True + break + + for car_id in delete_car_list: + vri.car_plate_no = car_id + vri.registration_id = ParkinglotsTable.get_car_reg_id(table_handler, car_id) + vri.action = "delete" + _callback, code, msg = sc.vehicle_registration(device_id, vri) + if _callback: + success_delete_vri.append([device_id, vri.copy()]) + else: + fail_callback = True + break + + for car_id in update_car_list: + vri.car_plate_no = car_id + vri.registration_id = ParkinglotsTable.get_car_reg_id(table_handler, car_id) + vri.action = "update" + _callback, code, msg = sc.vehicle_registration(device_id, vri) + if _callback: + vri.owner_id = owner_phone + vri.owner_name = owner_name + vri.begin_time = old_st + vri.registration_time = registration_time + vri.registration_type = car_type + success_update_origin_vri.append([device_id, vri.copy()]) + else: + fail_callback = True + break + # TODO 回滚逻辑待校验 + if fail_callback: # 只要存在失败的设备,就对所有之前成功的设备进行回滚 + for insert_item in success_insert_vri: + insert_item[1].action = "delete" + _callback, _, _ = sc.vehicle_registration(insert_item[0], insert_item[1]) + for delete_item in success_delete_vri: + delete_item[1].action = "insert" + _callback, _, _ = sc.vehicle_registration(delete_item[0], delete_item[1]) + for update_item in success_update_origin_vri: + _callback, _, _ = sc.vehicle_registration(update_item[0], update_item[1]) + + # 3. 更新月卡 + multi_car_id = '|'.join(obj.car_ids) + card_id = ParkinglotsTable.get_card_id_by_auth_id(table_handler, obj.auth_id) + ufci = UpdateFixedCardItem( + vehicle_owner_id=obj.owner_phone, + vehicle_owner_name=obj.owner_name, + vehicle_owner_phone=obj.owner_phone, + plate_number=multi_car_id, + effective_date_begin=obj.start_datetime, + effective_date_end=obj.expire_datetime, + action="update", + card_id=card_id + ) + success_device_ids_1 = [] + for device_id in device_ids: + _callback, code, msg = sc.update_fixed_card(device_id, ufci) + if not _callback: + raise InvalidException(f"车辆更新月卡失败,设备:{device_id},错误码:{code},{msg}") + else: + success_device_ids_1.append(device_id) + + # 4. 记录授权 cars_card & cars_auth & devices_auth + ParkinglotsTable.update_car_card(table_handler, obj.auth_id, obj.car_type, + obj.owner_name, obj.owner_phone, obj.associated_parking_space, + obj.start_datetime, obj.expire_datetime) + if insert_car_list: + obj.car_ids = insert_car_list + ParkinglotsTable.update_car_auth(table_handler, device_ids, reg_ids, obj) + if delete_car_list: + ParkinglotsTable.delete_car_auth(table_handler, delete_car_list) + return {"status": True} + + @staticmethod + def is_number_added(table_handler: BaseTable, number: str, _type: str): + """判断停车场编号是否已添加显示""" + number_col = "number" if _type != "parkinglot" else "id" + table_handler.query( + f"SELECT id FROM parkinglots WHERE {number_col} = ? and type = ? and running_status is not null", + (number, _type)) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists(table_handler: BaseTable, _id: str, _type: str): + table_handler.query(f"SELECT id FROM parkinglots WHERE id = ? and type = ?", (_id, _type)) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_number(table_handler: BaseTable, number: str, _type: str): + number_col = "number" if _type != "parkinglot" else "id" + table_handler.query(f"SELECT id FROM parkinglots WHERE {number_col} = ? and type = ?", + (number, _type)) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_phone(table_handler: BaseTable, phone: str): + table_handler.query(f"SELECT auth_id FROM cars_card WHERE phone = ?", + (encrypt_number(phone),)) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False + + @staticmethod + def exists_auth_id(table_handler: BaseTable, auth_id: int): + table_handler.query(f"SELECT auth_id FROM cars_card WHERE auth_id = ?", + (auth_id,)) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False diff --git a/SCLP/models/products.py b/SCLP/models/products.py new file mode 100644 index 0000000..9801788 --- /dev/null +++ b/SCLP/models/products.py @@ -0,0 +1,113 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 内置的产品信息表,在初始化时会初始化csv进行覆盖 +""" +import csv +import os +from datetime import datetime + +from pydantic import BaseModel + +from utils import logger +from utils.database import BaseTable +from utils.misc import now_datetime_second + + +class Product(BaseModel): + product_id: str + product_name: str + node_type: str + + +class ProductsTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/products.csv") + if os.path.exists(init_config_path): + table_handler.execute("DROP TABLE IF EXISTS products;") + table_handler.execute( + f""" + CREATE TABLE products ( + product_id TEXT, + product_name TEXT, + node_type TEXT, + update_datetime TEXT, + PRIMARY KEY (product_id) + ) + """ + ) + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 3: + for row in csvreader: + product_id = row[0].strip() + product_name = row[1].strip() + node_type = row[2].strip() + update_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + data.append((product_id, product_name, node_type, update_datetime)) + table_handler.executemany( + f""" + INSERT INTO products + (product_id, product_name, node_type, update_datetime) + VALUES (?, ?, ?, ?) + """, + data + ) + + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='brands'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE brands ( + brand_id INT, + brand_name TEXT, + status TEXT, + add_datetime TEXT, + update_datetime TEXT, + PRIMARY KEY (brand_id) + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/brands.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + data = [] + if len(head) == 3: + for row in csvreader: + brand_id = row[0].strip() + brand_name = row[1].strip() + status = row[2].strip() + data.append((brand_id, brand_name, status, now_datetime_second(), now_datetime_second())) + table_handler.executemany( + f""" + INSERT INTO brands + (brand_id, brand_name, status, add_datetime, update_datetime) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT (brand_id) DO NOTHING + """, + data + ) + + @staticmethod + def get_product_info(table_handler: BaseTable, product_id: str): + table_handler.query( + """ + SELECT product_name, node_type + FROM products + WHERE product_id = ? + """, + (product_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return Product(product_id=product_id, product_name=res[0][0], node_type=res[0][1]) + else: + return None diff --git a/SCLP/models/sessions.py b/SCLP/models/sessions.py new file mode 100644 index 0000000..7499515 --- /dev/null +++ b/SCLP/models/sessions.py @@ -0,0 +1,113 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 记录一些会话数据 +""" +import time + +from utils.database import BaseTable + + +class SessionsTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE sessions ( + session_id TEXT, + username TEXT, + token TEXT, + captcha TEXT, + last_timestamp TEXT, + PRIMARY KEY (session_id) + ) + """ + ) + table_handler.execute( + f""" + CREATE INDEX idx_sessions_token ON sessions(token); + """ + ) + # 去除冗余的验证会话信息 + table_handler.execute( + f""" + DELETE FROM sessions WHERE last_timestamp < {int(time.time() * 1000) - 3 * 24 * 60 * 60 * 1000} + """ + ) + + @staticmethod + def get_captcha(table_handler: BaseTable, session_id: str): + table_handler.query( + """ + SELECT captcha + FROM sessions + WHERE session_id = ? + """, + (session_id,), + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return None + + @staticmethod + def insert(table_handler: BaseTable, session_id, captcha): + timestamp = str(int(time.time() * 1000)) + table_handler.execute( + """ + INSERT INTO sessions + (session_id, captcha, last_timestamp) + VALUES (?, ?, ?) + ON CONFLICT (session_id) + DO UPDATE SET captcha=?, last_timestamp=? + """, + ( + session_id, + captcha, + timestamp, + captcha, + timestamp, + ), + ) + return True + + @staticmethod + def update(table_handler: BaseTable, session_id: str, username: str, token: str): + timestamp = str(int(time.time() * 1000)) + table_handler.execute( + """ + UPDATE sessions SET + username=?, + token=?, + last_timestamp=? + WHERE session_id=? + """, + ( + username, + token, + timestamp, + session_id + ), + ) + return True + + @staticmethod + def check_token(table_handler: BaseTable, timestamp: str, token: str): + table_handler.query( + """ + SELECT captcha + FROM sessions + WHERE last_timestamp > ? and token = ? + """, + (timestamp, token), + ) + res = table_handler.cursor.fetchall() + if res: + return True + else: + return False diff --git a/SCLP/models/spaces.py b/SCLP/models/spaces.py new file mode 100644 index 0000000..1ea7fdb --- /dev/null +++ b/SCLP/models/spaces.py @@ -0,0 +1,95 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 子系统映射表 增&改&查 +""" +import csv +import os + +from models.houses import HousesTable +from utils.database import BaseTable +from utils.misc import now_datetime_second, sql_export_xls + + +class SpacesTable(BaseTable): + + @staticmethod + def get_sub_system_name_by_room_id(table_handler: BaseTable, room_id: str): + table_handler.query( + """ + SELECT sub_space_name + FROM subsystem + WHERE room_id = ? + """, (room_id,) + ) + res = table_handler.cursor.fetchall() + if res: + return res[0][0] + else: + return None + + @staticmethod + def get_space_info_by_sub_space_name(table_handler: BaseTable, sub_space_name: str): + """根据第三方ID获取对应的房产信息""" + table_handler.query( + """ + SELECT subsystem.room_id, house_name, project_id, sub_system_id + FROM subsystem + LEFT JOIN houses ON subsystem.room_id = houses.room_id + WHERE sub_space_name = ? + """, (sub_space_name,) + ) + res = table_handler.cursor.fetchall() + if res: + return { + "room_id": res[0][0], + "house_name": res[0][1], + "project_id": res[0][2], + "sub_system_id": res[0][3] + } + else: + return None + + @staticmethod + def get_aiot_platform_data(table_handler: BaseTable): + query = """ + SELECT project_id, houses.room_id, house_name, COALESCE(subsystem_id, ''), COALESCE(sub_space_name, '') + FROM houses + LEFT JOIN subsystem ON subsystem.room_id = houses.room_id + """ + file_path = os.path.join(f"data/AIOT平台空间数据{now_datetime_second().replace(':', '_')}.xls") + sql_export_xls(query, table_handler.connection, file_path, + "房屋子系统映射数据表", + ["项目id", "房屋id", "房屋名称", "子系统id", "子系统空间名称"]) + return file_path + + @staticmethod + def get_sub_system_data(table_handler: BaseTable): + query = "SELECT subsystem_id, sub_space_name FROM subsystem" + file_path = os.path.join(f"data/子系统空间数据{now_datetime_second().replace(':', '_')}.xls") + sql_export_xls(query, table_handler.connection, file_path, + "子系统数据表", + ["子系统id", "子系统空间名称"]) + return file_path + + @staticmethod + def update(table_handler: BaseTable, objs: list): + data = [] + for obj in objs: + data.append((obj[0], now_datetime_second(), obj[1], obj[2])) + table_handler.executemany( + f""" + UPDATE subsystem + SET room_id = ?, update_datetime = ? + WHERE subsystem_id = ? and sub_space_name = ? + """, data + ) + + @staticmethod + def exits_room_id(table_handler: BaseTable, room_id: str): + table_handler.query("SELECT room_id FROM spaces WHERE room_id = ?", (room_id,)) + if table_handler.cursor.fetchone() is not None: + return True + else: + return False diff --git a/SCLP/models/subsystem.py b/SCLP/models/subsystem.py new file mode 100644 index 0000000..e1c3363 --- /dev/null +++ b/SCLP/models/subsystem.py @@ -0,0 +1,200 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 子系统房产信息表 +""" +import csv +import json +import os +import time +from typing import Optional + +from pydantic import BaseModel, Field, field_validator + +from models.spaces import SpacesTable +from utils.database import BaseTable, get_table_handler +from utils.misc import now_datetime_second, InvalidException, generate_captcha_text + + +class HouseDetailInfo(BaseModel): + project_id: str + subsystem_id: str + subsystem_name: str + subsystem_data: str + createby_id: str + subsystem_extend: dict + create_time: Optional[int] + corp_id: Optional[str] + message_id: Optional[str] = Field(alias="_id") + action: str = "insert" + house_id: Optional[str] = None + house_name: Optional[str] = None + + @field_validator("subsystem_extend") + def check_subsystem_extend(cls, value): + if value.get("label", None) is None: + raise InvalidException("subsystem_extend 中 label 值缺失") + return value + + # @field_validator("house_id") + # def check_house_id(cls, value, values): + # th = get_table_handler() + # if value is None or value == "": + # label = values.data.get("subsystem_extend")["label"] + # space_info = SpacesTable.get_space_info_by_sub_space_name(th, label) + # if space_info is None: + # raise InvalidException(f"subsystem_extend - label:{label} 不在映射表中") + # if SubsystemTable.exits_room_id(th, space_info["room_id"]): + # raise InvalidException(f"house_id:{space_info['room_id']} 记录已存在,禁止插入操作") + # return space_info["room_id"] + # if not SpacesTable.exits_room_id(th, value): + # raise InvalidException("映射表中不存在对应的 house_id") + # else: + # return value + + # @field_validator("house_name") + # def check_house_name(cls, value, values): + # th = get_table_handler() + # if value is None: + # label = values.data.get("subsystem_extend")["label"] + # space_info = SpacesTable.get_space_info_by_sub_space_name(th, label) + # if space_info is None: + # raise InvalidException(f"subsystem_extend - label:{label} 不在映射表中") + # if SubsystemTable.exits_room_id(th, space_info["room_id"]): + # raise InvalidException(f"house_id:{space_info['room_id']} 记录已存在,禁止插入操作") + # return space_info["house_name"] + # return value + + +class SubsystemTable(BaseTable): + @staticmethod + def check(table_handler: BaseTable): + """检测是否存在当前表""" + table_handler.query("SELECT name FROM sqlite_master WHERE type='table' AND name='subsystem'") + if table_handler.cursor.fetchone() is None: + table_handler.execute( + f""" + CREATE TABLE subsystem ( + room_id TEXT UNIQUE, + subsystem_id TEXT, + subsystem_project_id TEXT, + sub_space_name TEXT, + subsystem_name TEXT, + subsystem_data TEXT, + subsystem_extend TEXT, + createby_id TEXT, + corp_id TEXT, + _id TEXT, + create_time INTEGER, + update_time INTEGER, + update_datetime TEXT, + PRIMARY KEY (subsystem_id, subsystem_project_id, sub_space_name) + ) + """ + ) + init_config_path = os.path.join(os.path.dirname(os.path.abspath("__file__")), "data/InitialData/subsystem.csv") + if os.path.exists(init_config_path): + with open(init_config_path, newline='', encoding='utf8') as csvfile: + csvreader = csv.reader(csvfile) + head = next(csvreader) + if len(head) == 10: + data = [] + for row in csvreader: + room_id = row[0].strip() + subsystem_project_id = row[1].strip() + sub_space_name = row[2].strip() + subsystem_name = row[3].strip() + subsystem_data = row[4].strip() + subsystem_extend = row[5].strip() + createby_id = row[6].strip() + corp_id = row[7].strip() + _id = row[8].strip() + create_time = int(row[9].strip()) + update_datetime = now_datetime_second() + data.append((room_id, subsystem_project_id, sub_space_name, subsystem_name, subsystem_data, subsystem_extend, + createby_id, corp_id, _id, create_time, int(time.time() * 1000), update_datetime)) + table_handler.executemany( + f""" + INSERT INTO subsystem + (room_id, subsystem_project_id, sub_space_name, subsystem_name, subsystem_data, subsystem_extend, + createby_id, corp_id, _id, create_time, update_time, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (room_id) DO NOTHING + """, data + ) + + @staticmethod + def get_house_detail_info_by_project_id(table_handler: BaseTable, project_id: str) -> list: + """根据项目ID获取查找子系统空间结构中的房间信息""" + table_handler.query( + """ + SELECT subsystem.room_id, sub_space_name, house_name, subsystem_project_id, + subsystem_id, subsystem_name, subsystem_data, subsystem_extend, + createby_id, corp_id, _id, create_time, update_time + FROM subsystem + LEFT JOIN houses ON subsystem.room_id = houses.room_id + WHERE subsystem_project_id = ? + ORDER BY create_time DESC + """, (project_id,) + ) + res = table_handler.cursor.fetchall() + house_detail_info_list = [] + if res: + for info in res: + subsystem_extend = json.loads(info[7].replace("'", '"').replace("None", "null")) + subsystem_extend["label"] = info[1] + house_detail_info_list.append({ + "house_id": info[0] if info[0] else "", + "house_name": info[2] if info[0] else "", + "project_id": info[3], + "subsystem_id": info[4], + "subsystem_name": info[5], + "subsystem_data": info[6], + "subsystem_extend": subsystem_extend, + "createby_id": info[8], + "corp_id": info[9], + "_id": generate_captcha_text(16).lower(), + "create_time": info[11], + "update_time": info[12], + "is_stop": False, + "valid": True, + "ids": None + }) + return house_detail_info_list + + @staticmethod + def add_sub_system_house_info(table_handler: BaseTable, obj: HouseDetailInfo): + """添加房产信息""" + nt = now_datetime_second() + if obj.action == "insert": + table_handler.execute( + """ + INSERT OR IGNORE INTO subsystem + (subsystem_id, subsystem_project_id, sub_space_name, subsystem_name, subsystem_data, subsystem_extend, + createby_id, corp_id, _id, create_time, update_time, update_datetime) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (obj.subsystem_id, obj.project_id, obj.subsystem_extend["label"], obj.subsystem_name, obj.subsystem_data, str(obj.subsystem_extend), + obj.createby_id, obj.corp_id, obj.message_id, obj.create_time, int(time.time() * 1000), nt) + ) + else: + table_handler.execute( + """ + UPDATE subsystem + SET subsystem_id = ?, subsystem_project_id = ?, sub_space_name = ?, subsystem_name = ?, + subsystem_data = ?, subsystem_extend = ?, + createby_id = ?, corp_id = ?, _id = ?, create_time = ?, update_time = ?, update_datetime = ? + WHERE room_id = ? + """, (obj.subsystem_id, obj.project_id, obj.subsystem_extend["label"], obj.subsystem_name, + obj.subsystem_data, str(obj.subsystem_extend), + obj.createby_id, obj.corp_id, obj.message_id, + obj.create_time, int(time.time() * 1000), nt, obj.house_id) + ) + + @staticmethod + def exits_room_id(table_handler: BaseTable, room_id: str): + table_handler.query("SELECT room_id FROM subsystem WHERE room_id = ?", (room_id,)) + if table_handler.cursor.fetchone() is not None: + return True + else: + return False diff --git a/SCLP/readme.md b/SCLP/readme.md new file mode 100644 index 0000000..bfd8508 --- /dev/null +++ b/SCLP/readme.md @@ -0,0 +1,34 @@ +# 项目简介 + +> 智慧社区本地化平台 Smart Community Localization Platform,基于python实现的平台后端 + +# 功能模块 + +* 内置信息导入 +* 设备管理 +* 住户管理 +* 通行管理 +* 智慧停车 + +# 数据表 + +* **brands** 可视对讲厂商信息表 + - 初始化后只会进行更新不会进行增删 + +* **devices** 设备信息表 + - 基本aiot设备信息,接受边缘端注册、上线过程中保存的信息 + +* **devices_auth** 设备授权信息表 + - 记录设备授权过程中的信息 + +* **devices_scope** 单元ID与实际设备关系表 + - 在设备配置关联信息时插入,记录每一个单元权限下的所有设备 + - 移除设备关联信息时,在完成设备端授权清除后会清除该表中的关系 + +* **parkinglots** 停车场信息表 + - 在一张表中记录停车场、区域、通道、道闸的信息,通过type字段进行区分 + - 停车场:id(id就是编号)\number(aiot设备ID)\name\factory_name\running_status(是否为null控制页面显示) + - 区域:id\number\name\parkinglot_id\area_id(如果存在父区域的话为父区域ID) + - 通道:id\number\name\parkinglot_id\area_id\channel_type + - 道闸:id\number\name\parkinglot_id\area_id\channel_id\is_online\gate_status\running_status(工作状态) + diff --git a/SCLP/routers/access_devices_0.py b/SCLP/routers/access_devices_0.py new file mode 100644 index 0000000..dd801a7 --- /dev/null +++ b/SCLP/routers/access_devices_0.py @@ -0,0 +1,77 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 通行设备界面控制逻辑 +""" +from fastapi import APIRouter, Request, Header, Query + +from models.devices import DevicesTable, AccessDevicesScope, BuildingDevicesScope +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException + +router = APIRouter() + + +@router.get("/getAccessDevicesInfo", summary="获取通行设备信息") +async def get_access_devices_info(request: Request, + is_associated: bool = Query(...), + device_name: str = Query(None), + product_name: str = Query(None), + device_mac: str = Query(None), + device_id: int = Query(None), + token: str = Header(...)): + """获取通行设备信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + if device_name: + resp = DevicesTable.get_access_devices_info(th, is_associated, device_name=device_name) + elif product_name: + resp = DevicesTable.get_access_devices_info(th, is_associated, product_name=product_name) + elif device_mac: + resp = DevicesTable.get_access_devices_info(th, is_associated, device_mac=device_mac) + elif device_id: + resp = DevicesTable.get_access_devices_info(th, is_associated, device_id=device_id) + else: + resp = DevicesTable.get_access_devices_info(th, is_associated) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/addAccessDevicesAssociateInfo", summary="批量增加大门门禁/大门闸机关联信息") +async def add_access_devices_associate_info(request: Request, item: AccessDevicesScope, token: str = Header(...)): + """批量增加大门门禁/大门闸机关联信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + _callback = DevicesTable.add_access_devices(th, item.device_type, item.info) + resp = {"status": _callback} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/addBuildingDevicesAssociateInfo", summary="批量增加楼栋门禁关联信息") +async def add_building_devices_associate_info(request: Request, item: BuildingDevicesScope, token: str = Header(...)): + """批量增加楼栋门禁关联信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + _callback = DevicesTable.add_building_devices(th, "楼栋门禁", item.info) + resp = {"status": _callback} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.delete("/deleteAssociatedAccessDeviceInfo", summary="移除完成关联的设备及其已授权的人员信息") +async def delete_access_device_info(request: Request, device_id: int = Query(alias="id"), token: str = Header(...)): + """移除完成关联的设备及其已授权的人员信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = DevicesTable.delete_access_device_info(th, device_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/access_devices_1.py b/SCLP/routers/access_devices_1.py new file mode 100644 index 0000000..34e5cfc --- /dev/null +++ b/SCLP/routers/access_devices_1.py @@ -0,0 +1,193 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 通行授权界面控制逻辑 +""" +import os +import time + +from fastapi import APIRouter, Request, Header, File, UploadFile, Query + +from config import PREFIX_PATH, SUB_PATH +from config import APP_HOST as HOST_IP +from config import IMAGE_SERVER_PORT as PORT +from models.devices import SearchDevicesInfo, DevicesTable +from models.householders import HouseholdersTable, GetAuthHouseholdersInfo, GetAuthUsersInfo, \ + UpdateHouseholderFace, AddAuthUserInfo, UpdateAuthUserInfo +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException + +router = APIRouter() + + +@router.get("/getHouseholdersInfo", summary="授权住户查询") +async def get_householders_info(request: Request, + page: int = Query(..., gt=0), + limit: int = Query(..., gt=0), + token: str = Header(...)): + """获取已授权的住户信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_auth_householder_info(th, None, None, None, None, + page, limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/getHouseholdersInfo", summary="授权住户条件查询") +async def get_householders_info_by_condition(request: Request, + item: GetAuthHouseholdersInfo, + token: str = Header(...)): + """根据条件获取已授权的住户信息查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + logger.Logger.debug(f"{request.url.path} <- {item.__dict__}") + resp = HouseholdersTable.get_auth_householder_info(th, item.search_type, item.search_key, + item.space_type, item.space_id, + item.page, item.limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getHouseholderDetailInfo", summary="住户授权详细信息查询") +async def get_householder_detail_info(request: Request, householder_id: int = Query(...), token: str = Header(...)): + """获取指定住户授权详细信息查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_auth_householder_detail_info(th, householder_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getUsersInfo", summary="通用授权人员信息查询") +async def get_users_info(request: Request, + page: int = Query(..., gt=0), + limit: int = Query(..., gt=0), + token: str = Header(...)): + """获取已授权的通用人员信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_auth_users_info(th, None, None, None, page, limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/getUsersInfo", summary="通用授权人员信息条件查询") +async def get_users_info_by_condition(request: Request, + item: GetAuthUsersInfo, + token: str = Header(...)): + """根据条件获取已授权的通用人员信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_auth_users_info(th, item.search_type, item.search_key, + item.user_type, item.page, item.limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getUserDetailInfo", summary="通用授权人员详细信息查询") +async def get_user_detail_info(request: Request, + user_id: int = Query(alias="id"), + token: str = Header(...)): + """获取指定通用人员的详细信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_auth_user_info(th, user_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.delete("/deleteUserInfo", summary="删除通用授权人员信息及关联设备授权") +async def get_users_info(request: Request, user_id: int = Query(...), token: str = Header(...)): + """通用授权人员删除 & 移除关联设备授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.delete_auth_user_info(th, user_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/addUserInfo", summary="新增通用授权人员信息及关联设备授权") +async def add_user_info(request: Request, item: AddAuthUserInfo, token: str = Header(...)): + """新增通用授权人员 & 人脸图像绑定 & 添加关联设备授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.add_auth_user_info(th, item) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/updateUserInfo", summary="更新通用授权人员信息及关联设备授权") +async def update_user_info(request: Request, item: UpdateAuthUserInfo, token: str = Header(...)): + """更新通用授权人员信息 & 人脸图像绑定 & 判断是否需要刷新关联设备授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.update_auth_user_info(th, item) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/uploadHouseholderFace", summary="人脸图片上传") +async def upload_householder_face(request: Request, + face_file: UploadFile = File(...), + token: str = Header(...)): + """接收人脸图片上传并保存""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + # 检查文件类型是否为图像 + if not face_file.content_type.startswith("image"): + resp = {"status": False, "message": "仅支持上传图片"} + else: + file_content = await face_file.read() + max_size_mb = 10 # 设置最大允许的文件大小为 10MB + if face_file.file.seek(0, os.SEEK_END) > max_size_mb * 1024 * 1024: + resp = {"status": False, "message": f"文件大小不得超过 {max_size_mb}MB"} + else: + save_path = f"./data/FaceImages" + filename = f"_{int(time.time() * 1000)}" + if not os.path.exists(save_path): + os.mkdir(save_path) + file_path = f"{save_path}/{filename}.jpg" + with open(file_path, "wb") as f: + f.write(file_content) + + # 返回成功消息及人脸照片的 URL + face_url = f"http://{HOST_IP}:{PORT}{PREFIX_PATH}/{SUB_PATH}/{filename}.jpg" + resp = {"face_url": face_url} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/updateHouseholderFace", summary="更新住户人脸信息") +async def get_users_info(request: Request, item: UpdateHouseholderFace, token: str = Header(...)): + """人脸图像与住户ID绑定 & 刷新关联设备授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.update_householder_face(th, item.id, item.face_url) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/getAssociatedAccessDevicesInfo", summary="通用授权-门禁设备条件查询") +async def get_associated_access_devices_info(request: Request, item: SearchDevicesInfo, token: str = Header(...)): + """已具备关联空间信息的门禁设备条件查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = DevicesTable.get_associated_access_devices_info(th, item.search_type, item.search_key) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/devices_manage.py b/SCLP/routers/devices_manage.py new file mode 100644 index 0000000..ccd2933 --- /dev/null +++ b/SCLP/routers/devices_manage.py @@ -0,0 +1,30 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Created : +@Updated : +@Contact : xuxingchen@sinochem.com +@Desc : 设备管理界面控制逻辑 +""" +from fastapi import APIRouter, Request, Header + +from models.devices import DevicesTable +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException + +router = APIRouter() + + +@router.get("/getAllDevicesInfo", summary="获取全量的设备信息") +async def get_all_devices_info(request: Request, token: str = Header(...)): + """获取全量的设备信息列表""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + devices_info = DevicesTable.get_devices_info(th) + resp = {"devices": devices_info} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/edge_simulation_api.py b/SCLP/routers/edge_simulation_api.py new file mode 100644 index 0000000..013e3cc --- /dev/null +++ b/SCLP/routers/edge_simulation_api.py @@ -0,0 +1,163 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 模拟边缘对接所需的平台api接口 +""" +from fastapi import APIRouter, Request, Query + +from models.householders import HouseholdersTable +from models.houses import HousesTable +from models.spaces import SpacesTable +from models.subsystem import SubsystemTable, HouseDetailInfo +from routers.login import authenticate_token +from utils import logger +from utils.database import get_table_handler +from utils.misc import InvalidException, snake2camel_list_dict + +router = APIRouter() + + +@router.post("/v2/accesskey_auth", summary="回传一个无效的随机字符串") +async def access_key_auth(_item: dict): + return {"access_token": "Q0E5QjU4QUNDMDA4MTY4RDNFNkRGRjNGNzQ4NDAzMTQ5OTVBMDQwODMyNjBFMjBCQkIwQjA2QzlCNUQ3MUY3NA=="} + + +@router.get("/v3/realty-master-data/owners/{user_id}", summary="根据用户 id 获取房产信息") +async def get_property_info_by_user_id(request: Request, user_id: int): + logger.Logger.debug(f"{request.url.path} <- {user_id}") + th = get_table_handler() + house_ids = HouseholdersTable.get_room_ids(th, user_id) + resp = { + "status": 200, + "msg": "ok", + "data": { + "house_ids": house_ids + }, + "code": 200, + "det": 0 + } + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/v3/realty-master-data/houses", summary="根据房产 id 获取房产详细信息") +async def get_house_detail_info(request: Request, item: dict): + try: + logger.Logger.debug(f"{request.url.path} <- {item}") + house_ids = item["query"]["id"]["$in"] + house_detail_list = [] + th = get_table_handler() + for house_id in house_ids: + house_detail_info = HousesTable.get_house_detail_info(th, house_id) + if house_detail_info is not None: + house_detail_list.append(house_detail_info) + resp = { + "status": 200, + "msg": "ok", + "data": { + "total_count": len(house_detail_list), + "total_page": 0, + "list": house_detail_list + }, + "code": 200, + "det": 0 + } + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + except (KeyError, TypeError): + raise InvalidException("请求参数结构异常") + + +@router.post("/v2/realty-master-data/SubsystemSpaceMap/list", summary="查找空间结构中的房间信息") +async def get_subsystem_house_detail_info(request: Request, item: dict): + try: + logger.Logger.debug(f"{request.url.path} <- {item}") + project_id = item["query"]["project_id"]["$eq"] + th = get_table_handler() + data_list = SubsystemTable.get_house_detail_info_by_project_id(th, project_id) + resp = { + "status": 200, + "msg": "ok", + "data": { + "total_count": len(data_list), + "total_page": 0, + "list": data_list + }, + "code": 200, + "det": 0 + } + logger.Logger.debug(f"{request.url.path} -> {resp}") + return resp + except (KeyError, TypeError): + raise InvalidException("请求参数结构异常") + + +@router.post("/v2/realty-master-data/SubsystemSpaceMap/added", summary="添加房产信息") +async def add_subsystem_house_info(request: Request, item: HouseDetailInfo): + try: + logger.Logger.debug(f"{request.url.path} <- {item}") + th = get_table_handler() + SubsystemTable.add_sub_system_house_info(th, item) + resp = { + "status": 200, + "msg": "ok", + "data": item.message_id, + "code": 200, + "det": 0 + } + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + except (KeyError, TypeError): + raise InvalidException("请求参数结构异常") + + +@router.post("/v2/realty-master-data/SubsystemSpaceMap/update", summary="更新房产信息") +async def update_subsystem_house_info(request: Request, item: HouseDetailInfo): + try: + logger.Logger.debug(f"{request.url.path} <- {item}") + th = get_table_handler() + item.action = "update" + SubsystemTable.add_sub_system_house_info(th, item) + resp = { + "status": 200, + "msg": "ok", + "data": item.message_id, + "code": 200, + "det": 0 + } + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + except (KeyError, TypeError): + raise InvalidException("请求参数结构异常") + + +@router.post("/v2/smart-access/house/house_ty_maps", summary="查询房产的真实信息") +async def get_house_ty_info(request: Request, item: dict): + try: + logger.Logger.debug(f"{request.url.path} <- {item}") + project_id = item["query"]["project_id"]["$eq"] + room_id = item["query"]["_id"]["$eq"] + th = get_table_handler() + house_ty_info = HousesTable.get_house_ty_info(th, project_id, room_id) + if house_ty_info is None: + house_ty_infos = [] + else: + house_ty_infos = [house_ty_info] + resp = { + "status": 200, + "msg": "ok", + "data": { + "count": 1, + "list": house_ty_infos, + "query_res": { + "count": 1, + "list": snake2camel_list_dict(house_ty_infos) + } + }, + "det": 0 + } + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + except (KeyError, TypeError): + raise InvalidException("请求参数结构异常") diff --git a/SCLP/routers/householder_manage.py b/SCLP/routers/householder_manage.py new file mode 100644 index 0000000..a45cb4c --- /dev/null +++ b/SCLP/routers/householder_manage.py @@ -0,0 +1,84 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 住户管理界面控制逻辑 +""" +from fastapi import APIRouter, Request, Header, Query + +from models.householders import HouseholdersTable, GetHouseholdersInfo, AddHouseholderInfo, UpdateHouseholderInfo +from models.houses import HousesTable +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException + +router = APIRouter() + + +@router.post("/addHouseholderInfo", summary="新增住户信息") +async def add_householder_info(request: Request, item: AddHouseholderInfo, token: str = Header(...)): + """新增住户信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + # 进行住户信息插入 + householder_id = HouseholdersTable.insert(th, item.name, item.sex, item.phone, "住户") + if householder_id: + for room_id, _type in item.property_info: + # 房产关联表信息插入 + HouseholdersTable.insert_type(th, room_id, householder_id, _type) + # 更新房产中的住户数量 + old_count = HousesTable.get_householder_count(th, room_id) + HousesTable.update_householder_count(th, room_id, old_count + 1) + resp = {"status": True} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/getHouseholderInfo", summary="获取住户信息") +async def get_householder_info(request: Request, item: GetHouseholdersInfo, token: str = Header(...)): + """获取住户信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_householder_info(th, item.search_type, item.search_key, item.role_type, + item.page, item.limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getHouseholderInfo", summary="查询指定住户信息") +async def get_householder_detail_info(request: Request, + householder_id: int = Query(alias="id"), + token: str = Header(...)): + """查询指定住户信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.get_householder_info_by_id(th, householder_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/updateHouseholderInfo", summary="更新指定住户信息") +async def update_householder_info(request: Request, item: UpdateHouseholderInfo, token: str = Header(...)): + """更新指定住户信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.update_householder_info(th, item.householder_id, item.property_info) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.delete("/deleteHouseholderInfo", summary="删除指定住户信息及移除其关联设备授权") +async def delete_householder_info(request: Request, householder_id: int = Query(alias="id"), token: str = Header(...)): + """删除指定住户的信息及其关联授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = HouseholdersTable.delete_householder_info(th, householder_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/houses_manage.py b/SCLP/routers/houses_manage.py new file mode 100644 index 0000000..781f20a --- /dev/null +++ b/SCLP/routers/houses_manage.py @@ -0,0 +1,86 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 房产信息控制逻辑 +""" +from typing import Optional + +from fastapi import APIRouter, Header, Request, Query +from pydantic import BaseModel, field_validator, Field + +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from models.houses import HousesTable +from utils.misc import InvalidException + +router = APIRouter() + + +class ListsReq(BaseModel): + project: str = Field(description="必填项,用于限定项目编号") + area: Optional[str] = Field(None, description="可选项,用于限定区域") + building: Optional[str] = Field(None, description="可选项,用于限定楼栋") + unit: Optional[str] = Field(None, description="可选项,用于限定单元") + floor: Optional[str] = Field(None, description="可选项,用于限定楼层") + target: str = Field(description="必填项,用于控制输出结果,取值范围:area, building, unit, floor, room") + + def dict(self, *args, **kwargs): + _dict = self.__dict__.copy() + for key, value in self.__dict__.items(): + if value is None: + _dict.pop(key) + _dict.pop("target") + return _dict + + @field_validator("target") + def validate_target(cls, value): + if value not in ["area", "building", "unit", "floor", "room"]: + raise InvalidException("请提供正确的请求target [area, building, unit, floor, room]") + else: + return value + + +@router.get("/projects", summary="获取所有项目列表") +async def get_projects_list(request: Request, token: str = Header(...)): + """获取项目列表""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + projects_list = HousesTable.get_items_by_dynamic_item(th, "project", {}) + resp = {"projects": projects_list} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/project/lists", summary="动态获取房产信息") +async def get_items(request: Request, + item: ListsReq, + token: str = Header(...)): + """根据请求获取列表""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + item_list = HousesTable.get_items_by_dynamic_item(th, item.target, item.dict()) + resp = {f"{item.target.split('_')[0]}s": item_list} + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getHouseholderCount", summary="获取房产住户数量") +async def get_householder_count(request: Request, + room_id: str = Query(...), + token: str = Header(...)): + """根据请求获取对应房屋中当前住户数量""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + count = HousesTable.get_householder_count(th, room_id) + if count is not None: + resp = {"count": count} + else: + raise InvalidException("room_id 不存在") + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/login.py b/SCLP/routers/login.py new file mode 100644 index 0000000..c8b3adc --- /dev/null +++ b/SCLP/routers/login.py @@ -0,0 +1,140 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 登陆界面控制逻辑 +""" +import hashlib +import os.path +import random +import io +import time +from typing import Optional +from starlette.responses import StreamingResponse + +from pydantic import BaseModel +from fastapi import APIRouter, Query, Request +from PIL import Image, ImageDraw, ImageFont + +from utils.database import get_table_handler +from utils import logger +from utils.misc import generate_captcha_text +from models.sessions import SessionsTable + +router = APIRouter() + + +class LoginItem(BaseModel): + username: str + password: str + session_id: str + captcha: str + + +class LoginResp(BaseModel): + status: bool + message: str + token: Optional[str] = None + + +def generate_captcha_image(text, size=(120, 40), font_path='./data/mvboli.ttf', font_size=24): + """生成验证码图片""" + image = Image.new('RGB', size, (73, 109, 137)) # 设置背景色 + d = ImageDraw.Draw(image) + assert os.path.exists(font_path), "字体文件不存在" + font = ImageFont.truetype(font_path, font_size) + # d.text((5, 5), text, font=font, fill=(255, 255, 0)) # 设置字体颜色和位置 + font_box = font.getbbox(text) + text_width, text_height = font_box[2] - font_box[0], font_box[3] - font_box[1] + text_x = (size[0] - text_width) // 2 + text_y = (size[1] - text_height) // 2 + d.text((text_x, text_y - 8), text, font=font, fill=(255, 255, 0)) + + # 添加噪点 + for _ in range(100): # 可以根据需要调整噪点数量 + x = random.randint(0, size[0] - 1) + y = random.randint(0, size[1] - 1) + image.putpixel((x, y), (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) + + # 添加线条 + for _ in range(3): # 可以根据需要调整线条数量 + x1, y1 = random.randint(0, size[0] - 1), random.randint(0, size[1] - 1) + x2, y2 = random.randint(0, size[0] - 1), random.randint(0, size[1] - 1) + d.line([(x1, y1), (x2, y2)], fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)), + width=2) + + # 将图片保存为字节流 + bytes_io = io.BytesIO() + image.save(bytes_io, format='PNG') + # image.save("./data/tmp.png", format='PNG') + bytes_io.seek(0) + return bytes_io + + +def hash_password(password): + """对密码进行 SHA-256 哈希加密""" + password_bytes = password.encode('utf-8') + sha256_hash = hashlib.sha256() + sha256_hash.update(password_bytes) + return sha256_hash.hexdigest() + + +def verify_password(password, hashed_password): + """比对密码和哈希值""" + password_hash = hash_password(password) + return password_hash.lower() == hashed_password.lower() + + +def authenticate_token(token: str): + th = get_table_handler() + timeout_timestamp = str(int(time.time() * 1000 - 4 * 60 * 60 * 1000)) + return SessionsTable.check_token(th, timeout_timestamp, token) + + +@router.get("/generateCaptcha", summary="生成验证码") +async def generate_captcha_endpoint(request: Request, + session_id: str = Query(None, description="Session ID")): + """生成验证码图片并返回""" + if session_id: + captcha_text = generate_captcha_text() + logger.Logger.debug(f"{request.url.path} 验证码生成:{captcha_text}") + # session_id 入库 + th = get_table_handler() + SessionsTable.insert(th, session_id, captcha_text) + captcha_image = generate_captcha_image(captcha_text) + # 设置HTTP响应的头部,指定内容类型为PNG图片 + headers = {"Content-Type": "image/png"} + return StreamingResponse(captcha_image, headers=headers) + else: + logger.Logger.error(f"{request.url.path} 请求参数缺少 session_id - {request.client.host}") + return {"status": False, "message": "请求参数缺少 session_id"} + + +@router.post("/login", response_model=LoginResp, response_model_exclude_none=True, summary="登录请求") +async def login(item: LoginItem): + """简单的账户登录逻辑""" + status = False + token = None + th = get_table_handler() + # 验证码校验 + right_captcha = SessionsTable.get_captcha(th, item.session_id) + if right_captcha is not None: + if item.captcha.lower() == right_captcha.lower(): + # 账户密码校验 + if item.username == "admin": + if verify_password(item.password, "B12AD23C7E230E2CA365508DD16635DD3D7214BCD9BEA27457A356FD5C15F8BF"): + status = True + message = "校验通过" + token = generate_captcha_text(16) + SessionsTable.update(th, item.session_id, item.username, token) + else: + message = "密码错误" + else: + message = "账户不存在" + else: + message = "验证码错误" + else: + message = "无效 session_id" + resp = LoginResp(status=status, message=message, token=token) + logger.Logger.debug("/login " + str(resp.__dict__)) + return resp diff --git a/SCLP/routers/parkinglot_0.py b/SCLP/routers/parkinglot_0.py new file mode 100644 index 0000000..9bc4e3b --- /dev/null +++ b/SCLP/routers/parkinglot_0.py @@ -0,0 +1,85 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 车场管理界面控制逻辑 +""" +from fastapi import APIRouter, Request, Header, Query + +from models.parkinglots import ParkinglotsTable, AddParkingLot +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException + +router = APIRouter() + + +@router.get("/getParkingLotsInfo", summary="车场信息查询") +async def get_parkinglots_info(request: Request, token: str = Header(...)): + """获取车场信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_parkinglots_info(th) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/addParkingLot", summary="添加车场") +async def add_parkinglot(request: Request, item: AddParkingLot, token: str = Header(...)): + """添加显示信息的车场 & 更新车场编号对应的名字和供应商""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + factory_name = item.factory_name if item.factory_name else '' + resp = ParkinglotsTable.update_parkinglot_show(th, item.parkinglot_number, item.parkinglot_name, factory_name, True) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.delete("/deleteParkingLot", summary="移除车场") +async def delete_parkinglot(request: Request, + parkinglot_number: str = Query(alias="number", description="车场编号"), + token: str = Header(...)): + """移除显示信息的车场""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.update_parkinglot_show(th, parkinglot_number, None, None, False) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getParkingLotsAreaInfo", summary="区域信息查询") +async def get_parkinglots_area_info(request: Request, token: str = Header(...)): + """区域信息查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_showed_parkinglot_area_info(th) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getParkingLotsChannelInfo", summary="通道信息查询") +async def get_parkinglots_channel_info(request: Request, token: str = Header(...)): + """通道信息查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_showed_parkinglot_channel_info(th) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getParkingLotsGateInfo", summary="道闸信息查询") +async def get_parkinglots_channel_info(request: Request, token: str = Header(...)): + """道闸信息查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_showed_parkinglot_gate_info(th) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/parkinglot_1.py b/SCLP/routers/parkinglot_1.py new file mode 100644 index 0000000..39adc3d --- /dev/null +++ b/SCLP/routers/parkinglot_1.py @@ -0,0 +1,108 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 车辆授权界面控制逻辑 +""" +from fastapi import APIRouter, Query, Request, Header + +from models.parkinglots import ParkinglotsTable, GetCarsInfo, AddCarsCard, UpdateCarsCard +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException + +router = APIRouter() + + +@router.get("/getCarsInfo", summary="授权车辆信息查询") +async def get_cars_info(request: Request, + page: int = Query(None), + limit: int = Query(None), + token: str = Header(...)): + """授权车辆信息全量查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_cars_info(th, None, None, page, limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/getCarsInfo", summary="授权车辆信息条件查询") +async def get_cars_info(request: Request, + item: GetCarsInfo, + token: str = Header(...)): + """授权车辆信息全量查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_cars_info(th, item.search_type, item.search_key, item.page, item.limit) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getCarDetailInfo", summary="详细授权信息查询") +async def get_car_detail_info(request: Request, + auth_id: int = Query(...), + token: str = Header(...)): + """授权车辆信息全量查询""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_auth_info(th, auth_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getCarIdAuthStatus", summary="获取车牌号授权状态") +async def get_car_id_auth_status(request: Request, + car_id: str = Query(...), + token: str = Header(...)): + """获取车牌号授权状态""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.get_car_id_auth_status(th, car_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.delete("/deleteCarAuthId", summary="删除车辆授权") +async def delete_car_auth_id(request: Request, + auth_id: int = Query(...), + token: str = Header(...)): + """删除车辆月卡授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.delete_car_auth_id(th, auth_id) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/addCarAuthInfo", summary="新增车辆授权") +async def add_car_card_auth(request: Request, + item: AddCarsCard, + token: str = Header(...)): + """新增车辆月卡授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.add_car_card_auth(th, item) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.post("/updateCarInfo", summary="更新车辆授权") +async def add_car_card_auth(request: Request, + item: UpdateCarsCard, + token: str = Header(...)): + """更新车辆月卡授权""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = ParkinglotsTable.update_car_card_auth(th, item) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/routers/space_manage.py b/SCLP/routers/space_manage.py new file mode 100644 index 0000000..7640da6 --- /dev/null +++ b/SCLP/routers/space_manage.py @@ -0,0 +1,101 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 通行空间映射界面控制逻辑 +""" +import os.path +from io import BytesIO + +from fastapi import APIRouter, Request, Header, BackgroundTasks, UploadFile, File +from fastapi.responses import FileResponse + +from models.brands import BrandsTable, UpdateBrand +from models.spaces import SpacesTable +from routers.login import authenticate_token +from utils import logger +from utils.logger import TOKEN_ERROR +from utils.database import get_table_handler +from utils.misc import InvalidException, valid_xls + +router = APIRouter() + + +@router.post("/updateSubSystemPlatformName", summary="更新当前使用的可视对讲厂家") +async def update_sub_system_platform_name(request: Request, item: UpdateBrand, token: str = Header(...)): + """更新当前使用的可视对讲厂家""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = BrandsTable.update_checked_factory(th, item.name) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/getSubSystemPlatformName", summary="查询当前使用的可视对讲厂家") +async def get_sub_system_platform_name(request: Request, token: str = Header(...)): + """获取当前启用的可视对讲厂家""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + resp = BrandsTable.get_checked_factory(th) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp + + +@router.get("/exportAiotPlatformData", summary="导出AIOT平台空间数据") +async def export_aiot_platform_data(request: Request, background_tasks: BackgroundTasks, token: str = Header(...)): + """导出AIOT平台空间数据""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + file_path = SpacesTable.get_aiot_platform_data(th) + background_tasks.add_task(os.remove, file_path) + logger.Logger.debug(f"{request.url.path} 完成AIOT平台空间数据导出") + return FileResponse(path=file_path, + filename=os.path.basename(file_path), + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + +@router.get("/exportSubSystemData", summary="导出子系统空间结构数据表") +async def export_aiot_platform_data(request: Request, background_tasks: BackgroundTasks, token: str = Header(...)): + """导出子系统空间结构数据表""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + th = get_table_handler() + file_path = SpacesTable.get_sub_system_data(th) + background_tasks.add_task(os.remove, file_path) + logger.Logger.debug(f"{request.url.path} 完成子系统空间数据导出") + return FileResponse(path=file_path, + filename=os.path.basename(file_path), + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + +@router.post("/importIdMapData", summary="导入空间映射表") +async def import_id_map_data(request: Request, + xls_file: UploadFile = File(...), + token: str = Header(...)): + """接收空间映射表xls表格上传、校验并更新对应映射信息""" + if not authenticate_token(token): + raise InvalidException(TOKEN_ERROR) + # 检查文件类型是否为xls + if xls_file.content_type not in ["application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]: + resp = {"status": False, "message": "仅支持上传xls文件"} + else: + file_content = await xls_file.read() + max_size_mb = 10 # 设置最大允许的文件大小为 10MB + if xls_file.file.seek(0, os.SEEK_END) > max_size_mb * 1024 * 1024: + resp = {"status": False, "message": f"文件大小不得超过 {max_size_mb}MB"} + else: + # 解析xls,校验数据格式是否符合标准 + required_columns = ["房屋id", "子系统id", "子系统空间名称"] + _callback, message, data = valid_xls(BytesIO(file_content), required_columns) + if _callback: + th = get_table_handler() + SpacesTable.update(th, data) + resp = {"status": True} + else: + raise InvalidException(message) + logger.Logger.debug(f"{request.url.path} {resp}") + return resp diff --git a/SCLP/run.sh b/SCLP/run.sh new file mode 100644 index 0000000..6962083 --- /dev/null +++ b/SCLP/run.sh @@ -0,0 +1 @@ +nohup python main.py >> nohup.out 2>&1 & \ No newline at end of file diff --git a/SCLP/utils/database.py b/SCLP/utils/database.py new file mode 100644 index 0000000..3352b51 --- /dev/null +++ b/SCLP/utils/database.py @@ -0,0 +1,85 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 数据库处理基础方法 +""" +import sqlite3 + +from config import DB_PATH +from utils.logger import Logger, new_dc + + +class SQLiteDatabaseEngine: + def __init__(self, db_path: str = "demo.db") -> None: + self.sqlite3 = sqlite3 + self.db_path = db_path + self.connection = None + self.cursor = None + self.connect() + + def connect(self): + """连接SQLite 数据库(如果数据库不存在则会自动创建)""" + self.connection = self.sqlite3.connect(self.db_path) + self.cursor = self.connection.cursor() + Logger.init(new_dc(f"🔗 SQLite - {self.db_path} has connect successfully! 🔗", "[1;32m")) + + def disconnect(self): + try: + self.cursor.close() + self.connection.close() + except Exception as e: + if type(e).__name__ != "ProgrammingError": + Logger.error(f"{type(e).__name__}, {e}") + Logger.info(new_dc(f"🔌 Disconnect from SQLite - {self.db_path}! 🔌", "[1m")) + + def exist(self, table_name): + self.cursor.execute( + f"SELECT name FROM sqlite_master WHERE type='table' AND name=?", + (table_name,), + ) + result = self.cursor.fetchone() + if result: + return True + else: + return False + + +class BaseTable: + def __init__(self, connection=None, cursor=None): + self.connection = connection + self.cursor = cursor + + def set(self, connection, cursor): + self.connection = connection + self.cursor = cursor + + def execute(self, sql: str | list, params: tuple | list = ()): + if isinstance(sql, list): + for i, s in enumerate(sql): + self.cursor.execute(s, params[i]) + else: + self.cursor.execute(sql, params) + self.connection.commit() + + def executemany(self, sql: str, params: list[tuple]): + self.cursor.executemany(sql, params) + self.connection.commit() + + def query(self, sql: str, params: tuple = ()): + self.cursor.execute(sql, params) + + +class TableHandlerSingleton: + _instance = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + db = SQLiteDatabaseEngine(db_path=DB_PATH) + cls._instance = BaseTable(db.connection, db.cursor) + return cls._instance + + +def get_table_handler(): + return TableHandlerSingleton.get_instance() diff --git a/SCLP/utils/logger.py b/SCLP/utils/logger.py new file mode 100644 index 0000000..85b0861 --- /dev/null +++ b/SCLP/utils/logger.py @@ -0,0 +1,163 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 日志模块 +""" +import sys +import io +import time + +DEBUG = None +LOGGER_PATH = None +TOKEN_ERROR = "token无效" + + +def log(text: str, log_path: str = None): + """打印日志""" + log_path = log_path if log_path else LOGGER_PATH + log_line = '[{}] {}'.format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), text) + if log_path: + log_file = open(log_path, 'a', encoding='utf8') + log_file.write("{}\n".format(log_line)) + log_file.close() + print(log_line) + + +def log_plus(text: str, log_path: str = None, prefix_text: str = None, suffix_text: str = None): + """加强版打印日志,预置了不同文字颜色""" + log_path = log_path if log_path else LOGGER_PATH + if prefix_text: + log_line_start = f"[{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}] {prefix_text}" + else: + log_line_start = f"[{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}]" + if suffix_text: + log_line = f"{log_line_start} {text} {suffix_text}" + else: + log_line = f"{log_line_start} {text}" + if log_path: + log_file = open(log_path, 'a', encoding='utf8') + log_file.write("{}\n".format(log_line)) + log_file.close() + print(log_line) + + +def format_text(text_list: list, align_list: list = None) -> str: + """格式化一些文本信息, 可以根据 align_list 调整每列文本的距离""" + if not align_list: + align_list = [(30, '<'), (60, '<')] + formatted_text = [] + for txt, (int_align, flag) in zip(text_list, align_list): + formatted_text.append(f'{txt:{flag}{int_align}}') + return ' '.join(formatted_text) + + +def new_dc(msgs: str | float | int, fore_color: str = "", back_color: str = "") -> str: + """给文本上色 + + fore_color格式如下: + [{显示方式};{前景色};{背景色}m + + 显示方式 + 0(默认值)、1(高亮)、22(非粗体)、4(下划线)、24(非下划线)、 5(闪烁)、25(非闪烁)、7(反显)、27(非反显) + 前景色 + 30(黑色)、31(红色)、32(绿色)、 33(黄色)、34(蓝色)、35(洋 红)、36(青色)、37(白色) + 背景色 + 40(黑色)、41(红色)、42(绿色)、 43(黄色)、44(蓝色)、45(洋 红)、46(青色)、47(白色) + 如: + 高亮绿色红色背景 [1;32;41m + 默认-绿字体 [32m + """ + if fore_color == "": + fore_color = '[32m' # 默认绿色 + return "\033{}{}{}\033[0m".format(back_color, fore_color, str(msgs)) + + +def console_mute(func): + """采用装饰器的方式提供函数的控制台标准输出临时置空,净化控制台环境""" + + def wrapper(*args, **kwargs): + original_stdout = sys.stdout + try: + sys.stdout = io.StringIO() + return func(*args, **kwargs) + finally: + # 恢复原始的 sys.stdout 和 sys.stderr + sys.stdout = original_stdout + + return wrapper + + +def log_speed_ms(start_time: float, suffix: str = "", prefix: str = "", number_color: str = "", decimal: int = 4): + """控制台打印消耗的毫秒时间""" + log(f"{suffix}用时:{new_dc(str(round((time.time() - start_time) * 1000, decimal)), number_color)}ms{prefix}") + + +def speed_ms(start_time: float, decimal: int = 4): + """消耗的毫秒时间""" + return round((time.time() - start_time) * 1000, decimal) + + +class Logger: + """对控制台日志输出的二次封装,对部分常用日志进行预置""" + + @staticmethod + def debug(text: str, log_path: str = None): + """预置的debug形式的log""" + log_path = log_path if log_path else LOGGER_PATH + if isinstance(DEBUG, bool) and DEBUG: + log_plus(text, log_path, f"[{new_dc('INFO-DEBUG', '[34m')}]") + + @staticmethod + def info(text: str, log_path: str = None): + """预置的info形式的log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(text, log_path, f"[{new_dc('INFO', '[34m')}]") + + @staticmethod + def error(text: str, log_path: str = None): + """预置的error形式的log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(text, log_path, f"[{new_dc('ERROR', '[1;31m')}]") + + @staticmethod + def warn(text: str, log_path: str = None): + """预置的warn形式的log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(text, log_path, f"[{new_dc('WARN', '[1;33m')}]") + + @staticmethod + def init(text: str, log_path: str = None): + """预置的error形式的log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(text, log_path, f"[{new_dc('INIT', '[35m')}]") + + @staticmethod + def title(text: str, log_path: str = None): + """预置title形式的显目log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(new_dc(text, '[1m'), log_path, "🚀", "🚀") + + @staticmethod + def complete(text: str, log_path: str = None): + """预置complete形式的显目log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(new_dc(text, '[1;32m'), log_path, "✅", "✅") + + @staticmethod + def remove(text: str, log_path: str = None): + """预置remove形式的显目log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(new_dc(text, '[1m'), log_path, "🚮", "🚮") + + @staticmethod + def connect(text: str, log_path: str = None): + """预置connect形式的显目log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(new_dc(text, '[1;32m'), log_path, "🔗", "🔗") + + @staticmethod + def disconnect(text: str, log_path: str = None): + """预置disconnect形式的显目log""" + log_path = log_path if log_path else LOGGER_PATH + log_plus(new_dc(text, '[1m'), log_path, "🚫", "🚫") diff --git a/SCLP/utils/misc.py b/SCLP/utils/misc.py new file mode 100644 index 0000000..17e7df4 --- /dev/null +++ b/SCLP/utils/misc.py @@ -0,0 +1,360 @@ +# -*- coding:utf-8 -*- +""" +@Author : xuxingchen +@Contact : xuxingchen@sinochem.com +@Desc : 杂项 +""" +import base64 +import hashlib +import os +import time +import warnings +from io import BytesIO +import re +import random +import string +import threading +from datetime import datetime, timedelta +import socket +import psutil +from typing import Optional + +import numpy as np +import pandas as pd +import pytz +import requests +from PIL import Image +import paho.mqtt.client as mqtt +from openpyxl.styles import PatternFill, Font, Border, Side +from openpyxl.utils.dataframe import dataframe_to_rows +from openpyxl.workbook import Workbook +from pydantic import BaseModel + +from utils import logger + + +def get_ip_address(interface_name: str) -> tuple[str, str]: + interfaces = psutil.net_if_addrs() + interface = interfaces.get(interface_name, None) + ipv4_address, ipv6_address = "", "" + if interface: + for address in interface: + if address.family == socket.AF_INET: + ipv4_address = address.address + elif address.family == socket.AF_INET6 and not address.address.startswith("fe80"): + ipv6_address = address.address + return ipv4_address, ipv6_address + +def extract_fixed_length_number(number_str: str, fixed_length: int = 2) -> str: + """提取一串存在数值的字符串中的第一个数值,对其从右往左取值定值,不足补0""" + pattern = re.compile(r'[0-9]+') + number = pattern.search(number_str) + if number is None: + return "0" * fixed_length + else: + if len(number[0]) < fixed_length: + return "0" * (fixed_length - len(number[0])) + number[0] + else: + return number[0][-fixed_length:] + + +def snake2camel(key: str) -> str: + """snake命名风格转成camel命名风格""" + parts = key.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) + + +def snake2camel_list_dict(snake_list: list[dict]) -> list[dict]: + """将list[dict]中所有的snake格式的key转换成camel格式的key命名风格""" + camel_list = [] + for snake_dict in snake_list: + camel_dict = {} + for key, value in snake_dict.items(): + camel_dict[snake2camel(key)] = value + camel_list.append(camel_dict) + return camel_list + + +def clear_log_file(log_path: str, day: int = 7): + """每7天清除一次日志文件""" + creation_time = os.path.getctime(log_path) + days_since_creation = (time.time() - creation_time) / (60 * 60 * 24) + if os.path.exists(log_path) and days_since_creation >= day: + try: + f0 = open(log_path, "r", encoding="utf8") + f1 = open(f"{log_path}.old", "w", encoding="utf8") + f1.write(f0.read()) + f1.close() + f0.close() + os.remove(log_path) + print(f"日志文件 {logger.LOGGER_PATH} 完成重置") + except Exception as e: + print(f"日志文件 {logger.LOGGER_PATH} 重置失败: {e}") + + +def generate_captcha_text(characters: int = 6) -> str: + """生成指定长度的随机文本""" + letters_and_digits = string.ascii_letters + string.digits + return ''.join(random.choice(letters_and_digits) for _ in range(characters)) + + +def encrypt_number(phone_number: str, key: bytes = b'7A') -> str: + """将电话号码加密为字符串""" + phone_bytes = phone_number.encode('utf-8') # 将字符串转换为字节 + encrypted_bytes = bytes( + [byte ^ key[i % len(key)] for i, byte in enumerate(phone_bytes)] + ) + # 使用Base64编码加密后的字节 + encrypted_number = base64.b64encode(encrypted_bytes).decode('utf-8') + return encrypted_number + + +def decrypt_number(encrypted_number: str, key: bytes = b'7A') -> str: + """将字符串解密为电话号码""" + # 使用Base64解码加密后的字符串 + encrypted_bytes = base64.b64decode(encrypted_number) + decrypted_bytes = bytes( + [byte ^ key[i % len(key)] for i, byte in enumerate(encrypted_bytes)] + ) + decrypted_number = decrypted_bytes.decode('utf-8') + return decrypted_number + + +def now_tz_datetime(days: int = 0) -> str: + future_time = datetime.now() + timedelta(days=days) + return future_time.strftime("%Y-%m-%dT%H:%M:%S.") + f"{future_time.microsecond // 1000:03d}Z" + + +def now_datetime_nanosecond(days: int = 0) -> str: + future_time = datetime.now() + timedelta(days=days) + return future_time.strftime("%Y-%m-%d %H:%M:%S.%f") + + +def now_datetime_second(days: int = 0) -> str: + future_time = datetime.now() + timedelta(days=days) + return future_time.strftime("%Y-%m-%d %H:%M:%S") + + +def millisecond_timestamp2tz(timestamp_13: str): + timestamp = int(timestamp_13) / 1000 + dt_utc = datetime.fromtimestamp(timestamp, tz=pytz.UTC) + # 转换为所需的时区,这里以北京时间(China Standard Time)为例 + china_tz = pytz.timezone('Asia/Shanghai') + return dt_utc.astimezone(china_tz).strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt_utc.microsecond // 1000:03d}Z" + + +def is_image_url_valid(url: str) -> bool: + try: + # 发送请求获取URL内容 + response = requests.get(url) + response.raise_for_status() # 如果状态码不是200,会抛出异常 + + # 将内容加载为图片 + image = Image.open(BytesIO(response.content)) + image.verify() # 验证图像文件是否可读 + + # 如果上面的代码没有抛出异常,说明图片存在且格式可读 + return True + except (requests.RequestException, IOError): + # 如果有任何异常,说明图片不可用或格式不可读 + return False + + +def is_image_valid(path: str) -> bool: + try: + # 将内容加载为图片 + image = Image.open(open(path, 'rb')) + image.verify() # 验证图像文件是否可读 + + # 如果上面的代码没有抛出异常,说明图片存在且格式可读 + return True + except (requests.RequestException, IOError): + # 如果有任何异常,说明图片不可用或格式不可读 + return False + + +def get_file_md5(file_path): + content = open(file_path, 'rb') + md5hash = hashlib.md5(content.read()) + return md5hash.hexdigest() + + +def sql_export_xls(query, + db_connection, + save_file_path, + sheet_title, + sheet_header: Optional[list] = None, + header_background_color: str = "808080", + header_font_color: str = "ffffff"): + df = pd.read_sql_query(query, db_connection) + wb = Workbook() + ws = wb.active + ws.title = sheet_title + for i, r in enumerate(dataframe_to_rows(df, index=False, header=True)): + if sheet_header is not None and i == 0: + ws.append(sheet_header) + continue + ws.append(r) + # 表头样式 + header_fill = PatternFill(start_color=header_background_color, fill_type="solid") + header_font = Font(color=header_font_color, bold=True) + for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + # 边框样式 表头无边框-数据无顶实线框 + thin_border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + bottom=Side(style='thin') + ) + for i, row in enumerate(ws.iter_rows()): + if i == 0: + continue + for cell in row: + cell.border = thin_border + # 单元格宽度自适应调整 + for column in ws.columns: + max_length = 0 + column = list(column) + for cell in column: + if cell.value is not None: + cell_length = len(str(cell.value)) + if re.search(r'[\u4e00-\u9fff]', str(cell.value)): + cell_length += len(re.findall(r'[\u4e00-\u9fff]', str(cell.value))) + if cell_length > max_length: + max_length = cell_length + adjusted_width = (max_length + 2) + ws.column_dimensions[column[0].column_letter].width = adjusted_width + wb.save(save_file_path) + + +def valid_xls(file: BytesIO, required_columns: Optional[list]) -> tuple[bool, str, Optional[list]]: + """如果校验通过返回list结构的数据,如果检验不通过返回None""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + try: + df = pd.read_excel(file) + df = df.replace(np.nan, None) + if all(col in df.columns for col in required_columns): + return True, "", df[required_columns].values.tolist() + else: + missing_cols = [col for col in required_columns if col not in df.columns] + return False, f"缺少必要的列: {', '.join(missing_cols)}", None + except Exception as e: + return False, f"文件解析失败 {type(e).__name__}, {e}", None + + +class BasicCallback(BaseModel): + status: bool + message: Optional[str] + + +class InvalidException(Exception): + def __init__(self, message: str): + self.message = message + + +class UserData: + def __init__(self): + self.table_handle = None + self.topic: Optional[str] = None + self.topics: list = [] + self.table_handler = None + self.message = None + self.token = None + self.status: dict = {} + self.clients: dict = {} + self.lock = threading.Lock() # 添加一个锁用于线程同步 + + def set_table_handle(self, value): + with self.lock: + self.table_handle = value + + def set_topic(self, value: str): + with self.lock: + self.topic = value + + def set_topics(self, value: list): + with self.lock: + self.topics = value + + def set_table_handler(self, value): + with self.lock: + self.table_handler = value + + def set_message(self, value): + with self.lock: + self.message = value + + def set_token(self, value): + with self.lock: + self.token = value + + def set_status(self, value: dict): + with self.lock: + self.status = value + + def set_status_add(self, key, value): + with self.lock: + self.status[key] = value + + def set_status_remove(self, key): + with self.lock: + if self.status and key in self.status.keys(): + self.status.pop(key) + + def get_status(self, key): + if self.status and key in self.status.keys(): + return self.status[key] + + def set_clients(self, value: dict): + with self.lock: + self.clients = value + + def set_client_add(self, key, value): + with self.lock: + self.clients[key] = value + + +def create_mqtt_client(broker_host, + broker_port, + userdata: UserData, + on_message=None, + on_publish=None, + on_connect=None, + on_disconnect=None, + client_id: str = "", + username: str = "", + password: str = ""): + if client_id != "": + client = mqtt.Client(client_id=client_id) + else: + client = mqtt.Client() + client.user_data_set(userdata) + if on_connect: + client.on_connect = on_connect + if on_disconnect: + client.on_disconnect = on_disconnect + if on_message: + client.on_message = on_message + if on_publish: + client.on_publish = on_publish + client.username_pw_set(username, password) + client.connect(broker_host, broker_port) + return client + + +def on_connect(client, userdata, flags, rc): + logger.Logger.init(logger.new_dc(f"🔗 Mqtt connection! {{rc: {rc}}} 🔗", '[1;32m')) + if userdata.topics: + _topics = [(topic, 0) for topic in userdata.topics] + client.subscribe(_topics) + logger.Logger.debug(f"subscribe topics: {userdata.topics}") + + +def on_disconnect(client, userdata, rc): + logger.Logger.info(logger.new_dc(f"🔌 Break mqtt connection! {{rc: {rc}}} 🔌", "[1m")) + + +def on_publish(client, userdata, rc): + logger.Logger.debug(f"{userdata.topic} <- {userdata.message}")