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 0000000..b81d80c Binary files /dev/null and b/SCLP/data/mvboli.ttf differ 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}")