diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/dafuweng-saiadmin6.x.iml b/.idea/dafuweng-saiadmin6.x.iml new file mode 100644 index 0000000..d947920 --- /dev/null +++ b/.idea/dafuweng-saiadmin6.x.iml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d7db0b7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..9311fb5 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/phpspec.xml b/.idea/phpspec.xml new file mode 100644 index 0000000..98c82a9 --- /dev/null +++ b/.idea/phpspec.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..b0c1c68 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/saiadmin-artd/pnpm-lock.yaml b/saiadmin-artd/pnpm-lock.yaml index ebc4b0c..dcf6da0 100644 --- a/saiadmin-artd/pnpm-lock.yaml +++ b/saiadmin-artd/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: highlight.js: specifier: ^11.10.0 version: 11.11.1 + md-editor-v3: + specifier: ^6.3.1 + version: 6.3.1(vue@3.5.22(typescript@5.6.3)) mitt: specifier: ^3.0.1 version: 3.0.1 @@ -369,6 +372,96 @@ packages: '@cacheable/utils@2.0.2': resolution: {integrity: sha512-JTFM3raFhVv8LH95T7YnZbf2YoE9wEtkPPStuRF9a6ExZ103hFvs+QyCuYJ6r0hA9wRtbzgZtwUCoDWxssZd4Q==} + '@codemirror/autocomplete@6.20.1': + resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} + + '@codemirror/commands@6.10.2': + resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==} + + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/lang-jinja@6.0.0': + resolution: {integrity: sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.3.2': + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.2': + resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} + + '@codemirror/language-data@6.5.2': + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} + + '@codemirror/language@6.12.2': + resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==} + + '@codemirror/legacy-modes@6.5.2': + resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==} + + '@codemirror/lint@6.9.5': + resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==} + + '@codemirror/search@6.6.0': + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + + '@codemirror/state@6.5.4': + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} + + '@codemirror/view@6.39.16': + resolution: {integrity: sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==} + '@commitlint/cli@19.8.1': resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} engines: {node: '>=v18'} @@ -762,6 +855,60 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lezer/common@1.5.1': + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + + '@lezer/cpp@1.1.5': + resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} + + '@lezer/css@1.3.1': + resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + + '@lezer/markdown@1.6.3': + resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + + '@lezer/php@1.0.5': + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + + '@lezer/python@1.1.18': + resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -803,36 +950,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -909,56 +1062,67 @@ packages: resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.3': resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.3': resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.3': resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.3': resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.3': resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.3': resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.3': resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.3': resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.3': resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.3': resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.3': resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} @@ -1033,24 +1197,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.14': resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.14': resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.14': resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.14': resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} @@ -1100,12 +1268,21 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@24.8.1': resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} @@ -1197,6 +1374,12 @@ packages: peerDependencies: '@uppy/core': ^2.3.3 + '@vavt/copy2clipboard@1.0.3': + resolution: {integrity: sha512-HtG48r2FBYp9eRvGB3QGmtRBH1zzRRAVvFbGgFstOwz4/DDaNiX0uZc3YVKPydqgOav26pibr9MtoCaWxn7aeA==} + + '@vavt/util@2.1.1': + resolution: {integrity: sha512-QYn/B2qiQhAGZVBoe5/6pPcA3NiGcxIm9htpVBjcdIOZQaWe+8kE/akEgLPrHxv5dbQQawnyHBokadW1d1z1tw==} + '@vitejs/plugin-vue@6.0.1': resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1609,6 +1792,9 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + codepage@1.15.0: resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} engines: {node: '>=0.8'} @@ -1711,6 +1897,9 @@ packages: engines: {node: '>=0.8'} hasBin: true + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1731,6 +1920,9 @@ packages: engines: {node: '>=4'} hasBin: true + cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -2611,24 +2803,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -2653,6 +2849,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@15.5.2: resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} engines: {node: '>=18.12.0'} @@ -2751,12 +2950,37 @@ packages: resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} engines: {node: '>=0.10.0'} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-vue-next@0.543.0: + resolution: {integrity: sha512-Az5kpNm/koKAwSNIKjsZ4uHV2tVfmlQlcHwFBygQ8gc5/jFg7An9OrxgDy/aE5m+HLx7VfLYqDxLr8gWecZbQA==} + peerDependencies: + vue: '>=3.0.1' + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + markdown-it-image-figures@2.1.1: + resolution: {integrity: sha512-mwXSQ2nPeVUzCMIE3HlLvjRioopiqyJLNph0pyx38yf9mpqFDhNGnMpAXF9/A2Xv0oiF2cVyg9xwfF0HNAz05g==} + engines: {node: '>=12.0.0'} + peerDependencies: + markdown-it: '*' + + markdown-it-sub@2.0.0: + resolution: {integrity: sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA==} + + markdown-it-sup@2.0.0: + resolution: {integrity: sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA==} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2764,12 +2988,23 @@ packages: mathml-tag-names@2.1.3: resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + md-editor-v3@6.3.1: + resolution: {integrity: sha512-k9ynxS6Pexs+yNBuuWZ4cDpqvv/zD9p9lbPFGlCqhsCYyFJL7p/mYjUGNH3l8lsQJOUqlgbZe4Kta0ajN030Ug==} + peerDependencies: + vue: ^3.5.3 + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} mdn-data@2.24.0: resolution: {integrity: sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + medium-zoom@1.1.0: + resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -3127,6 +3362,10 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3372,6 +3611,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + stylelint-config-html@1.1.0: resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} engines: {node: ^12 || >=14} @@ -3551,6 +3793,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -3754,6 +3999,9 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -3822,6 +4070,11 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xss@1.0.15: + resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} + engines: {node: '>= 0.10.0'} + hasBin: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4072,6 +4325,254 @@ snapshots: '@cacheable/utils@2.0.2': {} + '@codemirror/autocomplete@6.20.1': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + + '@codemirror/commands@6.10.2': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/cpp': 1.1.5 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.1 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.1 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/java': 1.1.3 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-jinja@6.0.0': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-liquid@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/markdown': 1.6.3 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/php': 1.0.5 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/python': 1.1.18 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.12.2 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.2': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + '@lezer/yaml': 1.0.4 + + '@codemirror/language-data@6.5.2': + dependencies: + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.11 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.0 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2 + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.0 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2 + '@codemirror/lang-sql': 6.10.0 + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.2 + '@codemirror/language': 6.12.2 + '@codemirror/legacy-modes': 6.5.2 + + '@codemirror/language@6.12.2': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + + '@codemirror/legacy-modes@6.5.2': + dependencies: + '@codemirror/language': 6.12.2 + + '@codemirror/lint@6.9.5': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + crelt: 1.0.6 + + '@codemirror/search@6.6.0': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + crelt: 1.0.6 + + '@codemirror/state@6.5.4': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.39.16': + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@commitlint/cli@19.8.1(@types/node@24.8.1)(typescript@5.6.3)': dependencies: '@commitlint/format': 19.8.1 @@ -4444,6 +4945,101 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@lezer/common@1.5.1': {} + + '@lezer/cpp@1.1.5': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/css@1.3.1': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/go@1.0.1': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/java@1.1.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/lr@1.4.8': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/markdown@1.6.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.5': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/python@1.1.18': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/sass@1.1.0': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@marijn/find-cluster-break@1.0.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4686,12 +5282,21 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/linkify-it@5.0.0': {} + '@types/lodash-es@4.17.12': dependencies: '@types/lodash': 4.17.20 '@types/lodash@4.17.20': {} + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/node@24.8.1': dependencies: undici-types: 7.14.0 @@ -4826,6 +5431,10 @@ snapshots: '@uppy/utils': 4.1.3 nanoid: 3.3.11 + '@vavt/copy2clipboard@1.0.3': {} + + '@vavt/util@2.1.1': {} + '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 @@ -5329,6 +5938,16 @@ snapshots: clone@1.0.4: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/commands': 6.10.2 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.5 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + codepage@1.15.0: {} color-convert@1.9.3: @@ -5431,6 +6050,8 @@ snapshots: crc-32@1.2.2: {} + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5448,6 +6069,8 @@ snapshots: cssesc@3.0.0: {} + cssfilter@0.0.10: {} + csstype@3.1.3: {} cz-conventional-changelog@3.3.0(@types/node@24.8.1)(typescript@5.6.3): @@ -6364,6 +6987,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lint-staged@15.5.2: dependencies: chalk: 5.6.2 @@ -6461,22 +7088,74 @@ snapshots: longest@2.0.1: {} + lru-cache@11.2.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lucide-vue-next@0.543.0(vue@3.5.22(typescript@5.6.3)): + dependencies: + vue: 3.5.22(typescript@5.6.3) + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it-image-figures@2.1.1(markdown-it@14.1.1): + dependencies: + markdown-it: 14.1.1 + + markdown-it-sub@2.0.0: {} + + markdown-it-sup@2.0.0: {} + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} mathml-tag-names@2.1.3: {} + md-editor-v3@6.3.1(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/commands': 6.10.2 + '@codemirror/lang-markdown': 6.5.0 + '@codemirror/language': 6.12.2 + '@codemirror/language-data': 6.5.2 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.16 + '@lezer/highlight': 1.2.3 + '@types/markdown-it': 14.1.2 + '@vavt/copy2clipboard': 1.0.3 + '@vavt/util': 2.1.1 + codemirror: 6.0.2 + lru-cache: 11.2.6 + lucide-vue-next: 0.543.0(vue@3.5.22(typescript@5.6.3)) + markdown-it: 14.1.1 + markdown-it-image-figures: 2.1.1(markdown-it@14.1.1) + markdown-it-sub: 2.0.0 + markdown-it-sup: 2.0.0 + medium-zoom: 1.1.0 + vue: 3.5.22(typescript@5.6.3) + xss: 1.0.15 + mdn-data@2.12.2: {} mdn-data@2.24.0: {} + mdurl@2.0.0: {} + + medium-zoom@1.1.0: {} + memoize-one@6.0.0: {} meow@12.1.1: {} @@ -6780,6 +7459,8 @@ snapshots: proxy-from-env@1.1.0: {} + punycode.js@2.3.1: {} + punycode@2.3.1: {} qrcode.vue@3.6.0(vue@3.5.22(typescript@5.6.3)): @@ -7010,6 +7691,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + style-mod@4.1.3: {} + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): dependencies: postcss-html: 1.8.0 @@ -7223,6 +7906,8 @@ snapshots: typescript@5.6.3: {} + uc.micro@2.1.0: {} + ufo@1.6.1: {} undici-types@7.14.0: {} @@ -7451,6 +8136,8 @@ snapshots: optionalDependencies: typescript: 5.6.3 + w3c-keyname@2.2.8: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -7522,6 +8209,11 @@ snapshots: xml-name-validator@4.0.0: {} + xss@1.0.15: + dependencies: + commander: 2.20.3 + cssfilter: 0.0.10 + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/server/composer.lock b/server/composer.lock new file mode 100644 index 0000000..f727a1a --- /dev/null +++ b/server/composer.lock @@ -0,0 +1,6747 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0f81080058312194fb44472e562fd0c7", + "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v7.0.3" + }, + "time": "2026-02-25T22:16:40+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "godruoyi/php-snowflake", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/godruoyi/php-snowflake.git", + "reference": "1f32d0a27bb88adb66e7bdeb93579d5aed2ddd71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/godruoyi/php-snowflake/zipball/1f32d0a27bb88adb66e7bdeb93579d5aed2ddd71", + "reference": "1f32d0a27bb88adb66e7bdeb93579d5aed2ddd71", + "shasum": "" + }, + "require": { + "php-64bit": ">=8.1" + }, + "require-dev": { + "ext-redis": "*", + "ext-swoole": "*", + "illuminate/contracts": "^10.0 || ^11.0", + "laravel/pint": "^1.10", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10", + "predis/predis": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Godruoyi\\Snowflake\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Godruoyi", + "email": "g@godruoyi.com" + } + ], + "description": "An ID Generator for PHP based on Snowflake Algorithm (Twitter announced).", + "homepage": "https://github.com/godruoyi/php-snowflake", + "keywords": [ + "Unique ID", + "laravel snowflake", + "order id", + "php snowflake", + "php sonyflake", + "php unique id", + "snowflake algorithm", + "sonyflake", + "unique order id" + ], + "support": { + "issues": "https://github.com/godruoyi/php-snowflake/issues", + "source": "https://github.com/godruoyi/php-snowflake/tree/3.2.2" + }, + "funding": [ + { + "url": "https://images.godruoyi.com/wechat.png", + "type": "custom" + } + ], + "time": "2026-01-31T07:38:00+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "illuminate/bus", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "c2ceb60c70961815f88da24ab0b7a7f32d47cf4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/c2ceb60c70961815f88da24ab0b7a7f32d47cf4e", + "reference": "c2ceb60c70961815f88da24ab0b7a7f32d47cf4e", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/pipeline": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:43:34+00:00" + }, + { + "name": "illuminate/collections", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "f35c084f0d9bc57895515cb4d0665797c66285fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/f35c084f0d9bc57895515cb4d0665797c66285fd", + "reference": "f35c084f0d9bc57895515cb4d0665797c66285fd", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-16T14:10:38+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/container", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "648307e8f54bcd9450c858f99abd11bc50c364a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/648307e8f54bcd9450c858f99abd11bc50c364a0", + "reference": "648307e8f54bcd9450c858f99abd11bc50c364a0", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/reflection": "^12.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-12T16:13:27+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "099fd9b56ccaf776facaa27699b960a3f2451127" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/099fd9b56ccaf776facaa27699b960a3f2451127", + "reference": "099fd9b56ccaf776facaa27699b960a3f2451127", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-20T14:37:40+00:00" + }, + { + "name": "illuminate/database", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "c685208bdd8bfec3e13c7b52f6235b6f3bde9fea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/c685208bdd8bfec3e13c7b52f6235b6f3bde9fea", + "reference": "c685208bdd8bfec3e13c7b52f6235b6f3bde9fea", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "ext-pdo": "*", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "laravel/serializable-closure": "^1.3|^2.0", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^12.0).", + "illuminate/events": "Required to use the observers with Eloquent (^12.0).", + "illuminate/filesystem": "Required to use the migrations (^12.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^12.0).", + "illuminate/pagination": "Required to paginate the result set (^12.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T00:20:44+00:00" + }, + { + "name": "illuminate/events", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "b71e42451496175f8fd898cb6a67ad7fd613d00b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/b71e42451496175f8fd898cb6a67ad7fd613d00b", + "reference": "b71e42451496175f8fd898cb6a67ad7fd613d00b", + "shasum": "" + }, + "require": { + "illuminate/bus": "^12.0", + "illuminate/collections": "^12.0", + "illuminate/container": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:43:34+00:00" + }, + { + "name": "illuminate/filesystem", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/filesystem.git", + "reference": "c4c3f8612f218afcf09f3c7f5c7dc9e282626800" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/c4c3f8612f218afcf09f3c7f5c7dc9e282626800", + "reference": "c4c3f8612f218afcf09f3c7f5c7dc9e282626800", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0" + }, + "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", + "illuminate/http": "Required for handling uploaded files (^12.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.25.1).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/mime": "Required to enable support for guessing extensions (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Filesystem package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-13T20:26:32+00:00" + }, + { + "name": "illuminate/http", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "b6351a4b8d3b6b1aef4b08cb98892045b20aa0ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/b6351a4b8d3b6b1aef4b08cb98892045b20aa0ad", + "reference": "b6351a4b8d3b6b1aef4b08cb98892045b20aa0ad", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/session": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:41:33+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-07-23T16:31:01+00:00" + }, + { + "name": "illuminate/pagination", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pagination.git", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pagination/zipball/87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "reference": "87e7e3e7b02d6809b1bcd41782e1ca2c6d2a413b", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pagination\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pagination package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-16T14:36:17+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "reference": "b6a14c20d69a44bf0a6fba664a00d23ca71770ee", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/database": "Required to use database transactions (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-08-20T13:36:50+00:00" + }, + { + "name": "illuminate/redis", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/redis.git", + "reference": "26f41f22f285295f8eeb1e00ba3ff3f24021a9eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/redis/zipball/26f41f22f285295f8eeb1e00ba3ff3f24021a9eb", + "reference": "26f41f22f285295f8eeb1e00ba3ff3f24021a9eb", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2" + }, + "suggest": { + "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Redis\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Redis package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-01-02T17:50:54+00:00" + }, + { + "name": "illuminate/reflection", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/reflection.git", + "reference": "6188e97a587371b9951c2a7e337cd760308c17d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/reflection/zipball/6188e97a587371b9951c2a7e337cd760308c17d7", + "reference": "6188e97a587371b9951c2a7e337cd760308c17d7", + "shasum": "" + }, + "require": { + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Reflection package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-04T15:21:22+00:00" + }, + { + "name": "illuminate/session", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e", + "reference": "98802e67dd5e059c0b978b3fe8f5f0a3ac17ec4e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/filesystem": "^12.0", + "illuminate/support": "^12.0", + "php": "^8.2", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^12.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-14T23:03:41+00:00" + }, + { + "name": "illuminate/support", + "version": "v12.53.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "18d7d75366ddb9eded3b7f05173f791da47faf34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/18d7d75366ddb9eded3b7f05173f791da47faf34", + "reference": "18d7d75366ddb9eded3b7f05173f791da47faf34", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^12.0", + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "illuminate/reflection": "^12.0", + "nesbot/carbon": "^3.8.4", + "php": "^8.2", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php85": "^1.33", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^12.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.2).", + "symfony/uid": "Required to use Str::ulid() (^7.2).", + "symfony/var-dumper": "Required to use the dd function (^7.2).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-23T15:44:06+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "37308608e599f34a1a4845b16440047ec98a172a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a", + "reference": "37308608e599f34a1a4845b16440047ec98a172a", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-01T13:05:00+00:00" + }, + { + "name": "nelexa/zip", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/Ne-Lexa/php-zip.git", + "reference": "88a1b6549be813278ff2dd3b6b2ac188827634a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ne-Lexa/php-zip/zipball/88a1b6549be813278ff2dd3b6b2ac188827634a7", + "reference": "88a1b6549be813278ff2dd3b6b2ac188827634a7", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": "^7.4 || ^8.0", + "psr/http-message": "*", + "symfony/finder": "*" + }, + "require-dev": { + "ext-bz2": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-iconv": "*", + "ext-openssl": "*", + "ext-xml": "*", + "friendsofphp/php-cs-fixer": "^3.4.0", + "guzzlehttp/psr7": "^1.6", + "phpunit/phpunit": "^9", + "symfony/http-foundation": "*", + "symfony/var-dumper": "*", + "vimeo/psalm": "^4.6" + }, + "suggest": { + "ext-bz2": "Needed to support BZIP2 compression", + "ext-fileinfo": "Needed to get mime-type file", + "ext-iconv": "Needed to support convert zip entry name to requested character encoding", + "ext-openssl": "Needed to support encrypt zip entries or use ext-mcrypt" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpZip\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ne-Lexa", + "email": "alexey@nelexa.ru", + "role": "Developer" + } + ], + "description": "PhpZip is a php-library for extended work with ZIP-archives. Open, create, update, delete, extract and get info tool. Supports appending to existing ZIP files, WinZip AES encryption, Traditional PKWARE Encryption, BZIP2 compression, external file attributes and ZIP64 extensions. Alternative ZipArchive. It does not require php-zip extension.", + "homepage": "https://github.com/Ne-Lexa/php-zip", + "keywords": [ + "archive", + "extract", + "unzip", + "winzip", + "zip", + "ziparchive" + ], + "support": { + "issues": "https://github.com/Ne-Lexa/php-zip/issues", + "source": "https://github.com/Ne-Lexa/php-zip/tree/4.0.2" + }, + "time": "2022-06-17T11:17:46+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:26:29+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "openspout/openspout", + "version": "v4.28.5", + "source": { + "type": "git", + "url": "https://github.com/openspout/openspout.git", + "reference": "ab05a09fe6fce57c90338f83280648a9786ce36b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openspout/openspout/zipball/ab05a09fe6fce57c90338f83280648a9786ce36b", + "reference": "ab05a09fe6fce57c90338f83280648a9786ce36b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-libxml": "*", + "ext-xmlreader": "*", + "ext-zip": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "ext-zlib": "*", + "friendsofphp/php-cs-fixer": "^3.68.3", + "infection/infection": "^0.29.10", + "phpbench/phpbench": "^1.4.0", + "phpstan/phpstan": "^2.1.2", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^11.5.4" + }, + "suggest": { + "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)", + "ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenSpout\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adrien Loison", + "email": "adrien@box.com" + } + ], + "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way", + "homepage": "https://github.com/openspout/openspout", + "keywords": [ + "OOXML", + "csv", + "excel", + "memory", + "odf", + "ods", + "office", + "open", + "php", + "read", + "scale", + "spreadsheet", + "stream", + "write", + "xlsx" + ], + "support": { + "issues": "https://github.com/openspout/openspout/issues", + "source": "https://github.com/openspout/openspout/tree/v4.28.5" + }, + "funding": [ + { + "url": "https://paypal.me/filippotessarotto", + "type": "custom" + }, + { + "url": "https://github.com/Slamdunk", + "type": "github" + } + ], + "time": "2025-01-30T13:51:11+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.12.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2025-10-15T16:49:08+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "saithink/saiadmin", + "version": "6.0.7", + "source": { + "type": "git", + "url": "https://github.com/saithink/saiadmin.git", + "reference": "3f588dda8d3889db14cefcc265b6ae5e3c7ef368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/saithink/saiadmin/zipball/3f588dda8d3889db14cefcc265b6ae5e3c7ef368", + "reference": "3f588dda8d3889db14cefcc265b6ae5e3c7ef368", + "shasum": "" + }, + "require": { + "godruoyi/php-snowflake": "^3.0.0", + "guzzlehttp/guzzle": "^7.9", + "illuminate/events": "^12.43", + "illuminate/pagination": "^12.43", + "openspout/openspout": "^4.0", + "php": ">=8.1", + "phpmailer/phpmailer": "^6.9", + "ramsey/uuid": "^4.0", + "tinywan/jwt": "^1.11", + "tinywan/storage": "^1.1", + "topthink/think-orm": "^2.0.53 || ^3.0.0", + "topthink/think-validate": "^3.0", + "twig/twig": "^3.20", + "vlucas/phpdotenv": "^5.6", + "webman/cache": "^2.1", + "webman/captcha": "^1.0", + "webman/channel": "^2.1", + "webman/console": "^2.1", + "webman/database": "^2.1", + "webman/event": "^1.0", + "webman/redis": "^2.1", + "webman/think-cache": "^2.1", + "webman/think-orm": "^2.1", + "workerman/crontab": "^1.0", + "zoujingli/ip2region": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\saiadmin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "saithink", + "email": "1430792918@qq.com" + } + ], + "description": "webman plugin", + "support": { + "issues": "https://github.com/saithink/saiadmin/issues", + "source": "https://github.com/saithink/saiadmin/tree/6.0.7" + }, + "time": "2026-02-28T08:00:23+00:00" + }, + { + "name": "saithink/saipackage", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/saithink/saipackage.git", + "reference": "bc41b9dc89c38f6469b7524e57c6f85fb5e2a1e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/saithink/saipackage/zipball/bc41b9dc89c38f6469b7524e57c6f85fb5e2a1e8", + "reference": "bc41b9dc89c38f6469b7524e57c6f85fb5e2a1e8", + "shasum": "" + }, + "require": { + "nelexa/zip": "4.x" + }, + "type": "library", + "autoload": { + "psr-4": { + "Saithink\\Saipackage\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "saithink", + "email": "1430792918@qq.com" + } + ], + "description": "saiadmin plugin", + "support": { + "issues": "https://github.com/saithink/saipackage/issues", + "source": "https://github.com/saithink/saipackage/tree/6.0.1" + }, + "time": "2026-01-16T00:26:20+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.3.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "e3e76b9ba0dff3dfe08ebda500723976dd9de407" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/e3e76b9ba0dff3dfe08ebda500723976dd9de407", + "reference": "e3e76b9ba0dff3dfe08ebda500723976dd9de407", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.3.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:12:03+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6d643a93b47398599124022eb24d97c153c12f27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", + "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T17:02:47+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:34+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:40:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-21T16:25:55+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-26T08:30:57+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-05T15:57:06+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T09:33:46+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T07:53:42+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-15T10:53:20+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:15:23+00:00" + }, + { + "name": "tinywan/jwt", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/Tinywan/webman-jwt.git", + "reference": "bd1943f54e9d5489cd5126210e1da97f3dc40928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Tinywan/webman-jwt/zipball/bd1943f54e9d5489cd5126210e1da97f3dc40928", + "reference": "bd1943f54e9d5489cd5126210e1da97f3dc40928", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.8||^7.0", + "php": "^7.1||^8.0", + "workerman/webman-framework": "^1.2.1||^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.6", + "illuminate/database": "^8.83", + "mockery/mockery": "^1.5", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.0", + "topthink/think-orm": "^2.0", + "vimeo/psalm": "^4.22", + "workerman/webman": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tinywan\\Jwt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "JSON Web Token (JWT) for webman plugin", + "support": { + "issues": "https://github.com/Tinywan/webman-jwt/issues", + "source": "https://github.com/Tinywan/webman-jwt/tree/v1.15.0" + }, + "time": "2026-02-26T03:42:53+00:00" + }, + { + "name": "tinywan/storage", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/Tinywan/webman-storage.git", + "reference": "a39584356829860fd2acab396ded3e6d730c71ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Tinywan/webman-storage/zipball/a39584356829860fd2acab396ded3e6d730c71ae", + "reference": "a39584356829860fd2acab396ded3e6d730c71ae", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "workerman/webman-framework": "^1.2.1||^2.0" + }, + "require-dev": { + "aliyuncs/oss-sdk-php": "^2.4", + "friendsofphp/php-cs-fixer": "^3.6", + "league/flysystem-aws-s3-v3": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9.5", + "qcloud/cos-sdk-v5": "^2.5", + "qiniu/php-sdk": "^7.4", + "workerman/webman": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tinywan\\Storage\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "webman storage plugin", + "support": { + "issues": "https://github.com/Tinywan/webman-storage/issues", + "source": "https://github.com/Tinywan/webman-storage/tree/v1.1.2" + }, + "time": "2025-08-21T02:16:10+00:00" + }, + { + "name": "topthink/think-container", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-container.git", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-container/zipball/b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "reference": "b2df244be1e7399ad4c8be1ccc40ed57868f730a", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "psr/container": "^2.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "PHP Container & Facade Manager", + "support": { + "issues": "https://github.com/top-think/think-container/issues", + "source": "https://github.com/top-think/think-container/tree/v3.0.2" + }, + "time": "2025-04-07T03:21:51+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v3.1.12", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb", + "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP6 Helper Package", + "support": { + "issues": "https://github.com/top-think/think-helper/issues", + "source": "https://github.com/top-think/think-helper/tree/v3.1.12" + }, + "time": "2025-12-26T09:58:29+00:00" + }, + { + "name": "topthink/think-orm", + "version": "v3.0.34", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-orm.git", + "reference": "715e55da149fe32a12d68ef10e5b00e70bd3dbec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-orm/zipball/715e55da149fe32a12d68ef10e5b00e70bd3dbec", + "reference": "715e55da149fe32a12d68ef10e5b00e70bd3dbec", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pdo": "*", + "php": ">=8.0.0", + "psr/log": ">=1.0", + "psr/simple-cache": ">=1.0", + "topthink/think-helper": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6|^10" + }, + "suggest": { + "ext-mongodb": "provide mongodb support" + }, + "type": "library", + "autoload": { + "files": [ + "stubs/load_stubs.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the PHP Database&ORM Framework", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/top-think/think-orm/issues", + "source": "https://github.com/top-think/think-orm/tree/v3.0.34" + }, + "time": "2025-01-14T06:03:33+00:00" + }, + { + "name": "topthink/think-validate", + "version": "v3.0.7", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-validate.git", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-validate/zipball/85063f6d4ef8ed122f17a36179dc3e0949b30988", + "reference": "85063f6d4ef8ed122f17a36179dc3e0949b30988", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "topthink/think-container": ">=3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "think validate", + "support": { + "issues": "https://github.com/top-think/think-validate/issues", + "source": "https://github.com/top-think/think-validate/tree/v3.0.7" + }, + "time": "2025-06-11T05:51:40+00:00" + }, + { + "name": "twig/twig", + "version": "v3.23.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-01-23T21:00:41+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webman/cache", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/cache.git", + "reference": "8600b7a5172e3848f3f0d707a36d37ee9b8328d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/cache/zipball/8600b7a5172e3848f3f0d707a36d37ee9b8328d8", + "reference": "8600b7a5172e3848f3f0d707a36d37ee9b8328d8", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/simple-cache": "^3.0", + "symfony/cache": "^6.0 || ^7.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/cache/issues", + "source": "https://github.com/webman-php/cache/tree/v2.1.3" + }, + "time": "2025-11-25T12:35:45+00:00" + }, + { + "name": "webman/captcha", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/webman-php/captcha.git", + "reference": "0b2645b813466e4e70bff311511364080bad2ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/captcha/zipball/0b2645b813466e4e70bff311511364080bad2ec5", + "reference": "0b2645b813466e4e70bff311511364080bad2ec5", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-mbstring": "*", + "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Captcha\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net" + }, + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jeremy Livingston", + "email": "jeremy.j.livingston@gmail.com" + } + ], + "description": "Captcha generator", + "keywords": [ + "bot", + "captcha", + "spam" + ], + "support": { + "source": "https://github.com/webman-php/captcha/tree/v1.0.5" + }, + "time": "2025-03-01T08:43:36+00:00" + }, + { + "name": "webman/channel", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/webman-php/channel.git", + "reference": "148eb5ed53bca18d7da030d709d2d831164a7c27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/channel/zipball/148eb5ed53bca18d7da030d709d2d831164a7c27", + "reference": "148eb5ed53bca18d7da030d709d2d831164a7c27", + "shasum": "" + }, + "require": { + "workerman/channel": "^1.2.2", + "workerman/webman-framework": ">=2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Channel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/channel/issues", + "source": "https://github.com/webman-php/channel/tree/v2.1.0" + }, + "time": "2025-02-07T09:03:19+00:00" + }, + { + "name": "webman/console", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/webman-php/console.git", + "reference": "e66e21c3db1685ac76841df3316d488b25e23d81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/console/zipball/e66e21c3db1685ac76841df3316d488b25e23d81", + "reference": "e66e21c3db1685ac76841df3316d488b25e23d81", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "php": ">=8.1", + "symfony/console": "^6.1 || ^7.0" + }, + "require-dev": { + "workerman/webman": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "Webman console", + "homepage": "http://www.workerman.net", + "keywords": [ + "webman console" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://www.workerman.net/questions", + "issues": "https://github.com/webman-php/console/issues", + "source": "https://github.com/webman-php/console", + "wiki": "http://www.workerman.net/doc/webman" + }, + "time": "2026-02-26T02:45:43+00:00" + }, + { + "name": "webman/database", + "version": "v2.1.9", + "source": { + "type": "git", + "url": "https://github.com/webman-php/database.git", + "reference": "5a1463c96c79b35225f1cbd2f3e65830a7b4da0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/database/zipball/5a1463c96c79b35225f1cbd2f3e65830a7b4da0e", + "reference": "5a1463c96c79b35225f1cbd2f3e65830a7b4da0e", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0 || ^11.0 || ^12.0", + "illuminate/http": "^10.0 || ^11.0 || ^12.0", + "laravel/serializable-closure": "^1.0 || ^2.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Database\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman database", + "support": { + "issues": "https://github.com/webman-php/database/issues", + "source": "https://github.com/webman-php/database/tree/v2.1.9" + }, + "time": "2026-02-02T10:45:05+00:00" + }, + { + "name": "webman/event", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/webman-php/event.git", + "reference": "b1c3f6b70fd290e48288703d59bead0e28f9fb84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/event/zipball/b1c3f6b70fd290e48288703d59bead0e28f9fb84", + "reference": "b1c3f6b70fd290e48288703d59bead0e28f9fb84", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webman\\Event\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman event plugin", + "support": { + "issues": "https://github.com/webman-php/event/issues", + "source": "https://github.com/webman-php/event/tree/v1.0.5" + }, + "time": "2023-12-04T09:22:12+00:00" + }, + { + "name": "webman/redis", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/redis.git", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/redis/zipball/559eb1692d39c6fef5cf526223fff728be6c0fb9", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9", + "shasum": "" + }, + "require": { + "illuminate/redis": "^10.0 || ^11.0 || ^12.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Redis\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman redis", + "support": { + "issues": "https://github.com/webman-php/redis/issues", + "source": "https://github.com/webman-php/redis/tree/v2.1.3" + }, + "time": "2025-03-14T03:52:14+00:00" + }, + { + "name": "webman/think-cache", + "version": "v2.1.4", + "source": { + "type": "git", + "url": "https://github.com/webman-php/think-cache.git", + "reference": "8af05ef62388db6a2be9d5010bc1c314ef772e7b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/think-cache/zipball/8af05ef62388db6a2be9d5010bc1c314ef772e7b", + "reference": "8af05ef62388db6a2be9d5010bc1c314ef772e7b", + "shasum": "" + }, + "require": { + "psr/simple-cache": "^1.0|^2.0|^3.0", + "topthink/think-container": "^2.0|^3.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\ThinkCache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/think-cache/issues", + "source": "https://github.com/webman-php/think-cache/tree/v2.1.4" + }, + "time": "2025-12-17T03:13:13+00:00" + }, + { + "name": "webman/think-orm", + "version": "v2.1.10", + "source": { + "type": "git", + "url": "https://github.com/webman-php/think-orm.git", + "reference": "b779fce0de4d66e08eb8925eb5841107d1ca9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/think-orm/zipball/b779fce0de4d66e08eb8925eb5841107d1ca9ec4", + "reference": "b779fce0de4d66e08eb8925eb5841107d1ca9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "topthink/think-container": "^2.0|^3.0", + "topthink/think-orm": "^2.0.53 || ^3.0.0 || ^4.0.30 || dev-master", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\ThinkOrm\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/webman-php/think-orm/issues", + "source": "https://github.com/webman-php/think-orm/tree/v2.1.10" + }, + "time": "2026-02-04T09:07:18+00:00" + }, + { + "name": "workerman/channel", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/walkor/channel.git", + "reference": "5edb0008eae35bf2da7218d911042abd23aa4370" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/channel/zipball/5edb0008eae35bf2da7218d911042abd23aa4370", + "reference": "5edb0008eae35bf2da7218d911042abd23aa4370", + "shasum": "" + }, + "require": { + "workerman/workerman": ">=4.0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Channel\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "homepage": "http://www.workerman.net", + "support": { + "issues": "https://github.com/walkor/channel/issues", + "source": "https://github.com/walkor/channel/tree/v1.2.3" + }, + "time": "2025-07-08T01:33:22+00:00" + }, + { + "name": "workerman/coroutine", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/workerman-php/coroutine.git", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "reference": "b0bebfa9d41b992ad0a835ddf2ee8fa5d58eca44", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "psr/log": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "src", + "Workerman\\Coroutine\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Workerman coroutine", + "support": { + "issues": "https://github.com/workerman-php/coroutine/issues", + "source": "https://github.com/workerman-php/coroutine/tree/v1.1.4" + }, + "time": "2025-10-11T15:09:08+00:00" + }, + { + "name": "workerman/crontab", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/walkor/crontab.git", + "reference": "74f51ca8204e8eb628e57bc0e640561d570da2cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/crontab/zipball/74f51ca8204e8eb628e57bc0e640561d570da2cb", + "reference": "74f51ca8204e8eb628e57bc0e640561d570da2cb", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "workerman/workerman": ">=4.0.20" + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\Crontab\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "A crontab written in PHP based on workerman", + "homepage": "http://www.workerman.net", + "keywords": [ + "crontab" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://wenda.workerman.net/", + "issues": "https://github.com/walkor/workerman/issues", + "source": "https://github.com/walkor/crontab", + "wiki": "http://doc.workerman.net/" + }, + "time": "2025-01-15T07:20:50+00:00" + }, + { + "name": "workerman/webman-framework", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/walkor/webman-framework.git", + "reference": "2da4e49259d41925f1732f95f6fb052a3f42ceee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/webman-framework/zipball/2da4e49259d41925f1732f95f6fb052a3f42ceee", + "reference": "2da4e49259d41925f1732f95f6fb052a3f42ceee", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": ">=8.1", + "psr/container": ">=1.0", + "psr/log": "^2.0 || ^3.0", + "workerman/workerman": "^5.1 || dev-master" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "type": "library", + "autoload": { + "files": [ + "./src/support/helpers.php" + ], + "psr-4": { + "Webman\\": "./src", + "Support\\": "./src/support", + "support\\": "./src/support", + "Support\\View\\": "./src/support/view", + "Support\\Bootstrap\\": "./src/support/bootstrap", + "Support\\Exception\\": "./src/support/exception" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "https://www.workerman.net", + "role": "Developer" + } + ], + "description": "High performance HTTP Service Framework.", + "homepage": "https://www.workerman.net", + "keywords": [ + "High Performance", + "http service" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "https://wenda.workerman.net/", + "issues": "https://github.com/walkor/webman/issues", + "source": "https://github.com/walkor/webman-framework", + "wiki": "https://doc.workerman.net/" + }, + "time": "2026-02-20T02:37:25+00:00" + }, + { + "name": "workerman/workerman", + "version": "v5.1.9", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "fff0954628f8ceeccfe29d3e817f0fad87cfdbf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/fff0954628f8ceeccfe29d3e817f0fad87cfdbf2", + "reference": "fff0954628f8ceeccfe29d3e817f0fad87cfdbf2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "workerman/coroutine": "^1.1 || dev-main" + }, + "conflict": { + "ext-swow": "=5.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ], + "classmap": [ + "Ip2Region.php", + "XdbSearcher.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com", + "homepage": "https://thinkadmin.top", + "role": "Developer" + } + ], + "description": "IP2Region v2.0 for PHP - 轻量级 IP 地理位置查询库,支持 IPv4,高性能,零依赖", + "homepage": "https://github.com/zoujingli/Ip2Region", + "keywords": [ + "IP", + "Ip2Region", + "composer", + "geolocation", + "ipv4", + "location", + "php", + "轻量级", + "零依赖", + "高性能" + ], + "support": { + "issues": "https://github.com/zoujingli/Ip2Region/issues", + "source": "https://github.com/zoujingli/Ip2Region" + }, + "funding": [ + { + "url": "https://github.com/sponsors/zoujingli", + "type": "github" + } + ], + "time": "2025-09-16T01:31:03+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/server/config/bootstrap.php b/server/config/bootstrap.php index 95d2e87..ce7bfb2 100644 --- a/server/config/bootstrap.php +++ b/server/config/bootstrap.php @@ -14,4 +14,5 @@ return [ support\bootstrap\Session::class, + Webman\ThinkOrm\ThinkOrm::class, ]; diff --git a/server/config/event.php b/server/config/event.php new file mode 100644 index 0000000..28a3b2c --- /dev/null +++ b/server/config/event.php @@ -0,0 +1,5 @@ + true, + 'jwt' => [ + /** 算法类型 HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、ES512、PS256、PS384、PS512 */ + 'algorithms' => 'HS256', + + /** access令牌秘钥(安装时自动生成64位随机值) */ + 'access_secret_key' => 'd7d2058d654799bfdf290fb91757681793e6549effc2e41c6e7dafe3ecfcbc8a', + + /** access令牌过期时间,单位:秒。默认 2 小时 */ + 'access_exp' => 7200, + + /** refresh令牌秘钥(安装时自动生成64位随机值) */ + 'refresh_secret_key' => 'f02fc466bd03a133fa8f30bd58f302f4ea706c41eff521b6319a8af099b11e64', + + /** refresh令牌过期时间,单位:秒。默认 7 天 */ + 'refresh_exp' => 604800, + + /** refresh 令牌是否禁用,默认不禁用 false */ + 'refresh_disable' => false, + + /** 令牌签发者 */ + 'iss' => 'webman.tinywan.cn', + + /** 某个时间点后才能访问,单位秒。(如:30 表示当前时间30秒后才能使用) */ + 'nbf' => 0, + + /** 时钟偏差冗余时间,单位秒。建议这个余地应该不大于几分钟 */ + 'leeway' => 60, + + /** 是否允许单设备登录,默认不允许 false */ + 'is_single_device' => false, + + /** 缓存令牌时间,单位:秒。默认 7 天 */ + 'cache_token_ttl' => 604800, + + /** 缓存令牌前缀,默认 JWT:TOKEN: */ + 'cache_token_pre' => 'JWT:TOKEN:', + + /** 缓存刷新令牌前缀,默认 JWT:REFRESH_TOKEN: */ + 'cache_refresh_token_pre' => 'JWT:REFRESH_TOKEN:', + + /** 用户信息模型 */ + 'user_model' => function ($uid) { + return []; + }, + + /** 是否支持 get 请求获取令牌 */ + 'is_support_get_token' => false, + /** GET 请求获取令牌请求key */ + 'is_support_get_token_key' => 'authorization', + + /** access令牌私钥 */ + 'access_private_key' => << << << << true, + 'storage' => [ + 'default' => 'local', // local:本地 oss:阿里云 cos:腾讯云 qos:七牛云 + 'single_limit' => 1024 * 1024 * 200, // 单个文件的大小限制,默认200M 1024 * 1024 * 200 + 'total_limit' => 1024 * 1024 * 200, // 所有文件的大小限制,默认200M 1024 * 1024 * 200 + 'nums' => 10, // 文件数量限制,默认10 + 'include' => [], // 被允许的文件类型列表 + 'exclude' => [], // 不被允许的文件类型列表 + // 本地对象存储 + 'local' => [ + 'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class, + 'root' => runtime_path().'/storage', + 'dirname' => function () { + return date('Ymd'); + }, + 'domain' => 'http://127.0.0.1:8787', + 'uri' => '/runtime', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问 + 'algo' => 'sha1', + ], + // 阿里云对象存储 + 'oss' => [ + 'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class, + 'accessKeyId' => 'xxxxxxxxxxxx', + 'accessKeySecret' => 'xxxxxxxxxxxx', + 'bucket' => 'resty-webman', + 'dirname' => function () { + return 'storage'; + }, + 'domain' => 'http://webman.oss.tinywan.com', + 'endpoint' => 'oss-cn-hangzhou.aliyuncs.com', + 'algo' => 'sha1', + ], + // 腾讯云对象存储 + 'cos' => [ + 'adapter' => \Tinywan\Storage\Adapter\CosAdapter::class, + 'secretId' => 'xxxxxxxxxxxxx', + 'secretKey' => 'xxxxxxxxxxxx', + 'bucket' => 'resty-webman-xxxxxxxxx', + 'dirname' => 'storage', + 'domain' => 'http://webman.oss.tinywan.com', + 'region' => 'ap-shanghai', + ], + // 七牛云对象存储 + 'qiniu' => [ + 'adapter' => \Tinywan\Storage\Adapter\QiniuAdapter::class, + 'accessKey' => 'xxxxxxxxxxxxx', + 'secretKey' => 'xxxxxxxxxxxxx', + 'bucket' => 'resty-webman', + 'dirname' => 'storage', + 'domain' => 'http://webman.oss.tinywan.com', + ], + // aws + 's3' => [ + 'adapter' => \Tinywan\Storage\Adapter\S3Adapter::class, + 'key' => 'xxxxxxxxxxxxx', + 'secret' => 'xxxxxxxxxxxxx', + 'bucket' => 'resty-webman', + 'dirname' => 'storage', + 'domain' => 'http://webman.oss.tinywan.com', + 'region' => 'S3_REGION', + 'version' => 'latest', + 'use_path_style_endpoint' => true, + 'endpoint' => 'S3_ENDPOINT', + 'acl' => 'public-read', + ], + ], +]; diff --git a/server/config/plugin/webman/channel/app.php b/server/config/plugin/webman/channel/app.php new file mode 100644 index 0000000..8f9c426 --- /dev/null +++ b/server/config/plugin/webman/channel/app.php @@ -0,0 +1,4 @@ + true, +]; \ No newline at end of file diff --git a/server/config/plugin/webman/channel/process.php b/server/config/plugin/webman/channel/process.php new file mode 100644 index 0000000..921e21e --- /dev/null +++ b/server/config/plugin/webman/channel/process.php @@ -0,0 +1,27 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + + +use Webman\Channel\Server; +use Workerman\Protocols\Frame; + +return [ + 'server' => [ + 'listen' => 'frame://0.0.0.0:2206', + 'protocol' => Frame::class, + 'handler' => Server::class, + 'reloadable' => false, + 'count' => 1, // 必须是1 + ] +]; diff --git a/server/config/plugin/webman/console/app.php b/server/config/plugin/webman/console/app.php new file mode 100644 index 0000000..d7bf968 --- /dev/null +++ b/server/config/plugin/webman/console/app.php @@ -0,0 +1,28 @@ + true, + + 'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build', + + 'phar_filename' => 'webman.phar', + + 'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP + + 'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2 + + 'bin_filename' => 'webman.bin', + + 'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL. + + 'private_key_file' => '', // The file path for certificate or OpenSSL private key file. + + 'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#', + + 'exclude_files' => [ + '.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin' + ], + + 'custom_ini' => ' +memory_limit = 256M + ', +]; diff --git a/server/config/plugin/webman/event/app.php b/server/config/plugin/webman/event/app.php new file mode 100644 index 0000000..8f9c426 --- /dev/null +++ b/server/config/plugin/webman/event/app.php @@ -0,0 +1,4 @@ + true, +]; \ No newline at end of file diff --git a/server/config/plugin/webman/event/bootstrap.php b/server/config/plugin/webman/event/bootstrap.php new file mode 100644 index 0000000..e5b09ba --- /dev/null +++ b/server/config/plugin/webman/event/bootstrap.php @@ -0,0 +1,17 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + Webman\Event\BootStrap::class, +]; diff --git a/server/config/plugin/webman/event/command.php b/server/config/plugin/webman/event/command.php new file mode 100644 index 0000000..e860cf7 --- /dev/null +++ b/server/config/plugin/webman/event/command.php @@ -0,0 +1,7 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace plugin\saiadmin\app\cache; + +use plugin\saiadmin\app\logic\system\SystemConfigLogic; +use support\think\Cache; + +/** + * 配置缓存 + */ +class ConfigCache +{ + /** + * 读取缓存配置 + * @return array + */ + public static function cacheConfig(): array + { + return config('plugin.saiadmin.saithink.config_cache', [ + 'expire' => 60 * 60 * 24 * 365, + 'prefix' => 'saiadmin:config_cache:config_', + 'tag' => 'saiadmin:config_cache' + ]); + } + + /** + * 获取配置信息 + */ + public static function getConfig(string $code = ''): array + { + if (empty($code)) { + return []; + } + $cache = static::cacheConfig(); + // 直接从缓存获取 + $config = Cache::get($cache['prefix'] . md5($code)); + if ($config) { + return $config; + } + + // 设置配置并获取 + $config = static::setConfig($code); + if ($config) { + return $config; + } + + return []; + } + + /** + * 设置配置数据 + */ + public static function setConfig(string $code): array + { + $cache = static::cacheConfig(); + + $data = (new SystemConfigLogic())->getData($code); + if (empty($data)) { + return []; + } + + $tag = []; + $tag[] = $cache['tag']; + + // 保存到缓存 + Cache::tag($tag)->set($cache['prefix'] . md5($code), $data, $cache['expire']); + return $data; + } + + /** + * 清理单个配置缓存 + */ + public static function clearConfig(string $code): bool + { + $cache = static::cacheConfig(); + return Cache::delete($cache['prefix'] . md5($code)); + } + + /** + * 清理全部配置缓存 + * @return bool + */ + public static function clear(): bool + { + $cache = static::cacheConfig(); + return Cache::tag($cache['tag'])->clear(); + } +} diff --git a/server/plugin/saiadmin/app/cache/DictCache.php b/server/plugin/saiadmin/app/cache/DictCache.php new file mode 100644 index 0000000..dc85e63 --- /dev/null +++ b/server/plugin/saiadmin/app/cache/DictCache.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace plugin\saiadmin\app\cache; + +use plugin\saiadmin\app\logic\system\SystemDictTypeLogic; +use plugin\saiadmin\app\model\system\SystemDictType; +use support\think\Cache; + +/** + * 字典信息缓存 + */ +class DictCache +{ + /** + * 读取缓存配置 + * @return array + */ + public static function cacheConfig(): array + { + return config('plugin.saiadmin.saithink.dict_cache', [ + 'expire' => 60 * 60 * 24 * 365, + 'tag' => 'saiadmin:dict_cache', + ]); + } + + /** + * 获取全部字典 + */ + public static function getDictAll(): array + { + $cache = static::cacheConfig(); + // 直接从缓存获取 + $data = Cache::get($cache['tag']); + if ($data) { + return $data; + } + + // 获取信息并返回 + $data = static::setDictAll(); + if ($data) { + return $data; + } + + return []; + } + + /** + * 获取单个字典 + */ + public static function getDict($code): array + { + $data = static::getDictAll(); + if (isset($data[$code])) { + return $data[$code]; + } else { + return []; + } + } + + /** + * 设置全部字典 + */ + public static function setDictAll(): array + { + $cache = static::cacheConfig(); + $data = (new SystemDictTypeLogic)->getDictAll(); + + Cache::set($cache['tag'], $data, $cache['expire']); + return $data; + } + + /** + * 清除全部字典信息 + */ + public static function clear(): bool + { + $cache = static::cacheConfig(); + return Cache::delete($cache['tag']); + } +} diff --git a/server/plugin/saiadmin/app/cache/ReflectionCache.php b/server/plugin/saiadmin/app/cache/ReflectionCache.php new file mode 100644 index 0000000..d6753e5 --- /dev/null +++ b/server/plugin/saiadmin/app/cache/ReflectionCache.php @@ -0,0 +1,104 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace plugin\saiadmin\app\cache; + +use ReflectionClass; +use ReflectionMethod; +use plugin\saiadmin\service\Permission; +use support\think\Cache; + +/** + * 反射文件缓存 + */ +class ReflectionCache +{ + /** + * 读取缓存配置 + * @return array + */ + public static function cacheConfig(): array + { + return config('plugin.saiadmin.saithink.reflection_cache', [ + 'tag' => 'saiadmin:reflection', + 'expire' => 60 * 60 * 24 * 365, + 'no_need' => 'saiadmin:reflection_cache:no_need_', + 'attr' => 'saiadmin:reflection_cache:attr_', + ]); + } + + /** + * 获取控制器中无需登录的方法列表 + */ + public static function getNoNeedLogin(string $controller): array + { + $cache = static::cacheConfig(); + $tag = []; + $tag[] = $cache['tag']; + $key = $cache['no_need'] . md5($controller); + + $data = Cache::get($key); + if ($data !== null) { + return $data; + } + + // 反射逻辑 + if (class_exists($controller)) { + $ref = new ReflectionClass($controller); + $data = $ref->getDefaultProperties()['noNeedLogin'] ?? []; + } else { + $data = []; + } + + Cache::tag($tag)->set($key, $data, $cache['expire']); + return $data; + } + + /** + * 获取方法上的权限属性 + */ + public static function getPermissionAttributes(string $controller, string $action): array + { + $cache = static::cacheConfig(); + $tag = []; + $tag[] = $cache['tag']; + $key = $cache['attr'] . md5($controller . '::' . $action); + + $data = Cache::get($key); + if ($data) { + return $data; + } + + $data = []; + if (method_exists($controller, $action)) { + $refMethod = new ReflectionMethod($controller, $action); + $attributes = $refMethod->getAttributes(Permission::class); + if (!empty($attributes)) { + $attr = $attributes[0]->newInstance(); + $data = [ + 'title' => $attr->getTitle(), + 'slug' => $attr->getSlug(), + ]; + } + } + + Cache::tag($tag)->set($key, $data, $cache['expire']); + return $data; + } + + /** + * 清理所有反射缓存 + * @return bool + */ + public static function clear(): bool + { + $cache = static::cacheConfig(); + return Cache::tag($cache['tag'])->clear(); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/cache/UserAuthCache.php b/server/plugin/saiadmin/app/cache/UserAuthCache.php new file mode 100644 index 0000000..3617ce5 --- /dev/null +++ b/server/plugin/saiadmin/app/cache/UserAuthCache.php @@ -0,0 +1,143 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace plugin\saiadmin\app\cache; + +use plugin\saiadmin\app\logic\system\SystemMenuLogic; +use plugin\saiadmin\app\model\system\SystemUserRole; +use support\think\Cache; + +/** + * 用户权限缓存 + */ +class UserAuthCache +{ + /** + * 读取缓存配置 + * @return array + */ + public static function cacheConfig(): array + { + return config('plugin.saiadmin.saithink.button_cache', [ + 'prefix' => 'saiadmin:button_cache:user_', + 'expire' => 60 * 60 * 2, + 'all' => 'saiadmin:button_cache:all', + 'role' => 'saiadmin:button_cache:role_', + 'tag' => 'saiadmin:button_cache', + ]); + } + + /** + * 获取用户的权限 + */ + public static function getUserAuth($uid): array + { + if (empty($uid)) { + return []; + } + $cache = static::cacheConfig(); + // 直接从缓存获取 + $auth = Cache::get($cache['prefix'] . $uid); + if ($auth) { + return $auth; + } + + // 设置权限并返回 + $auth = static::setUserAuth($uid); + if ($auth) { + return $auth; + } + + return []; + } + + /** + * 设置用户的权限 + */ + public static function setUserAuth($uid): array + { + // 从缓存获取,直接返回 + $roleIds = SystemUserRole::getRoleIds($uid); + + // 获取角色关联的菜单权限 + $data = (new SystemMenuLogic())->getAuthByRole($roleIds); + if (empty($data)) { + return []; + } + + $cache = static::cacheConfig(); + + $tag = []; + $tag[] = $cache['tag']; + if (!empty($roleIds)) { + foreach ($roleIds as $role) { + $tag[] = $cache['role'] . $role; + } + } + + // 保存到缓存 + Cache::tag($tag)->set($cache['prefix'] . $uid, $data, $cache['expire']); + return $data; + } + + /** + * 获取全部权限 + */ + public static function getAllAuth(): array + { + $cache = static::cacheConfig(); + // 直接从缓存获取 + $auth = Cache::get($cache['all']); + if ($auth) { + return $auth; + } + + $all = (new SystemMenuLogic())->getAllAuth(); + + // 设置权限并返回 + Cache::tag($cache['tag'])->set($cache['all'], $all, $cache['expire']); + + return $all; + } + + /** + * 清理缓存 + */ + public static function clearUserAuth($uid): bool + { + $cache = static::cacheConfig(); + return Cache::delete($cache['prefix'] . $uid); + } + + /** + * 清理角色缓存 + */ + public static function clearUserAuthByRoleId($role_id): bool + { + $cache = static::cacheConfig(); + if (is_array($role_id)) { + $tags = []; + foreach ($role_id as $id) { + $tags[] = $cache['role'] . $id; + } + } else { + $tags = $cache['role'] . $role_id; + } + return Cache::tag($tags)->clear(); + } + + /** + * 清理所有用户缓存 + * @return bool + */ + public static function clear(): bool + { + $cache = static::cacheConfig(); + return Cache::tag($cache['tag'])->clear(); + } +} diff --git a/server/plugin/saiadmin/app/cache/UserInfoCache.php b/server/plugin/saiadmin/app/cache/UserInfoCache.php new file mode 100644 index 0000000..2fa32a3 --- /dev/null +++ b/server/plugin/saiadmin/app/cache/UserInfoCache.php @@ -0,0 +1,145 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace plugin\saiadmin\app\cache; + +use plugin\saiadmin\app\logic\system\SystemUserLogic; +use support\think\Cache; + +/** + * 用户信息缓存 + */ +class UserInfoCache +{ + /** + * 读取缓存配置 + * @return array + */ + public static function cacheConfig(): array + { + return config('plugin.saiadmin.saithink.user_cache', [ + 'prefix' => 'saiadmin:user_cache:info_', + 'expire' => 60 * 60 * 4, + 'dept' => 'saiadmin:user_cache:dept_', + 'role' => 'saiadmin:user_cache:role_', + 'post' => 'saiadmin:user_cache:post_', + ]); + } + + /** + * 通过id获取缓存管理员信息 + */ + public static function getUserInfo($uid): array + { + if (empty($uid)) { + return []; + } + $cache = static::cacheConfig(); + // 直接从缓存获取 + $adminInfo = Cache::get($cache['prefix'] . $uid); + + if ($adminInfo) { + return $adminInfo; + } + + // 获取缓存信息并返回 + $adminInfo = static::setUserInfo($uid); + if ($adminInfo) { + return $adminInfo; + } + + return []; + } + + /** + * 设置管理员信息 + */ + public static function setUserInfo($uid): array + { + $data = (new SystemUserLogic())->getUser($uid); + $cache = static::cacheConfig(); + + $tags = []; + if (!empty($data['deptList'])) { + $tags[] = $cache['dept'] . $data['deptList']['id']; + } + if (!empty($data['roleList'])) { + foreach ($data['roleList'] as $role) { + $tags[] = $cache['role'] . $role['id']; + } + } + if (!empty($data['postList'])) { + foreach ($data['postList'] as $post) { + $tags[] = $cache['post'] . $post['id']; + } + } + Cache::tag($tags)->set($cache['prefix'] . $uid, $data, $cache['expire']); + return $data; + } + + /** + * 清理管理员信息缓存 + */ + public static function clearUserInfo($uid): bool + { + $cache = static::cacheConfig(); + return Cache::delete($cache['prefix'] . $uid); + } + + /** + * 清理部门下所有用户缓存 + */ + public static function clearUserInfoByDeptId($dept_id): bool + { + $cache = static::cacheConfig(); + if (is_array($dept_id)) { + $tags = []; + foreach ($dept_id as $id) { + $tags[] = $cache['dept'] . $id; + } + } else { + $tags = $cache['dept'] . $dept_id; + } + return Cache::tag($tags)->clear(); + } + + /** + * 清理角色下所有用户缓存 + */ + public static function clearUserInfoByRoleId($role_id): bool + { + $cache = static::cacheConfig(); + if (is_array($role_id)) { + $tags = []; + foreach ($role_id as $id) { + $tags[] = $cache['role'] . $id; + } + } else { + $tags = $cache['role'] . $role_id; + } + return Cache::tag($tags)->clear(); + } + + /** + * 清理岗位下所有用户缓存 + */ + public static function clearUserInfoByPostId($post_id): bool + { + $cache = static::cacheConfig(); + if (is_array($post_id)) { + $tags = []; + foreach ($post_id as $id) { + $tags[] = $cache['post'] . $id; + } + } else { + $tags = $cache['post'] . $post_id; + } + return Cache::tag($tags)->clear(); + } + +} diff --git a/server/plugin/saiadmin/app/cache/UserMenuCache.php b/server/plugin/saiadmin/app/cache/UserMenuCache.php new file mode 100644 index 0000000..c7af35e --- /dev/null +++ b/server/plugin/saiadmin/app/cache/UserMenuCache.php @@ -0,0 +1,100 @@ + +// +---------------------------------------------------------------------- +declare(strict_types=1); + +namespace plugin\saiadmin\app\cache; + +use plugin\saiadmin\app\logic\system\SystemMenuLogic; +use plugin\saiadmin\app\model\system\SystemUserRole; +use support\think\Cache; + +/** + * 用户菜单缓存 + */ +class UserMenuCache +{ + /** + * 读取缓存配置 + * @return array + */ + public static function cacheConfig(): array + { + return config('plugin.saiadmin.saithink.menu_cache', [ + 'prefix' => 'saiadmin:menu_cache:user_', + 'expire' => 60 * 60 * 24 * 7, + 'tag' => 'saiadmin:menu_cache', + ]); + } + + /** + * 获取用户的菜单 + */ + public static function getUserMenu($uid): array + { + if (empty($uid)) { + return []; + } + $cache = static::cacheConfig(); + // 直接从缓存获取 + $menu = Cache::get($cache['prefix'] . $uid); + if ($menu) { + return $menu; + } + + // 设置用户菜单并获取 + $menu = static::setUserMenu($uid); + if ($menu) { + return $menu; + } + + return []; + } + + /** + * 设置用户菜单 + */ + public static function setUserMenu($uid): array + { + $cache = static::cacheConfig(); + $tag = []; + $tag[] = $cache['tag']; + $logic = new SystemMenuLogic(); + if ($uid == 1) { + $data = $logic->getAllMenus(); + } else { + $roleIds = SystemUserRole::getRoleIds($uid); + $data = $logic->getMenuByRole($roleIds); + if (empty($data)) { + return []; + } + } + + // 保存到缓存 + Cache::tag($tag)->set($cache['prefix'] . $uid, $data, $cache['expire']); + return $data; + } + + /** + * 清理用户缓存 + */ + public static function clearUserMenu($uid): bool + { + $cache = static::cacheConfig(); + return Cache::delete($cache['prefix'] . $uid); + } + + /** + * 清理所有菜单缓存 + * @return bool + */ + public static function clearMenuCache(): bool + { + $cache = static::cacheConfig(); + return Cache::tag($cache['tag'])->clear(); + } + +} diff --git a/server/plugin/saiadmin/app/controller/InstallController.php b/server/plugin/saiadmin/app/controller/InstallController.php new file mode 100644 index 0000000..beecfec --- /dev/null +++ b/server/plugin/saiadmin/app/controller/InstallController.php @@ -0,0 +1,363 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller; + +use Throwable; +use support\Request; +use support\Response; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\basic\OpenController; + +/** + * 安装控制器 + */ +class InstallController extends OpenController +{ + /** + * 不需要登录的方法 + */ + protected array $noNeedLogin = ['index', 'install']; + + /** + * 应用名称 + * @var string + */ + protected string $app = 'saiadmin'; + + protected string $version = '6.0.0'; + + /** + * 安装首页 + */ + public function index() + { + $data['app'] = $this->app; + $data['version'] = config('plugin.saiadmin.app.version', $this->version); + + $env = base_path() . DIRECTORY_SEPARATOR . '.env'; + + clearstatcache(); + if (is_file($env)) { + $data['error'] = '程序已经安装'; + return view('install/error', $data); + } + + if (!is_writable(base_path() . DIRECTORY_SEPARATOR . 'config')) { + $data['error'] = '权限认证失败'; + return view('install/error', $data); + } + + return view('install/index', $data); + } + + /** + * 执行安装 + */ + public function install(Request $request) + { + $env = base_path() . DIRECTORY_SEPARATOR . '.env'; + + clearstatcache(); + if (is_file($env)) { + return $this->fail('管理后台已经安装!如需重新安装,请删除根目录env配置文件并重启'); + } + + $user = $request->post('username'); + $password = $request->post('password'); + $database = $request->post('database'); + $host = $request->post('host'); + $port = (int) $request->post('port') ?: 3306; + $dataType = $request->post('dataType', 'demo'); + + try { + $db = $this->getPdo($host, $user, $password, $port); + $smt = $db->query("show databases like '$database'"); + if (empty($smt->fetchAll())) { + $db->exec("create database `$database` CHARSET utf8mb4 COLLATE utf8mb4_general_ci"); + } + } catch (\Throwable $e) { + $message = $e->getMessage(); + if (stripos($message, 'Access denied for user')) { + return $this->fail('数据库用户名或密码错误'); + } + if (stripos($message, 'Connection refused')) { + return $this->fail('Connection refused. 请确认数据库IP端口是否正确,数据库已经启动'); + } + if (stripos($message, 'timed out')) { + return $this->fail('数据库连接超时,请确认数据库IP端口是否正确,安全组及防火墙已经放行端口'); + } + throw $e; + } + + $db->exec("use `$database`"); + + $smt = $db->query("show tables like 'sa_system_menu';"); + $tables = $smt->fetchAll(); + if (count($tables) > 0) { + return $this->fail('数据库已经安装,请勿重复安装'); + } + + if ($dataType == 'demo') { + $sql_file = base_path() . '/plugin/saiadmin/db/saiadmin-6.0.sql'; + } else { + $sql_file = base_path() . '/plugin/saiadmin/db/saiadmin-pure.sql'; + } + + if (!is_file($sql_file)) { + return $this->fail('数据库SQL文件不存在'); + } + + $sql_query = file_get_contents($sql_file); + + $db->exec($sql_query); + + $this->generateConfig(); + + $env_config = <<success('安装成功'); + } + + /** + * 生成配置文件 + */ + protected function generateConfig() + { + // 1、think-orm配置文件 + $think_orm_config = << 'mysql', + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => env('DB_TYPE', 'mysql'), + // 服务器地址 + 'hostname' => env('DB_HOST', '127.0.0.1'), + // 数据库名 + 'database' => env('DB_NAME', 'saiadmin'), + // 数据库用户名 + 'username' => env('DB_USER', 'root'), + // 数据库密码 + 'password' => env('DB_PASSWORD', '123456'), + // 数据库连接端口 + 'hostport' => env('DB_PORT', 3306), + // 数据库连接参数 + 'params' => [ + // 连接超时3秒 + \PDO::ATTR_TIMEOUT => 3, + ], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => env('DB_PREFIX', ''), + // 断线重连 + 'break_reconnect' => true, + // 自定义分页类 + 'bootstrap' => '', + // 连接池配置 + 'pool' => [ + 'max_connections' => 5, // 最大连接数 + 'min_connections' => 1, // 最小连接数 + 'wait_timeout' => 3, // 从连接池获取连接等待超时时间 + 'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收 + 'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒 + ], + ], + ], +]; +EOF; + file_put_contents(base_path() . '/config/think-orm.php', $think_orm_config); + + // 2、chache配置文件 + $cache_config = << env('CACHE_MODE', 'file'), + 'stores' => [ + 'file' => [ + 'driver' => 'file', + 'path' => runtime_path('cache') + ], + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default' + ], + 'array' => [ + 'driver' => 'array' + ] + ] +]; +EOF; + file_put_contents(base_path() . '/config/cache.php', $cache_config); + + // 3、redis配置文件 + $redis_config = << [ + 'password' => env('REDIS_PASSWORD', ''), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', 6379), + 'database' => env('REDIS_DB', 0), + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ] +]; +EOF; + file_put_contents(base_path() . '/config/redis.php', $redis_config); + + // 4、think-cache配置文件 + $think_cache_config = << env('CACHE_MODE', 'file'), + // 缓存连接方式配置 + 'stores' => [ + // redis缓存 + 'redis' => [ + // 驱动方式 + 'type' => 'redis', + // 服务器地址 + 'host' => env('REDIS_HOST', '127.0.0.1'), + // 服务器端口 + 'port' => env('REDIS_PORT', 6379), + // 服务器密码 + 'password' => env('REDIS_PASSWORD', ''), + // 数据库 + 'select' => env('REDIS_DB', 0), + // 缓存前缀 + 'prefix' => 'cache:', + // 默认缓存有效期 0表示永久缓存 + 'expire' => 0, + // Thinkphp官方没有这个参数,由于生成的tag键默认不过期,如果tag键数量很大,避免长时间占用内存,可以设置一个超过其他缓存的过期时间,0为不设置 + 'tag_expire' => 86400 * 30, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 连接池配置 + 'pool' => [ + 'max_connections' => 5, // 最大连接数 + 'min_connections' => 1, // 最小连接数 + 'wait_timeout' => 3, // 从连接池获取连接等待超时时间 + 'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收 + 'heartbeat_interval' => 50, // 心跳检测间隔,需要小于60秒 + ], + ], + // 文件缓存 + 'file' => [ + // 驱动方式 + 'type' => 'file', + // 设置不同的缓存保存目录 + 'path' => runtime_path() . '/file/', + ], + ], +]; +EOF; + file_put_contents(base_path() . '/config/think-cache.php', $think_cache_config); + + // 5、database配置文件 + $database = << 'mysql', + 'connections' => [ + 'mysql' => [ + 'driver' => env('DB_TYPE', 'mysql'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', 3306), + 'database' => env('DB_NAME', 'saiadmin'), + 'username' => env('DB_USER', 'root'), + 'password' => env('DB_PASSWORD', '123456'), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_general_ci'), + 'prefix' => env('DB_PREFIX', ''), + 'strict' => true, + 'engine' => null, + 'options' => [ + PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers. + ], + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ], + ], +]; +EOF; + file_put_contents(base_path() . '/config/database.php', $database); + + } + + /** + * 获取pdo连接 + * @param $host + * @param $username + * @param $password + * @param $port + * @param $database + * @return \PDO + */ + protected function getPdo($host, $username, $password, $port, $database = null): \PDO + { + $dsn = "mysql:host=$host;port=$port;"; + if ($database) { + $dsn .= "dbname=$database"; + } + $params = [ + \PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4", + \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + \PDO::ATTR_EMULATE_PREPARES => false, + \PDO::ATTR_TIMEOUT => 5, + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]; + return new \PDO($dsn, $username, $password, $params); + } +} diff --git a/server/plugin/saiadmin/app/controller/LoginController.php b/server/plugin/saiadmin/app/controller/LoginController.php new file mode 100644 index 0000000..97c372e --- /dev/null +++ b/server/plugin/saiadmin/app/controller/LoginController.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller; + +use support\Request; +use support\Response; +use plugin\saiadmin\utils\Captcha; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemUserLogic; + +/** + * 登录控制器 + */ +class LoginController extends BaseController +{ + + /** + * 不需要登录的方法 + */ + protected array $noNeedLogin = ['captcha', 'login']; + + /** + * 获取验证码 + */ + public function captcha() : Response + { + $captcha = new Captcha(); + $result = $captcha->imageCaptcha(); + if ($result['result'] !== 1) { + return $this->fail($result['message']); + } + return $this->success($result); + } + + /** + * 登录 + * @param Request $request + * @return Response + */ + public function login(Request $request): Response + { + $username = $request->post('username', ''); + $password = $request->post('password', ''); + $type = $request->post('type', 'pc'); + + $code = $request->post('code', ''); + $uuid = $request->post('uuid', ''); + $captcha = new Captcha(); + if (!$captcha->checkCaptcha($uuid, $code)) { + return $this->fail('验证码错误'); + } + $logic = new SystemUserLogic(); + $data = $logic->login($username, $password, $type); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/SystemController.php b/server/plugin/saiadmin/app/controller/SystemController.php new file mode 100644 index 0000000..3aed312 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/SystemController.php @@ -0,0 +1,263 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller; + +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\logic\system\SystemCategoryLogic; +use plugin\saiadmin\app\logic\system\SystemLoginLogLogic; +use plugin\saiadmin\app\logic\system\SystemOperLogLogic; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemUserLogic; +use plugin\saiadmin\app\logic\system\SystemAttachmentLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; +use plugin\saiadmin\utils\Arr; +use Tinywan\Storage\Storage; + +/** + * 系统控制器 + */ +class SystemController extends BaseController +{ + + /** + * 用户信息 + */ + public function userInfo(): Response + { + $info['user'] = $this->adminInfo; + $info = []; + $info['id'] = $this->adminInfo['id']; + $info['username'] = $this->adminInfo['username']; + $info['dashboard'] = $this->adminInfo['dashboard']; + $info['avatar'] = $this->adminInfo['avatar']; + $info['email'] = $this->adminInfo['email']; + $info['phone'] = $this->adminInfo['phone']; + $info['gender'] = $this->adminInfo['gender']; + $info['signed'] = $this->adminInfo['signed']; + $info['realname'] = $this->adminInfo['realname']; + $info['department'] = $this->adminInfo['deptList']; + if ($this->adminInfo['id'] === 1) { + $info['buttons'] = ['*']; + $info['roles'] = ['super_admin']; + } else { + $info['buttons'] = UserAuthCache::getUserAuth($this->adminInfo['id']); + $info['roles'] = Arr::getArrayColumn($this->adminInfo['roleList'], 'code'); + } + return $this->success($info); + } + + /** + * 全部字典数据 + */ + public function dictAll(): Response + { + $dict = DictCache::getDictAll(); + return $this->success($dict); + } + + /** + * 菜单数据 + * @return Response + */ + public function menu(): Response + { + $data = UserMenuCache::getUserMenu($this->adminInfo['id']); + return $this->success($data); + } + + /** + * 获取资源列表 + * @param Request $request + * @return Response + */ + #[Permission('附件列表读取', 'core:system:resource')] + public function getResourceCategory(Request $request): Response + { + $logic = new SystemCategoryLogic(); + $data = $logic->tree([]); + return $this->success($data); + } + + /** + * 获取资源列表 + * @param Request $request + * @return Response + */ + #[Permission('附件列表读取', 'core:system:resource')] + public function getResourceList(Request $request): Response + { + $logic = new SystemAttachmentLogic(); + $where = $request->more([ + ['origin_name', ''], + ['category_id', ''], + ]); + $query = $logic->search($where); + $query->whereIn('mime_type', ['image/jpeg', 'image/png', 'image/gif', 'image/webp']); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 获取用户列表 + * @param Request $request + * @return Response + */ + #[Permission('用户列表读取', 'core:system:user')] + public function getUserList(Request $request): Response + { + $logic = new SystemUserLogic(); + $where = $request->more([ + ['keyword', ''], + ['dept_id', ''], + ]); + $data = $logic->openUserList($where); + return $this->success($data); + } + + /** + * 下载网络图片 + */ + #[Permission('上传网络图片', 'core:system:uploadImage')] + public function saveNetworkImage(Request $request): Response + { + $url = $request->input('url', ''); + $config = Storage::getConfig('local'); + $logic = new SystemAttachmentLogic(); + $data = $logic->saveNetworkImage($url, $config); + return $this->success($data, '操作成功'); + } + + /** + * 上传图片 + */ + #[Permission('上传图片', 'core:system:uploadImage')] + public function uploadImage(Request $request): Response + { + $logic = new SystemAttachmentLogic(); + $type = $request->input('mode', 'system'); + if ($type == 'local') { + return $this->success($logic->uploadBase('image', true)); + } + return $this->success($logic->uploadBase('image')); + } + + /** + * 上传文件 + */ + #[Permission('上传文件', 'core:system:uploadFile')] + public function uploadFile(Request $request): Response + { + $logic = new SystemAttachmentLogic(); + $type = $request->input('mode', 'system'); + if ($type == 'local') { + return $this->success($logic->uploadBase('file', true)); + } + return $this->success($logic->uploadBase('file')); + } + + /** + * 切片上传 + */ + #[Permission('上传文件', 'core:system:chunkUpload')] + public function chunkUpload(Request $request): Response + { + $logic = new SystemAttachmentLogic(); + $data = $request->post(); + $result = $logic->chunkUpload($data); + return $this->success($result); + } + + /** + * 获取登录日志 + * @return Response + */ + public function getLoginLogList(): Response + { + $logic = new SystemLoginLogLogic(); + $logic->init($this->adminInfo); + $query = $logic->search(['username' => $this->adminName]); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 获取操作日志 + * @return Response + */ + public function getOperationLogList(): Response + { + $logic = new SystemOperLogLogic(); + $logic->init($this->adminInfo); + $data = $logic->getOwnOperLogList(['username' => $this->adminName]); + return $this->success($data); + } + + /** + * 清除缓存 + * @return Response + */ + public function clearAllCache(): Response + { + UserInfoCache::clearUserInfo($this->adminId); + UserAuthCache::clearUserAuth($this->adminId); + UserMenuCache::clearUserMenu($this->adminId); + return $this->success([], '清除缓存成功!'); + } + + /** + * 基本统计 + * @return Response + */ + #[Permission('工作台数据统计', 'core:console:list')] + public function statistics(): Response + { + $userLogic = new SystemUserLogic(); + $userCount = $userLogic->count('id'); + $uploadLogic = new SystemAttachmentLogic(); + $attachCount = $uploadLogic->count('id'); + $loginLogic = new SystemLoginLogLogic(); + $loginCount = $loginLogic->count('id'); + $operLogic = new SystemOperLogLogic(); + $operCount = $operLogic->count('id'); + return $this->success([ + 'user' => $userCount, + 'attach' => $attachCount, + 'login' => $loginCount, + 'operate' => $operCount, + ]); + } + + /** + * 登录统计曲线图 + * @return Response + */ + #[Permission('工作台数据统计', 'core:console:list')] + public function loginChart(): Response + { + $logic = new SystemLoginLogLogic(); + $data = $logic->loginChart(); + return $this->success($data); + } + + /** + * 登录统计柱状图 + * @return Response + */ + #[Permission('工作台数据统计', 'core:console:list')] + public function loginBarChart(): Response + { + $logic = new SystemLoginLogLogic(); + $data = $logic->loginBarChart(); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/DataBaseController.php b/server/plugin/saiadmin/app/controller/system/DataBaseController.php new file mode 100644 index 0000000..1cd4168 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/DataBaseController.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\logic\system\DatabaseLogic; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 数据表维护控制器 + */ +class DataBaseController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new DatabaseLogic(); + parent::__construct(); + } + + /** + * 数据源列表 + * @return Response + */ + public function source(): Response + { + $data = $this->logic->getDbSource(); + return $this->success($data); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('数据表列表', 'core:database:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['source', ''], + ]); + $data = $this->logic->getList($where); + return $this->success($data); + } + + /** + * 回收站数据 + * @param Request $request + * @return Response + */ + #[Permission('回收站数据', 'core:recycle:index')] + public function recycle(Request $request): Response + { + $table = $request->input('table', ''); + $data = $this->logic->recycleData($table); + return $this->success($data); + } + + /** + * 销毁数据 + * @param Request $request + * @return Response + */ + #[Permission('回收站销毁', 'core:recycle:edit')] + public function delete(Request $request): Response + { + $table = $request->input('table', ''); + $ids = $request->input('ids', ''); + if (!empty($ids)) { + $result = $this->logic->delete($table, $ids); + if (!$result) { + return $this->fail('操作失败'); + } + return $this->success('操作成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + + /** + * 恢复数据 + * @param Request $request + * @return Response + */ + #[Permission('回收站恢复', 'core:recycle:edit')] + public function recovery(Request $request): Response + { + $table = $request->input('table', ''); + $ids = $request->input('ids', ''); + if (!empty($ids)) { + $result = $this->logic->recovery($table, $ids); + if (!$result) { + return $this->fail('操作失败'); + } + return $this->success('操作成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + + /** + * 获取表字段信息 + * @param Request $request + * @return Response + */ + #[Permission('数据表字段', 'core:database:index')] + public function detailed(Request $request): Response + { + $table = $request->input('table', ''); + $data = $this->logic->getColumnList($table, ''); + return $this->success($data); + } + + /** + * 优化表 + * @param Request $request + * @return Response + */ + #[Permission('数据表优化表', 'core:database:edit')] + public function optimize(Request $request): Response + { + $tables = $request->input('tables', []); + $this->logic->optimizeTable($tables); + return $this->success('优化成功'); + } + + /** + * 清理表碎片 + */ + #[Permission('数据表清理碎片', 'core:database:edit')] + public function fragment(Request $request): Response + { + $tables = $request->input('tables', []); + $this->logic->fragmentTable($tables); + return $this->success('清理成功'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemAttachmentController.php b/server/plugin/saiadmin/app/controller/system/SystemAttachmentController.php new file mode 100644 index 0000000..2e62ffe --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemAttachmentController.php @@ -0,0 +1,107 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemAttachmentLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 附件管理控制器 + */ +class SystemAttachmentController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemAttachmentLogic(); + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('附件数据列表', 'core:attachment:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['origin_name', ''], + ['category_id', ''], + ['storage_mode', ''], + ['mime_type', ''], + ['create_time', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('附件数据修改', 'core:attachment:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $result = $this->logic->edit($data['id'], ['origin_name' => $data['origin_name']]); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('附件数据删除', 'core:attachment:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 移动分类 + * @param Request $request + * @return Response + */ + #[Permission('附件移动分类', 'core:attachment:edit')] + public function move(Request $request) : Response + { + $category_id = $request->post('category_id', ''); + $ids = $request->post('ids', ''); + if (empty($ids) || empty($category_id)) { + return $this->fail('参数错误,请检查参数'); + } + $result = $this->logic->move($category_id, $ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemCategoryController.php b/server/plugin/saiadmin/app/controller/system/SystemCategoryController.php new file mode 100644 index 0000000..cd5330b --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemCategoryController.php @@ -0,0 +1,120 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\validate\system\SystemCategoryValidate; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemCategoryLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 附件分类控制器 + */ +class SystemCategoryController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemCategoryLogic(); + $this->validate = new SystemCategoryValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('附件分类列表', 'core:attachment:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['category_name', ''], + ]); + $data = $this->logic->tree($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('附件分类读取', 'core:attachment:index')] + public function read(Request $request) : Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('附件分类添加', 'core:attachment:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('附件分类修改', 'core:attachment:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('附件分类删除', 'core:attachment:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemConfigController.php b/server/plugin/saiadmin/app/controller/system/SystemConfigController.php new file mode 100644 index 0000000..f14c665 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemConfigController.php @@ -0,0 +1,126 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemConfigLogic; +use plugin\saiadmin\app\logic\system\SystemConfigGroupLogic; +use plugin\saiadmin\app\validate\system\SystemConfigValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 配置项数据控制器 + */ +class SystemConfigController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemConfigLogic(); + $this->validate = new SystemConfigValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('系统设置列表', 'core:config:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['group_id', ''], + ['name', ''], + ['key', ''], + ]); + $this->logic->setOrderField('sort'); + $this->logic->setOrderType('desc'); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('系统设置管理', 'core:config:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('系统设置管理', 'core:config:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('系统设置管理', 'core:config:edit')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 修改配置内容 + * @param Request $request + * @return Response + */ + #[Permission('系统设置修改', 'core:config:update')] + public function batchUpdate(Request $request): Response + { + $group_id = $request->post('group_id'); + $config = $request->post('config'); + if (empty($group_id) || empty($config)) { + return $this->fail('参数错误'); + } + $this->logic->batchUpdate($group_id, $config); + return $this->success('操作成功'); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemConfigGroupController.php b/server/plugin/saiadmin/app/controller/system/SystemConfigGroupController.php new file mode 100644 index 0000000..f644c82 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemConfigGroupController.php @@ -0,0 +1,154 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemConfigGroupLogic; +use plugin\saiadmin\app\validate\system\SystemConfigGroupValidate; +use plugin\saiadmin\service\Permission; +use plugin\saiadmin\utils\Arr; +use support\Request; +use support\Response; +use plugin\saiadmin\service\EmailService; +use plugin\saiadmin\app\model\system\SystemMail; + +/** + * 配置控制器 + */ +class SystemConfigGroupController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemConfigGroupLogic(); + $this->validate = new SystemConfigGroupValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('系统设置列表', 'core:config:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getAll($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('系统设置管理', 'core:config:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('系统设置管理', 'core:config:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + ConfigCache::clearConfig($data['code']); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('系统设置管理', 'core:config:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 邮件测试 + * @param Request $request + * @return Response + */ + #[Permission('系统设置修改', 'core:config:update')] + public function email(Request $request) : Response + { + $email = $request->input('email', ''); + if (empty($email)) { + return $this->fail('请输入邮箱'); + } + $subject = "测试邮件"; + $code = "9527"; + $content = "

验证码:{code}

这是一封测试邮件,请忽略

"; + $template = [ + 'code' => $code + ]; + $config = EmailService::getConfig(); + $model = SystemMail::create([ + 'gateway' => Arr::getConfigValue($config,'Host'), + 'from' => Arr::getConfigValue($config,'From'), + 'email' => $email, + 'code' => $code, + ]); + try { + $result = EmailService::sendByTemplate($email, $subject, $content, $template); + if (!empty($result)) { + $model->status = 'failure'; + $model->response = $result; + $model->save(); + return $this->fail('发送失败,请查看日志'); + } else { + $model->status = 'success'; + $model->save(); + return $this->success([], '发送成功'); + } + } catch (\Exception $e) { + $model->status = 'failure'; + $model->response = $e->getMessage(); + $model->save(); + return $this->fail($e->getMessage()); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemDeptController.php b/server/plugin/saiadmin/app/controller/system/SystemDeptController.php new file mode 100644 index 0000000..38c18b0 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemDeptController.php @@ -0,0 +1,134 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\validate\system\SystemDeptValidate; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemDeptLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 部门控制器 + */ +class SystemDeptController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemDeptLogic(); + $this->validate = new SystemDeptValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('部门数据列表', 'core:dept:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $data = $this->logic->tree($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据读取', 'core:dept:read')] + public function read(Request $request) : Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据添加', 'core:dept:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据修改','core:dept:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据删除','core:dept:destroy')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 可操作部门 + * @param Request $request + * @return Response + */ + public function accessDept(Request $request) : Response + { + $where = ['status' => 1]; + $data = $this->logic->accessDept($where); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemDictDataController.php b/server/plugin/saiadmin/app/controller/system/SystemDictDataController.php new file mode 100644 index 0000000..fa72e6c --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemDictDataController.php @@ -0,0 +1,112 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemDictDataLogic; +use plugin\saiadmin\app\validate\system\SystemDictDataValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 字典数据控制器 + */ +class SystemDictDataController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemDictDataLogic(); + $this->validate = new SystemDictDataValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('数据字典列表', 'core:dict:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['label', ''], + ['value', ''], + ['type_id', ''], + ['status', ''], + ]); + $this->logic->setOrderField('sort'); + $this->logic->setOrderType('desc'); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + DictCache::clear(); + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + DictCache::clear(); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + DictCache::clear(); + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemDictTypeController.php b/server/plugin/saiadmin/app/controller/system/SystemDictTypeController.php new file mode 100644 index 0000000..f8fecf4 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemDictTypeController.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemDictTypeLogic; +use plugin\saiadmin\app\validate\system\SystemDictTypeValidate; +use plugin\saiadmin\service\Permission; +use support\Cache; +use support\Request; +use support\Response; + +/** + * 字典类型控制器 + */ +class SystemDictTypeController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemDictTypeLogic(); + $this->validate = new SystemDictTypeValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('数据字典列表', 'core:dict:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + DictCache::clear(); + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + DictCache::clear(); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + DictCache::clear(); + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemLogController.php b/server/plugin/saiadmin/app/controller/system/SystemLogController.php new file mode 100644 index 0000000..7878486 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemLogController.php @@ -0,0 +1,100 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemLoginLogLogic; +use plugin\saiadmin\app\logic\system\SystemOperLogLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 日志控制器 + */ +class SystemLogController extends BaseController +{ + + /** + * 登录日志列表 + * @param Request $request + * @return Response + */ + #[Permission('登录日志列表', 'core:logs:login')] + public function getLoginLogPageList(Request $request) : Response + { + $where = $request->more([ + ['login_time', ''], + ['username', ''], + ['status', ''], + ['ip', ''], + ]); + $logic = new SystemLoginLogLogic(); + $query = $logic->search($where); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 删除登录日志 + * @param Request $request + * @return Response + */ + #[Permission('登录日志删除', 'core:logs:deleteLogin')] + public function deleteLoginLog(Request $request) : Response + { + $ids = $request->input('ids', ''); + $logic = new SystemLoginLogLogic(); + if (!empty($ids)) { + $logic->destroy($ids); + return $this->success('删除成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + + /** + * 操作日志列表 + * @param Request $request + * @return Response + */ + #[Permission('操作日志列表', 'core:logs:Oper')] + public function getOperLogPageList(Request $request) : Response + { + $where = $request->more([ + ['create_time', ''], + ['username', ''], + ['service_name', ''], + ['router', ''], + ['ip', ''], + ]); + $logic = new SystemOperLogLogic(); + $logic->init($this->adminInfo); + $query = $logic->search($where); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 删除操作日志 + * @param Request $request + * @return Response + */ + #[Permission('操作日志删除', 'core:logs:deleteOper')] + public function deleteOperLog(Request $request) : Response + { + $ids = $request->input('ids', ''); + $logic = new SystemOperLogLogic(); + if (!empty($ids)) { + $logic->destroy($ids); + return $this->success('删除成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemMailController.php b/server/plugin/saiadmin/app/controller/system/SystemMailController.php new file mode 100644 index 0000000..7dcb52d --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemMailController.php @@ -0,0 +1,72 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\service\Permission; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemMailLogic; +use plugin\saiadmin\app\validate\system\SystemMailValidate; +use support\Request; +use support\Response; + +/** + * 邮件记录控制器 + */ +class SystemMailController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemMailLogic(); + $this->validate = new SystemMailValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('邮件日志列表', 'core:email:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['gateway', ''], + ['from', ''], + ['code', ''], + ['email', ''], + ['status', ''], + ['create_time', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('邮件日志删除', 'core:email:destroy')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemMenuController.php b/server/plugin/saiadmin/app/controller/system/SystemMenuController.php new file mode 100644 index 0000000..5126d2c --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemMenuController.php @@ -0,0 +1,143 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemMenuLogic; +use plugin\saiadmin\app\validate\system\SystemMenuValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 菜单控制器 + */ +class SystemMenuController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemMenuLogic(); + $this->validate = new SystemMenuValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据列表', 'core:menu:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['path', ''], + ['menu', ''], + ['status', ''], + ]); + $data = $this->logic->tree($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据读取', 'core:menu:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据添加', 'core:menu:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + UserMenuCache::clearMenuCache(); + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据修改', 'core:menu:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + UserMenuCache::clearMenuCache(); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据删除', 'core:menu:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + UserMenuCache::clearMenuCache(); + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 可操作菜单 + * @param Request $request + * @return Response + */ + public function accessMenu(Request $request): Response + { + $where = []; + if ($this->adminId > 1) { + $data = $this->logic->auth(); + } else { + $data = $this->logic->tree($where); + } + return $this->success($data); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemPostController.php b/server/plugin/saiadmin/app/controller/system/SystemPostController.php new file mode 100644 index 0000000..08759ff --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemPostController.php @@ -0,0 +1,177 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemPostLogic; +use plugin\saiadmin\app\validate\system\SystemPostValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 岗位信息控制器 + */ +class SystemPostController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemPostLogic(); + $this->validate = new SystemPostValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据列表', 'core:post:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据读取', 'core:post:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据添加', 'core:post:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据修改', 'core:post:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据删除', 'core:post:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 导入数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据导入', 'core:post:import')] + public function import(Request $request): Response + { + $file = current($request->file()); + if (!$file || !$file->isValid()) { + return $this->fail('未找到上传文件'); + } + $this->logic->import($file); + return $this->success('导入成功'); + } + + /** + * 导出数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据导出', 'core:post:export')] + public function export(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + return $this->logic->export($where); + } + + /** + * 下载导入模板 + * @return Response + */ + public function downloadTemplate(): Response + { + $file_name = "template.xlsx"; + return downloadFile($file_name); + } + + /** + * 可操作岗位 + * @param Request $request + * @return Response + */ + public function accessPost(Request $request): Response + { + $where = ['status' => 1]; + $data = $this->logic->accessPost($where); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemRoleController.php b/server/plugin/saiadmin/app/controller/system/SystemRoleController.php new file mode 100644 index 0000000..47d07e5 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemRoleController.php @@ -0,0 +1,168 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\model\system\SystemUserRole; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\model\system\SystemUser; +use plugin\saiadmin\app\validate\system\SystemRoleValidate; +use plugin\saiadmin\app\logic\system\SystemRoleLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 角色控制器 + */ +class SystemRoleController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemRoleLogic(); + $this->validate = new SystemRoleValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('角色数据列表', 'core:role:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $query = $this->logic->search($where); + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + $query->where('level', '<', $maxLevel); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据读取', 'core:role:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据添加', 'core:role:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据修改', 'core:role:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据删除', 'core:role:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 根据角色获取菜单 + * @param Request $request + * @return Response + */ + #[Permission('角色数据列表', 'core:role:index')] + public function getMenuByRole(Request $request): Response + { + $id = $request->get('id'); + $data = $this->logic->getMenuByRole($id); + return $this->success($data); + } + + /** + * 菜单权限 + * @param Request $request + * @return Response + */ + #[Permission('角色菜单权限', 'core:role:menu')] + public function menuPermission(Request $request): Response + { + $id = $request->post('id'); + $menu_ids = $request->post('menu_ids'); + $this->logic->saveMenuPermission($id, $menu_ids); + return $this->success('操作成功'); + } + + /** + * 可操作角色 + * @param Request $request + * @return Response + */ + public function accessRole(Request $request): Response + { + $where = ['status' => 1]; + $data = $this->logic->accessRole($where); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemServerController.php b/server/plugin/saiadmin/app/controller/system/SystemServerController.php new file mode 100644 index 0000000..1304f63 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemServerController.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\service\Permission; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\utils\ServerMonitor; +use support\think\Cache; +use support\Request; +use support\Response; + +/** + * 邮件记录控制器 + */ +class SystemServerController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('服务监控', 'core:server:monitor')] + public function monitor(Request $request): Response + { + $service = new ServerMonitor(); + return $this->success([ + 'memory' => $service->getMemoryInfo(), + 'disk' => $service->getDiskInfo(), + 'phpEnv' => $service->getPhpAndEnvInfo(), + ]); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('缓存信息', 'core:server:cache')] + public function cache(Request $request): Response + { + $menu_cache = config('plugin.saiadmin.saithink.menu_cache', []); + $button_cache = config('plugin.saiadmin.saithink.button_cache', []); + $config_cache = config('plugin.saiadmin.saithink.config_cache', []); + $dict_cache = config('plugin.saiadmin.saithink.dict_cache', []); + $reflection_cache = config('plugin.saiadmin.saithink.reflection_cache', []); + + return $this->success([ + 'menu_cache' => $menu_cache, + 'button_cache' => $button_cache, + 'config_cache' => $config_cache, + 'dict_cache' => $dict_cache, + 'reflection_cache' => $reflection_cache + ]); + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('缓存数据清理', 'core:server:clear')] + public function clear(Request $request) : Response + { + $tag = $request->input('tag', ''); + if (empty($tag)) { + return $this->fail('请选择要删除的缓存'); + } + Cache::tag($tag)->clear(); + Cache::delete($tag); + return $this->success('删除成功'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemUserController.php b/server/plugin/saiadmin/app/controller/system/SystemUserController.php new file mode 100644 index 0000000..c169769 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemUserController.php @@ -0,0 +1,210 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\logic\system\SystemUserLogic; +use plugin\saiadmin\app\validate\system\SystemUserValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 用户信息控制器 + */ +class SystemUserController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemUserLogic(); + $this->validate = new SystemUserValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('用户数据列表', 'core:user:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['username', ''], + ['phone', ''], + ['email', ''], + ['status', ''], + ['dept_id', ''], + ['create_time', ''], + ]); + $data = $this->logic->indexList($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据读取', 'core:user:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据保存', 'core:user:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据更新', 'core:user:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据删除', 'core:user:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->input('ids', ''); + if (!empty($ids)) { + $this->logic->destroy($ids); + return $this->success('操作成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + + /** + * 清理用户缓存 + * @param Request $request + * @return Response + */ + #[Permission('清理用户缓存', 'core:user:cache')] + public function clearCache(Request $request): Response + { + $id = $request->post('id', ''); + UserInfoCache::clearUserInfo($id); + UserAuthCache::clearUserAuth($id); + UserMenuCache::clearUserMenu($id); + return $this->success('操作成功'); + } + + /** + * 修改用户密码 + * @param Request $request + * @return Response + */ + #[Permission('修改用户密码', 'core:user:password')] + public function initUserPassword(Request $request): Response + { + $id = $request->post('id', ''); + $password = $request->post('password', ''); + if ($id == 1) { + return $this->fail('超级管理员不允许重置密码'); + } + $data = ['password' => password_hash($password, PASSWORD_DEFAULT)]; + $this->logic->authEdit($id, $data); + UserInfoCache::clearUserInfo($id); + return $this->success('操作成功'); + } + + /** + * 设置用户首页 + * @param Request $request + * @return Response + */ + #[Permission('设置用户首页', 'core:user:home')] + public function setHomePage(Request $request): Response + { + $id = $request->post('id', ''); + $dashboard = $request->post('dashboard', ''); + $data = ['dashboard' => $dashboard]; + $this->logic->authEdit($id, $data); + UserInfoCache::clearUserInfo($id); + return $this->success('操作成功'); + } + + /** + * 更新资料 + * @param Request $request + * @return Response + */ + #[Permission('用户修改资料')] + public function updateInfo(Request $request): Response + { + $data = $request->post(); + unset($data['deptList']); + unset($data['postList']); + unset($data['roleList']); + $result = $this->logic->updateInfo($this->adminId, $data); + if ($result) { + UserInfoCache::clearUserInfo($this->adminId); + return $this->success('操作成功'); + } else { + return $this->fail('操作失败'); + } + } + + /** + * 修改密码 + * @param Request $request + * @return Response + */ + #[Permission('用户修改密码')] + public function modifyPassword(Request $request): Response + { + $oldPassword = $request->input('oldPassword'); + $newPassword = $request->input('newPassword'); + $this->logic->modifyPassword($this->adminId, $oldPassword, $newPassword); + UserInfoCache::clearUserInfo($this->adminId); + return $this->success('修改成功'); + } +} diff --git a/server/plugin/saiadmin/app/controller/tool/CrontabController.php b/server/plugin/saiadmin/app/controller/tool/CrontabController.php new file mode 100644 index 0000000..3b1e032 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/tool/CrontabController.php @@ -0,0 +1,181 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\tool; + +use plugin\saiadmin\app\logic\tool\CrontabLogic; +use plugin\saiadmin\app\logic\tool\CrontabLogLogic; +use plugin\saiadmin\app\validate\tool\CrontabValidate; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\service\Permission; +use Webman\Channel\Client; +use support\Request; +use support\Response; + +/** + * 定时任务控制器 + */ +class CrontabController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new CrontabLogic(); + $this->validate = new CrontabValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('定时任务列表', 'tool:crontab:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['type', ''], + ['status', ''], + ['create_time', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('定时任务添加', 'tool:crontab:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('定时任务修改', 'tool:crontab:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('定时任务删除', 'tool:crontab:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 修改状态 + * @param Request $request + * @return Response + */ + #[Permission('定时任务状态修改', 'tool:crontab:edit')] + public function changeStatus(Request $request) : Response + { + $id = $request->input('id', ''); + $status = $request->input('status', 1); + if (empty($id)) { + return $this->fail('参数错误,请检查'); + } + $result = $this->logic->changeStatus($id, $status); + if ($result) { + return $this->success('操作成功'); + } else { + return $this->fail('操作失败'); + } + } + + /** + * 执行定时任务 + * @param Request $request + * @return Response + */ + #[Permission('定时任务执行', 'tool:crontab:run')] + public function run(Request $request) : Response + { + $id = $request->input('id', ''); + $result = $this->logic->run($id); + if ($result) { + return $this->success('执行成功'); + } else { + return $this->fail('执行失败'); + } + } + + /** + * 定时任务日志 + * @param Request $request + * @return Response + */ + #[Permission('定时任务日志', 'tool:crontab:index')] + public function logPageList(Request $request) : Response + { + $where = $request->more([ + ['crontab_id', ''], + ['create_time', []] + ]); + $logic = new CrontabLogLogic(); + $query = $logic->search($where); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 定时任务日志删除 + * @param Request $request + * @return Response + */ + #[Permission('定时任务日志删除', 'tool:crontab:edit')] + public function deleteCrontabLog(Request $request) : Response + { + $ids = $request->input('ids', ''); + if (!empty($ids)) { + $logic = new CrontabLogLogic(); + $logic->destroy($ids); + return $this->success('操作成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/tool/GenerateTablesController.php b/server/plugin/saiadmin/app/controller/tool/GenerateTablesController.php new file mode 100644 index 0000000..8765993 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/tool/GenerateTablesController.php @@ -0,0 +1,178 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\tool; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\tool\GenerateTablesLogic; +use plugin\saiadmin\app\validate\tool\GenerateTablesValidate; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 代码生成控制器 + */ +class GenerateTablesController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new GenerateTablesLogic(); + $this->validate = new GenerateTablesValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('代码生成列表', 'tool:code:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['table_name', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('代码生成列表', 'tool:code:index')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 修改数据 + * @param Request $request + * @return Response + */ + #[Permission('代码生成修改', 'tool:code:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('代码生成删除', 'tool:code:edit')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 装载数据表 + * @param Request $request + * @return Response + */ + #[Permission('代码生成装载', 'tool:code:edit')] + public function loadTable(Request $request): Response + { + $names = $request->input('names', []); + $source = $request->input('source', ''); + $this->logic->loadTable($names, $source); + return $this->success('操作成功'); + } + + /** + * 同步数据表字段信息 + * @param Request $request + * @return Response + */ + #[Permission('代码生成同步表结构', 'tool:code:edit')] + public function sync(Request $request): Response + { + $id = $request->input('id', ''); + $this->logic->sync($id); + return $this->success('操作成功'); + } + + /** + * 代码预览 + */ + #[Permission('代码生成预览', 'tool:code:edit')] + public function preview(Request $request): Response + { + $id = $request->input('id', ''); + $data = $this->logic->preview($id); + return $this->success($data); + } + + /** + * 代码生成 + */ + #[Permission('代码生成文件', 'tool:code:edit')] + public function generate(Request $request): Response + { + $ids = $request->input('ids', ''); + $data = $this->logic->generate($ids); + return response()->download($data['download'], $data['filename']); + } + + /** + * 生成到模块 + */ + #[Permission('代码生成到模块', 'tool:code:edit')] + public function generateFile(Request $request): Response + { + $id = $request->input('id', ''); + $this->logic->generateFile($id); + UserMenuCache::clearMenuCache(); + return $this->success('操作成功'); + } + + /** + * 获取数据表字段信息 + * @param Request $request + * @return Response + */ + #[Permission('代码生成读取表字段', 'tool:code:index')] + public function getTableColumns(Request $request): Response + { + $table_id = $request->input('table_id', ''); + $data = $this->logic->getTableColumns($table_id); + return $this->success($data); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/event/SystemUser.php b/server/plugin/saiadmin/app/event/SystemUser.php new file mode 100644 index 0000000..f85ef3f --- /dev/null +++ b/server/plugin/saiadmin/app/event/SystemUser.php @@ -0,0 +1,162 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\event; + +use plugin\saiadmin\app\cache\ReflectionCache; +use plugin\saiadmin\app\model\system\SystemLoginLog; +use plugin\saiadmin\app\model\system\SystemOperLog; + +class SystemUser +{ + /** + * 登录日志 + * @param $item + */ + public function login($item) + { + $request = request(); + $ip = $request ? $request->getRealIp() : '127.0.0.1'; + $http_user_agent = $request ? $request->header('user-agent') : ''; + $data['username'] = $item['username']; + $data['ip'] = $ip; + $data['ip_location'] = self::getIpLocation($ip); + $data['os'] = self::getOs($http_user_agent); + $data['browser'] = self::getBrowser($http_user_agent); + $data['status'] = $item['status']; + $data['message'] = $item['message']; + $data['login_time'] = date('Y-m-d H:i:s'); + if (isset($item['admin_id'])) { + $data['created_by'] = $item['admin_id']; + $data['updated_by'] = $item['admin_id']; + } + SystemLoginLog::create($data); + } + + /** + * 记录操作日志 + */ + public function operateLog(): bool + { + $request = request(); + if (!$request) { + return false; + } + if ($request->method() === 'GET') { + return false; + } + $info = getCurrentInfo(); + $ip = $request->getRealIp(); + $module = $request->plugin; + $rule = trim($request->uri()); + $data['username'] = $info['username']; + $data['method'] = $request->method(); + $data['router'] = $rule; + $data['service_name'] = self::getServiceName(); + $data['app'] = $module; + $data['ip'] = $ip; + $data['ip_location'] = self::getIpLocation($ip); + $data['request_data'] = $this->filterParams($request->all()); + SystemOperLog::create($data); + return true; + } + + /** + * 获取业务名称 + */ + protected function getServiceName(): string + { + $request = request(); + if (!$request) { + return '未命名业务'; + } + $permissions = ReflectionCache::getPermissionAttributes($request->controller, $request->action); + if (!empty($permissions)) { + return $permissions['title'] ?? '未命名业务'; + } else { + return '未命名业务'; + } + } + + /** + * 过滤字段 + */ + protected function filterParams($params): string + { + $blackList = ['password', 'oldPassword', 'newPassword']; + foreach ($params as $key => $value) { + if (in_array($key, $blackList)) { + $params[$key] = '******'; + } + } + return json_encode($params, JSON_UNESCAPED_UNICODE); + } + + /** + * 获取IP地理位置 + */ + protected function getIpLocation($ip): string + { + $ip2region = new \Ip2Region(); + try { + $region = $ip2region->memorySearch($ip); + } catch (\Exception $e) { + return '未知'; + } + list($country, $province, $city, $network) = explode('|', $region['region']); + if ($network === '内网IP') { + return $network; + } + if ($country == '中国') { + return $province . '-' . $city . ':' . $network; + } else if ($country == '0') { + return '未知'; + } else { + return $country; + } + } + + /** + * 获取浏览器信息 + */ + protected function getBrowser($user_agent): string + { + $br = 'Unknown'; + if (preg_match('/MSIE/i', $user_agent)) { + $br = 'MSIE'; + } elseif (preg_match('/Firefox/i', $user_agent)) { + $br = 'Firefox'; + } elseif (preg_match('/Chrome/i', $user_agent)) { + $br = 'Chrome'; + } elseif (preg_match('/Safari/i', $user_agent)) { + $br = 'Safari'; + } elseif (preg_match('/Opera/i', $user_agent)) { + $br = 'Opera'; + } else { + $br = 'Other'; + } + return $br; + } + + /** + * 获取操作系统信息 + */ + protected function getOs($user_agent): string + { + $os = 'Unknown'; + if (preg_match('/win/i', $user_agent)) { + $os = 'Win'; + } elseif (preg_match('/mac/i', $user_agent)) { + $os = 'Mac'; + } elseif (preg_match('/linux/i', $user_agent)) { + $os = 'Linux'; + } else { + $os = 'Other'; + } + return $os; + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/exception/Handler.php b/server/plugin/saiadmin/app/exception/Handler.php new file mode 100644 index 0000000..59ca1ed --- /dev/null +++ b/server/plugin/saiadmin/app/exception/Handler.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\exception; + +use Throwable; +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\Exception\ExceptionHandler; +use plugin\saiadmin\exception\ApiException; + +/** + * 异常处理类 + */ +class Handler extends ExceptionHandler +{ + public $dontReport = [ + ApiException::class, + ]; + + public function report(Throwable $exception) + { + if ($this->shouldntReport($exception)) { + return; + } + $logs = ''; + if ($request = \request()) { + $user = getCurrentInfo(); + $logs .= $request->method() . ' ' . $request->uri(); + $logs .= PHP_EOL . '[request_param]: ' . json_encode($request->all()); + $logs .= PHP_EOL . '[timestamp]: ' . date('Y-m-d H:i:s'); + $logs .= PHP_EOL . '[client_ip]: ' . $request->getRealIp(); + $logs .= PHP_EOL . '[action_user]: ' . var_export($user, true); + $logs .= PHP_EOL . '[exception_handle]: ' . get_class($exception); + $logs .= PHP_EOL . '[exception_info]: ' . PHP_EOL . $exception; + } + $this->logger->error($logs); + } + + public function render(Request $request, Throwable $exception): Response + { + $debug = config('app.debug', true); + $code = $exception->getCode(); + $json = [ + 'code' => $code ? $code : 500, + 'message' => $code !== 500 ? $exception->getMessage() : 'Server internal error', + 'type' => 'failed' + ]; + if ($debug) { + $json['request_url'] = $request->method() . ' ' . $request->uri(); + $json['timestamp'] = date('Y-m-d H:i:s'); + $json['client_ip'] = $request->getRealIp(); + $json['request_param'] = $request->all(); + $json['exception_handle'] = get_class($exception); + $json['exception_info'] = [ + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()) + ]; + } + return new Response(200, ['Content-Type' => 'application/json;charset=utf-8'], json_encode($json)); + } +} diff --git a/server/plugin/saiadmin/app/functions.php b/server/plugin/saiadmin/app/functions.php new file mode 100644 index 0000000..2340248 --- /dev/null +++ b/server/plugin/saiadmin/app/functions.php @@ -0,0 +1,114 @@ + +// +---------------------------------------------------------------------- +use Webman\Route; +use support\Response; +use Tinywan\Jwt\JwtToken; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\app\cache\DictCache; + +if (!function_exists('getCurrentInfo')) { + /** + * 获取当前登录用户 + */ + function getCurrentInfo(): bool|array + { + if (!request()) { + return false; + } + try { + $token = JwtToken::getExtend(); + } catch (\Throwable $e) { + return false; + } + return $token; + } +} + +if (!function_exists('fastRoute')) { + /** + * 快速注册路由[index|save|update|read|destroy|import|export] + * @param string $name + * @param string $controller + * @return void + */ + function fastRoute(string $name, string $controller): void + { + $name = trim($name, '/'); + if (method_exists($controller, 'index')) + Route::get("/$name/index", [$controller, 'index']); + if (method_exists($controller, 'save')) + Route::post("/$name/save", [$controller, 'save']); + if (method_exists($controller, 'update')) + Route::put("/$name/update", [$controller, 'update']); + if (method_exists($controller, 'read')) + Route::get("/$name/read", [$controller, 'read']); + if (method_exists($controller, 'destroy')) + Route::delete("/$name/destroy", [$controller, 'destroy']); + if (method_exists($controller, 'import')) + Route::post("/$name/import", [$controller, 'import']); + if (method_exists($controller, 'export')) + Route::post("/$name/export", [$controller, 'export']); + } +} + +if (!function_exists('downloadFile')) { + /** + * 下载模板 + * @param $file_name + * @return Response + */ + function downloadFile($file_name): Response + { + $base_dir = config('plugin.saiadmin.saithink.template', base_path() . '/public/template'); + if (file_exists($base_dir . DIRECTORY_SEPARATOR . $file_name)) { + return response()->download($base_dir . DIRECTORY_SEPARATOR . $file_name, urlencode($file_name)); + } else { + throw new ApiException('模板不存在'); + } + } +} + +if (!function_exists('formatBytes')) { + /** + * 根据字节计算大小 + * @param $bytes + * @return string + */ + function formatBytes($bytes): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + for ($i = 0; $bytes > 1024; $i++) { + $bytes /= 1024; + } + return round($bytes, 2) . ' ' . $units[$i]; + } +} + +if (!function_exists('getConfigGroup')) { + /** + * 读取配置组 + * @param $group + * @return array + */ + function getConfigGroup($group): array + { + return ConfigCache::getConfig($group); + } +} + +if (!function_exists('dictDataList')) { + /** + * 根据字典编码获取字典列表 + * @param string $code 字典编码 + * @return array + */ + function dictDataList(string $code): array + { + return DictCache::getDict($code); + } +} diff --git a/server/plugin/saiadmin/app/logic/system/DatabaseLogic.php b/server/plugin/saiadmin/app/logic/system/DatabaseLogic.php new file mode 100644 index 0000000..0570726 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/DatabaseLogic.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use support\think\Db; + +/** + * 数据表维护逻辑层 + */ +class DatabaseLogic extends BaseLogic +{ + /** + * 获取数据源 + * @return array + */ + public function getDbSource(): array + { + $data = config('think-orm.connections'); + $list = []; + foreach ($data as $k => $v) { + $list[] = $k; + } + return $list; + } + + /** + * 数据列表 + * @param $query + * @return mixed + */ + public function getList($query): mixed + { + $request = request(); + $page = $request ? ($request->input('page') ?: 1) : 1; + $limit = $request ? ($request->input('limit') ?: 10) : 10; + + return self::getTableList($query, $page, $limit); + } + + /** + * 获取数据库表数据 + */ + public function getTableList($query, $current_page = 1, $per_page = 10): array + { + if (!empty($query['source'])) { + if (!empty($query['name'])) { + $sql = 'show table status where name=:name '; + $list = Db::connect($query['source'])->query($sql, ['name' => $query['name']]); + } else { + $list = Db::connect($query['source'])->query('show table status'); + } + } else { + if (!empty($query['name'])) { + $sql = 'show table status where name=:name '; + $list = Db::query($sql, ['name' => $query['name']]); + } else { + $list = Db::query('show table status'); + } + } + + $data = []; + foreach ($list as $item) { + $data[] = [ + 'name' => $item['Name'], + 'engine' => $item['Engine'], + 'rows' => $item['Rows'], + 'data_free' => $item['Data_free'], + 'data_length' => $item['Data_length'], + 'index_length' => $item['Index_length'], + 'collation' => $item['Collation'], + 'create_time' => $item['Create_time'], + 'update_time' => $item['Update_time'], + 'comment' => $item['Comment'], + ]; + } + $total = count($data); + $last_page = ceil($total / $per_page); + $startIndex = ($current_page - 1) * $per_page; + $pageData = array_slice($data, $startIndex, $per_page); + return [ + 'data' => $pageData, + 'total' => $total, + 'current_page' => $current_page, + 'per_page' => $per_page, + 'last_page' => $last_page, + ]; + } + + /** + * 获取列信息 + */ + public function getColumnList($table, $source): array + { + $columnList = []; + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + if (!empty($source)) { + $list = Db::connect($source)->query('SHOW FULL COLUMNS FROM `' . $table . '`'); + } else { + $list = Db::query('SHOW FULL COLUMNS FROM `' . $table . '`'); + } + foreach ($list as $column) { + preg_match('/^\w+/', $column['Type'], $matches); + $columnList[] = [ + 'column_key' => $column['Key'], + 'column_name' => $column['Field'], + 'column_type' => $matches[0], + 'column_comment' => trim(preg_replace("/\([^()]*\)/", "", $column['Comment'])), + 'extra' => $column['Extra'], + 'default_value' => $column['Default'], + 'is_nullable' => $column['Null'], + ]; + } + } + return $columnList; + } + + /** + * 优化表 + */ + public function optimizeTable($tables) + { + foreach ($tables as $table) { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + Db::execute('OPTIMIZE TABLE `' . $table . '`'); + } + } + } + + /** + * 清理表碎片 + */ + public function fragmentTable($tables) + { + foreach ($tables as $table) { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + Db::execute('ANALYZE TABLE `' . $table . '`'); + } + } + } + + /** + * 获取回收站数据 + */ + public function recycleData($table) + { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + // 查询表字段 + $sql = 'SHOW COLUMNS FROM `' . $table . '` where Field = "delete_time"'; + $columns = Db::query($sql); + $isDeleteTime = false; + if (count($columns) > 0) { + $isDeleteTime = true; + } + if (!$isDeleteTime) { + throw new ApiException('当前表不支持回收站功能'); + } + // 查询软删除数据 + $request = request(); + $limit = $request ? ($request->input('limit') ?: 10) : 10; + return Db::table($table)->whereNotNull('delete_time') + ->order('delete_time', 'desc') + ->paginate($limit) + ->toArray(); + } else { + return []; + } + } + + /** + * 删除数据 + * @param $table + * @param $ids + * @return bool + */ + public function delete($table, $ids) + { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + $count = Db::table($table)->whereIn('id', $ids)->delete($ids); + return $count > 0; + } else { + return false; + } + } + + /** + * 恢复数据 + * @param $table + * @param $ids + * @return bool + */ + public function recovery($table, $ids) + { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + $count = Db::table($table) + ->where('id', 'in', $ids) + ->update(['delete_time' => null]); + return $count > 0; + } else { + return false; + } + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemAttachmentLogic.php b/server/plugin/saiadmin/app/logic/system/SystemAttachmentLogic.php new file mode 100644 index 0000000..d7f63ea --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemAttachmentLogic.php @@ -0,0 +1,199 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use Exception; +use plugin\saiadmin\app\model\system\SystemAttachment; +use plugin\saiadmin\app\model\system\SystemCategory; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\service\storage\ChunkUploadService; +use plugin\saiadmin\service\storage\UploadService; +use plugin\saiadmin\utils\Arr; +use plugin\saiadmin\utils\Helper; + +/** + * 附件逻辑层 + */ +class SystemAttachmentLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemAttachment(); + } + + /** + * @param $category_id + * @param $ids + * @return mixed + */ + public function move($category_id, $ids): mixed + { + $category = SystemCategory::where('id', $category_id)->findOrEmpty(); + if ($category->isEmpty()) { + throw new ApiException('目标分类不存在'); + } + return $this->model->whereIn('id', $ids)->update(['category_id' => $category_id]); + } + + /** + * 保存网络图片 + * @param $url + * @param $config + * @return array + * @throws ApiException|Exception + */ + public function saveNetworkImage($url, $config): array + { + $image_data = file_get_contents($url); + if ($image_data === false) { + throw new ApiException('获取文件资源失败'); + } + $image_resource = imagecreatefromstring($image_data); + if (!$image_resource) { + throw new ApiException('创建图片资源失败'); + } + $filename = basename($url); + $file_extension = pathinfo($filename, PATHINFO_EXTENSION); + $full_dir = runtime_path() . '/resource/'; + if (!is_dir($full_dir)) { + mkdir($full_dir, 0777, true); + } + $save_path = $full_dir . $filename; + $mime_type = 'image/'; + switch ($file_extension) { + case 'jpg': + case 'jpeg': + $mime_type = 'image/jpeg'; + $result = imagejpeg($image_resource, $save_path); + break; + case 'png': + $mime_type = 'image/png'; + $result = imagepng($image_resource, $save_path); + break; + case 'gif': + $mime_type = 'image/gif'; + $result = imagegif($image_resource, $save_path); + break; + default: + imagedestroy($image_resource); + throw new ApiException('文件格式错误'); + } + imagedestroy($image_resource); + if (!$result) { + throw new ApiException('文件保存失败'); + } + + $hash = md5_file($save_path); + $size = filesize($save_path); + + $model = $this->model->where('hash', $hash)->find(); + if ($model) { + unlink($save_path); + return $model->toArray(); + } else { + + $logic = new SystemConfigLogic(); + $uploadConfig = $logic->getGroup('upload_config'); + + $root = Arr::getConfigValue($uploadConfig, 'local_root'); + + $folder = date('Ymd'); + $full_dir = base_path() . DIRECTORY_SEPARATOR . $root . $folder . DIRECTORY_SEPARATOR; + if (!is_dir($full_dir)) { + mkdir($full_dir, 0777, true); + } + $object_name = bin2hex(pack('Nn', time(), random_int(1, 65535))) . ".$file_extension"; + $newPath = $full_dir . $object_name; + + copy($save_path, $newPath); + unlink($save_path); + $domain = Arr::getConfigValue($uploadConfig, 'local_domain'); + $uri = Arr::getConfigValue($uploadConfig, 'local_uri'); + $baseUrl = $domain . $uri . $folder . '/'; + + $info['storage_mode'] = 1; + $info['category_id'] = request()->input('category_id', 1); + $info['origin_name'] = $filename; + $info['object_name'] = $object_name; + $info['hash'] = $hash; + $info['mime_type'] = $mime_type; + $info['storage_path'] = $root . $folder . '/' . $object_name; + $info['suffix'] = $file_extension; + $info['size_byte'] = $size; + $info['size_info'] = formatBytes($size); + $info['url'] = $baseUrl . $object_name; + $this->model->save($info); + return $info; + } + } + + /** + * 文件上传 + * @param string $upload + * @param bool $local + * @return array + */ + public function uploadBase(string $upload = 'image', bool $local = false): array + { + $logic = new SystemConfigLogic(); + $uploadConfig = $logic->getGroup('upload_config'); + $type = Arr::getConfigValue($uploadConfig, 'upload_mode'); + if ($local === true) { + $type = 1; + } + $result = UploadService::disk($type, $upload)->uploadFile(); + $data = $result[0]; + $hash = $data['unique_id']; + $hash_check = config('plugin.saiadmin.saithink.file_hash', false); + if ($hash_check) { + $model = $this->model->where('hash', $hash)->findOrEmpty(); + if (!$model->isEmpty()) { + return $model->toArray(); + } + } + $url = str_replace('\\', '/', $data['url']); + $savePath = str_replace('\\', '/', $data['save_path']); + $info['storage_mode'] = $type; + $info['category_id'] = request()->input('category_id', 1); + $info['origin_name'] = $data['origin_name']; + $info['object_name'] = $data['save_name']; + $info['hash'] = $data['unique_id']; + $info['mime_type'] = $data['mime_type']; + $info['storage_path'] = $savePath; + $info['suffix'] = $data['extension']; + $info['size_byte'] = $data['size']; + $info['size_info'] = formatBytes($data['size']); + $info['url'] = $url; + $this->model->save($info); + return $info; + } + + /** + * 切片上传 + * @param $data + * @return array + */ + public function chunkUpload($data): array + { + $chunkService = new ChunkUploadService(); + if ($data['index'] == 0) { + $model = $this->model->where('hash', $data['hash'])->findOrEmpty(); + if (!$model->isEmpty()) { + return $model->toArray(); + } else { + return $chunkService->checkChunk($data); + } + } else { + return $chunkService->uploadChunk($data); + } + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemCategoryLogic.php b/server/plugin/saiadmin/app/logic/system/SystemCategoryLogic.php new file mode 100644 index 0000000..16a7010 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemCategoryLogic.php @@ -0,0 +1,101 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemCategory; +use plugin\saiadmin\utils\Helper; +use plugin\saiadmin\utils\Arr; + +/** + * 附件分类逻辑层 + */ +class SystemCategoryLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemCategory(); + } + + /** + * 添加数据 + */ + public function add($data): bool + { + $data = $this->handleData($data); + return $this->model->save($data); + } + + /** + * 修改数据 + */ + public function edit($id, $data): bool + { + $data = $this->handleData($data); + if ($data['parent_id'] == $id) { + throw new ApiException('上级分类和当前分类不能相同'); + } + if (in_array($id, explode(',', $data['level']))) { + throw new ApiException('不能将上级分类设置为当前分类的子分类'); + } + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + return $model->save($data); + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + $num = $this->model->where('parent_id', 'in', $ids)->count(); + if ($num > 0) { + throw new ApiException('该部门下存在子分类,请先删除子分类'); + } else { + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + if (empty($data['parent_id']) || $data['parent_id'] == 0) { + $data['level'] = '0'; + $data['parent_id'] = 0; + } else { + $parentMenu = SystemCategory::findOrEmpty($data['parent_id']); + $data['level'] = $parentMenu['level'] . $parentMenu['id'] . ','; + } + return $data; + } + + /** + * 数据树形化 + * @param array $where + * @return array + */ + public function tree(array $where = []): array + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { + $query->field('id, id as value, category_name as label, parent_id, category_name, sort'); + } + $query->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemConfigGroupLogic.php b/server/plugin/saiadmin/app/logic/system/SystemConfigGroupLogic.php new file mode 100644 index 0000000..455d266 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemConfigGroupLogic.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\app\model\system\SystemConfigGroup; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemConfig; +use support\think\Db; + +/** + * 参数配置分组逻辑层 + */ +class SystemConfigGroupLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemConfigGroup(); + } + + /** + * 删除配置信息 + */ + public function destroy($ids): bool + { + $id = $ids[0]; + $model = $this->model->where('id', $id)->findOrEmpty(); + if ($model->isEmpty()) { + throw new ApiException('配置数据未找到'); + } + if (in_array(intval($id), [1, 2, 3])) { + throw new ApiException('系统默认分组,无法删除'); + } + Db::startTrans(); + try { + // 删除配置组 + $model->delete(); + // 删除配置组数据 + $typeIds = SystemConfig::where('group_id', $id)->column('id'); + SystemConfig::destroy($typeIds); + ConfigCache::clearConfig($model->code); + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + throw new ApiException('删除数据异常,请检查'); + } + } +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemConfigLogic.php b/server/plugin/saiadmin/app/logic/system/SystemConfigLogic.php new file mode 100644 index 0000000..75e96c4 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemConfigLogic.php @@ -0,0 +1,107 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\app\model\system\SystemConfig; +use plugin\saiadmin\app\model\system\SystemConfigGroup; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Helper; + +/** + * 参数配置逻辑层 + */ +class SystemConfigLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemConfig(); + } + + /** + * 添加数据 + * @param mixed $data + * @return mixed + */ + public function add($data): mixed + { + $result = $this->model->create($data); + $group = SystemConfigGroup::find($data['group_id']); + ConfigCache::clearConfig($group->code); + return $result; + } + + /** + * 编辑数据 + * @param mixed $id + * @param mixed $data + * @return bool + */ + public function edit($id, $data): bool + { + $result = parent::edit($id, $data); + $group = SystemConfigGroup::find($data['group_id']); + ConfigCache::clearConfig($group->code); + return $result; + } + + /** + * 批量更新 + * @param mixed $group_id + * @param mixed $config + * @return bool + */ + public function batchUpdate($group_id, $config): bool + { + $group = SystemConfigGroup::find($group_id); + if (!$group) { + throw new ApiException('配置组未找到'); + } + $saveData = []; + foreach ($config as $key => $value) { + $saveData[] = [ + 'id' => $value['id'], + 'group_id' => $group_id, + 'name' => $value['name'], + 'key' => $value['key'], + 'value' => $value['value'] + ]; + } + // upsert: 根据 id 更新,如果不存在则插入 + $this->model->saveAll($saveData); + ConfigCache::clearConfig($group->code); + return true; + } + + /** + * 获取配置数据 + * @param mixed $code + * @return array + */ + public function getData($code): array + { + $group = SystemConfigGroup::where('code', $code)->findOrEmpty(); + if (empty($group)) { + return []; + } + $config = SystemConfig::where('group_id', $group['id'])->select()->toArray(); + return $config; + } + + /** + * 获取配置组 + */ + public function getGroup($config): array + { + return ConfigCache::getConfig($config); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemDeptLogic.php b/server/plugin/saiadmin/app/logic/system/SystemDeptLogic.php new file mode 100644 index 0000000..933fa3a --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemDeptLogic.php @@ -0,0 +1,127 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemDept; +use plugin\saiadmin\app\model\system\SystemUser; +use plugin\saiadmin\utils\Helper; +use plugin\saiadmin\utils\Arr; + +/** + * 部门逻辑层 + */ +class SystemDeptLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemDept(); + } + + /** + * 添加数据 + */ + public function add($data): mixed + { + $data = $this->handleData($data); + $this->model->save($data); + return $this->model->getKey(); + } + + /** + * 修改数据 + */ + public function edit($id, $data): mixed + { + $oldLevel = $data['level'] . $id . ','; + $data = $this->handleData($data); + if ($data['parent_id'] == $id) { + throw new ApiException('上级部门和当前部门不能相同'); + } + if (in_array($id, explode(',', $data['level']))) { + throw new ApiException('不能将上级部门设置为当前部门的子部门'); + } + $newLevel = $data['level'] . $id . ','; + $deptIds = $this->model->where('level', 'like', $oldLevel . '%')->column('id'); + + return $this->transaction(function () use ($deptIds, $oldLevel, $newLevel, $data, $id) { + $this->model->whereIn('id', $deptIds)->exp('level', "REPLACE(level, '$oldLevel', '$newLevel')")->update([]); + return $this->model->update($data, ['id' => $id]); + }); + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + $num = $this->model->where('parent_id', 'in', $ids)->count(); + if ($num > 0) { + throw new ApiException('该部门下存在子部门,请先删除子部门'); + } else { + $count = SystemUser::where('dept_id', 'in', $ids)->count(); + if ($count > 0) { + throw new ApiException('该部门下存在用户,请先删除或者转移用户'); + } + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + // 处理上级部门 + if (empty($data['parent_id']) || $data['parent_id'] == 0) { + $data['level'] = '0'; + $data['parent_id'] = 0; + } else { + $parentMenu = SystemDept::findOrEmpty($data['parent_id']); + $data['level'] = $parentMenu['level'] . $parentMenu['id'] . ','; + } + return $data; + } + + /** + * 数据树形化 + * @param array $where + * @return array + */ + public function tree(array $where = []): array + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { + $query->field('id, id as value, name as label, parent_id'); + } + $query->order('sort', 'desc'); + $query->with(['leader']); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + + /** + * 可操作部门 + * @param array $where + * @return array + */ + public function accessDept(array $where = []): array + { + $query = $this->search($where); + $query->auth($this->adminInfo['deptList']); + $query->field('id, id as value, name as label, parent_id'); + $query->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemDictDataLogic.php b/server/plugin/saiadmin/app/logic/system/SystemDictDataLogic.php new file mode 100644 index 0000000..a152841 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemDictDataLogic.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemDictData; +use plugin\saiadmin\app\model\system\SystemDictType; +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\utils\Helper; + +/** + * 字典类型逻辑层 + */ +class SystemDictDataLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemDictData(); + } + + /** + * 添加数据 + * @param $data + * @return mixed + */ + public function add($data): mixed + { + $type = SystemDictType::where('id', $data['type_id'])->findOrEmpty(); + if ($type->isEmpty()) { + throw new ApiException('字典类型不存在'); + } + $data['code'] = $type->code; + $model = $this->model->create($data); + DictCache::clear(); + return $model->getKey(); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemDictTypeLogic.php b/server/plugin/saiadmin/app/logic/system/SystemDictTypeLogic.php new file mode 100644 index 0000000..c3c5065 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemDictTypeLogic.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemDictType; +use plugin\saiadmin\app\model\system\SystemDictData; +use support\think\Db; + +/** + * 字典类型逻辑层 + */ +class SystemDictTypeLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemDictType(); + } + + /** + * 添加数据 + */ + public function add($data): mixed + { + $model = $this->model->where('code', $data['code'])->findOrEmpty(); + if (!$model->isEmpty()) { + throw new ApiException('该字典标识已存在'); + } + return $this->model->save($data); + } + + /** + * 数据更新 + */ + public function edit($id, $data): mixed + { + Db::startTrans(); + try { + // 修改数据字典类型 + $result = $this->model->update($data, ['id' => $id]); + // 更新数据字典数据 + SystemDictData::update(['code' => $data['code']], ['type_id' => $id]); + Db::commit(); + return $result; + } catch (\Exception $e) { + Db::rollback(); + throw new ApiException('修改数据异常,请检查'); + } + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + Db::startTrans(); + try { + // 删除数据字典类型 + $result = $this->model->destroy($ids); + // 删除数据字典数据 + $typeIds = SystemDictData::where('type_id', 'in', $ids)->column('id'); + SystemDictData::destroy($typeIds); + Db::commit(); + return $result; + } catch (\Exception $e) { + Db::rollback(); + throw new ApiException('删除数据异常,请检查'); + } + } + + /** + * 获取全部字典 + * @return array + */ + public function getDictAll(): array + { + $data = $this->model->where('status', 1)->field('id, name, code, remark') + ->with([ + 'dicts' => function ($query) { + $query->where('status', 1)->field('id, type_id, label, value, color, code, sort')->order('sort', 'desc'); + } + ])->select()->toArray(); + return $this->packageDict($data, 'code'); + } + + /** + * 组合数据 + * @param $array + * @param $field + * @return array + */ + private function packageDict($array, $field): array + { + $result = []; + foreach ($array as $item) { + if (isset($item[$field])) { + if (isset($result[$item[$field]])) { + $result[$item[$field]] = [($result[$item[$field]])]; + $result[$item[$field]][] = $item['dicts']; + } else { + $result[$item[$field]] = $item['dicts']; + } + } + } + return $result; + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemLoginLogLogic.php b/server/plugin/saiadmin/app/logic/system/SystemLoginLogLogic.php new file mode 100644 index 0000000..4f7b227 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemLoginLogLogic.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemLoginLog; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; +use support\think\Db; + +/** + * 登录日志逻辑层 + */ +class SystemLoginLogLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemLoginLog(); + } + + /** + * 登录统计图表 + * @return array + */ + public function loginChart(): array + { + $sql = " + SELECT + d.date AS login_date, + COUNT(l.login_time) AS login_count + FROM + (SELECT CURDATE() - INTERVAL (a.N) DAY AS date + FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 + UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 + UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a + ) d + LEFT JOIN sa_system_login_log l + ON DATE(l.login_time) = d.date + GROUP BY d.date + ORDER BY d.date ASC; + "; + $data = Db::query($sql); + return [ + 'login_count' => array_column($data, 'login_count'), + 'login_date' => array_column($data, 'login_date'), + ]; + } + + /** + * 登录统计图表 + * @return array + */ + public function loginBarChart(): array + { + $sql = " + SELECT + -- 拼接成 YYYY-MM 格式,例如 2023-01 + CONCAT(LPAD(m.month_num, 2, '0'), '月') AS login_month, + COUNT(l.login_time) AS login_count + FROM + -- 生成 1 到 12 的月份数字 + (SELECT 1 AS month_num UNION ALL SELECT 2 UNION ALL SELECT 3 + UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 + UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 + UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12) m + LEFT JOIN sa_system_login_log l + -- 关联条件:年份等于今年 且 月份等于生成的数字 + ON YEAR(l.login_time) = YEAR(CURDATE()) + AND MONTH(l.login_time) = m.month_num + GROUP BY + m.month_num + ORDER BY + m.month_num ASC; + "; + $data = Db::query($sql); + return [ + 'login_count' => array_column($data, 'login_count'), + 'login_month' => array_column($data, 'login_month'), + ]; + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemMailLogic.php b/server/plugin/saiadmin/app/logic/system/SystemMailLogic.php new file mode 100644 index 0000000..8eecc2d --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemMailLogic.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemMail; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; + +/** + * 邮件模型逻辑层 + */ +class SystemMailLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemMail(); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemMenuLogic.php b/server/plugin/saiadmin/app/logic/system/SystemMenuLogic.php new file mode 100644 index 0000000..a5e534d --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemMenuLogic.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemMenu; +use plugin\saiadmin\app\model\system\SystemRoleMenu; +use plugin\saiadmin\app\model\system\SystemUserRole; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Arr; +use plugin\saiadmin\utils\Helper; + +/** + * 菜单逻辑层 + */ +class SystemMenuLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemMenu(); + } + + /** + * 数据添加 + */ + public function add($data): mixed + { + $data = $this->handleData($data); + return $this->model->save($data); + } + + /** + * 数据修改 + */ + public function edit($id, $data): mixed + { + $data = $this->handleData($data); + if ($data['parent_id'] == $id) { + throw new ApiException('不能设置父级为自身'); + } + return $this->model->update($data, ['id' => $id]); + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + $num = $this->model->where('parent_id', 'in', $ids)->count(); + if ($num > 0) { + throw new ApiException('该菜单下存在子菜单,请先删除子菜单'); + } else { + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + // 处理上级菜单 + if (empty($data['parent_id']) || $data['parent_id'] == 0) { + $data['level'] = '0'; + $data['parent_id'] = 0; + } else { + $parentMenu = $this->model->findOrEmpty($data['parent_id']); + $data['level'] = $parentMenu['level'] . $parentMenu['id'] . ','; + } + return $data; + } + + /** + * 数据树形化 + * @param $where + * @return array + */ + public function tree($where = []): array + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { + $query->field('id, id as value, name as label, parent_id, type'); + } + $query->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + + /** + * 权限菜单 + * @return array + */ + public function auth(): array + { + $roleLogic = new SystemRoleLogic(); + $role_ids = Arr::getArrayColumn($this->adminInfo['roleList'], 'id'); + $roles = $roleLogic->getMenuIdsByRoleIds($role_ids); + $ids = $this->filterMenuIds($roles); + $query = $this->model + ->field('id, id as value, name as label, parent_id, type') + ->where('status', 1) + ->where('id', 'in', $ids) + ->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + + /** + * 获取全部菜单 + */ + public function getAllMenus(): array + { + $query = $this->search(['status' => 1, 'type' => [1, 2, 4]])->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeArtdMenus($data); + } + + /** + * 获取全部权限 + * @return array + */ + public function getAllAuth(): array + { + return SystemMenu::where('type', 3) + ->where('status', 1) + ->column('slug'); + } + + /** + * 根据角色获取权限 + * @param $roleIds + * @return array + */ + public function getAuthByRole($roleIds): array + { + $menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id'); + + return SystemMenu::distinct(true) + ->where('type', 3) + ->where('status', 1) + ->where('id', 'in', array_unique($menuId)) + ->column('slug'); + } + + /** + * 根据角色获取菜单 + * @param $roleIds + * @return array + */ + public function getMenuByRole($roleIds): array + { + $menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id'); + + $data = SystemMenu::distinct(true) + ->where('status', 1) + ->where('type', 'in', [1, 2, 4]) + ->where('id', 'in', array_unique($menuId)) + ->order('sort', 'desc') + ->select() + ->toArray(); + return Helper::makeArtdMenus($data); + } + + /** + * 过滤通过角色查询出来的菜单id列表,并去重 + * @param array $roleData + * @return array + */ + public function filterMenuIds(array &$roleData): array + { + $ids = []; + foreach ($roleData as $val) { + foreach ($val['menus'] as $menu) { + $ids[] = $menu['id']; + } + } + unset($roleData); + return array_unique($ids); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/logic/system/SystemOperLogLogic.php b/server/plugin/saiadmin/app/logic/system/SystemOperLogLogic.php new file mode 100644 index 0000000..de56274 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemOperLogLogic.php @@ -0,0 +1,38 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemOperLog; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; + +/** + * 操作日志逻辑层 + */ +class SystemOperLogLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemOperLog(); + } + + /** + * 获取自己的操作日志 + * @param mixed $where + * @return array + */ + public function getOwnOperLogList($where): array + { + $query = $this->search($where); + $query->field('id, username, method, router, service_name, ip, ip_location, create_time'); + return $this->getList($query); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemPostLogic.php b/server/plugin/saiadmin/app/logic/system/SystemPostLogic.php new file mode 100644 index 0000000..8ca13a0 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemPostLogic.php @@ -0,0 +1,95 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemPost; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\service\OpenSpoutWriter; +use OpenSpout\Reader\XLSX\Reader; + +/** + * 岗位管理逻辑层 + */ +class SystemPostLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemPost(); + } + + /** + * 可操作岗位 + * @param array $where + * @return array + */ + public function accessPost(array $where = []): array + { + $query = $this->search($where); + $query->field('id, id as value, name as label, name, code'); + return $this->getAll($query); + } + + /** + * 导入数据 + */ + public function import($file) + { + $path = $this->getImport($file); + $reader = new Reader(); + try { + $reader->open($path); + $data = []; + foreach ($reader->getSheetIterator() as $sheet) { + $isHeader = true; + foreach ($sheet->getRowIterator() as $row) { + if ($isHeader) { + $isHeader = false; + continue; + } + $cells = $row->getCells(); + $data[] = [ + 'name' => $cells[0]->getValue(), + 'code' => $cells[1]->getValue(), + 'sort' => $cells[2]->getValue(), + 'status' => $cells[3]->getValue(), + ]; + } + } + $this->saveAll($data); + } catch (\Exception $e) { + throw new ApiException('导入文件错误,请上传正确的文件格式xlsx'); + } + } + + /** + * 导出数据 + */ + public function export($where = []) + { + $query = $this->search($where)->field('id,name,code,sort,status,create_time'); + $data = $this->getAll($query); + $file_name = '岗位数据.xlsx'; + $header = ['编号', '岗位名称', '岗位标识', '排序', '状态', '创建时间']; + $filter = [ + 'status' => [ + ['value' => 1, 'label' => '正常'], + ['value' => 2, 'label' => '禁用'] + ] + ]; + $writer = new OpenSpoutWriter($file_name); + $writer->setWidth([15, 15, 20, 15, 15, 25]); + $writer->setHeader($header); + $writer->setData($data, null, $filter); + $file_path = $writer->returnFile(); + return response()->download($file_path, urlencode($file_name)); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemRoleLogic.php b/server/plugin/saiadmin/app/logic/system/SystemRoleLogic.php new file mode 100644 index 0000000..c9429a0 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemRoleLogic.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\model\system\SystemRole; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Helper; +use support\think\Cache; +use support\think\Db; + +/** + * 角色逻辑层 + */ +class SystemRoleLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemRole(); + } + + /** + * 添加数据 + */ + public function add($data): bool + { + $data = $this->handleData($data); + return $this->model->save($data); + } + + /** + * 修改数据 + */ + public function edit($id, $data): bool + { + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + $data = $this->handleData($data); + return $model->save($data); + } + + /** + * 删除数据 + */ + public function destroy($ids): bool + { + // 越权保护 + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + + $num = SystemRole::where('level', '>=', $maxLevel)->whereIn('id', $ids)->count(); + if ($num > 0) { + throw new ApiException('不能操作比当前账户职级高的角色'); + } else { + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + // 越权保护 + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + if ($data['level'] >= $maxLevel) { + throw new ApiException('不能操作比当前账户职级高的角色'); + } + return $data; + } + + /** + * 可操作角色 + * @param array $where + * @return array + */ + public function accessRole(array $where = []): array + { + $query = $this->search($where); + // 越权保护 + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + $query->where('level', '<', $maxLevel); + $query->order('sort', 'desc'); + return $this->getAll($query); + } + + /** + * 根据角色数组获取菜单 + * @param $ids + * @return array + */ + public function getMenuIdsByRoleIds($ids): array + { + if (empty($ids)) + return []; + return $this->model->where('id', 'in', $ids)->with([ + 'menus' => function ($query) { + $query->where('status', 1)->order('sort', 'desc'); + } + ])->select()->toArray(); + + } + + /** + * 根据角色获取菜单 + * @param $id + * @return array + */ + public function getMenuByRole($id): array + { + $role = $this->model->findOrEmpty($id); + $menus = $role->menus ?: []; + return [ + 'id' => $id, + 'menus' => $menus + ]; + } + + /** + * 保存菜单权限 + * @param $id + * @param $menu_ids + * @return mixed + */ + public function saveMenuPermission($id, $menu_ids): mixed + { + return $this->transaction(function () use ($id, $menu_ids) { + $role = $this->model->findOrEmpty($id); + if ($role) { + $role->menus()->detach(); + $data = array_map(function ($menu_id) use ($id) { + return ['menu_id' => $menu_id, 'role_id' => $id]; + }, $menu_ids); + Db::name('sa_system_role_menu')->limit(100)->insertAll($data); + } + $cache = config('plugin.saiadmin.saithink.button_cache'); + $tag = $cache['role'] . $id; + Cache::tag($tag)->clear(); // 清理权限缓存-角色TAG + UserMenuCache::clearMenuCache(); // 清理菜单缓存 + return true; + }); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemUserLogic.php b/server/plugin/saiadmin/app/logic/system/SystemUserLogic.php new file mode 100644 index 0000000..7188082 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemUserLogic.php @@ -0,0 +1,336 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\model\system\SystemDept; +use plugin\saiadmin\app\model\system\SystemRole; +use plugin\saiadmin\app\model\system\SystemUser; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\basic\think\BaseLogic; +use Webman\Event\Event; +use Tinywan\Jwt\JwtToken; + +/** + * 用户信息逻辑层 + */ +class SystemUserLogic extends BaseLogic +{ + + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemUser(); + } + + /** + * 分页数据列表 + * @param mixed $where + * @return array + */ + public function indexList($where): array + { + $query = $this->search($where); + $query->with(['depts']); + $query->auth($this->adminInfo['deptList']); + return $this->getList($query); + } + + /** + * 用户列表数据 + * @param mixed $where + * @return array + */ + public function openUserList($where): array + { + $query = $this->search($where); + $query->field('id, username, realname, avatar, phone, email'); + return $this->getList($query); + } + + /** + * 读取用户信息 + * @param mixed $id + * @return array + */ + public function getUser($id): array + { + $admin = $this->model->findOrEmpty($id); + $data = $admin->hidden(['password'])->toArray(); + $data['roleList'] = $admin->roles->toArray() ?: []; + $data['postList'] = $admin->posts->toArray() ?: []; + $data['deptList'] = $admin->depts ? $admin->depts->toArray() : []; + return $data; + } + + /** + * 读取数据 + * @param $id + * @return array + */ + public function read($id): array + { + $data = $this->getUser($id); + if ($this->adminInfo['id'] > 1) { + // 部门保护 + if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) { + throw new ApiException('没有权限操作该部门数据'); + } + } + return $data; + } + + /** + * 添加数据 + * @param $data + * @return mixed + */ + public function add($data): mixed + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return $this->transaction(function () use ($data) { + $role_ids = $data['role_ids'] ?? []; + $post_ids = $data['post_ids'] ?? []; + if ($this->adminInfo['id'] > 1) { + // 部门保护 + if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) { + throw new ApiException('没有权限操作该部门数据'); + } + // 越权保护 + if (!$this->roleProtect($this->adminInfo['roleList'], $role_ids)) { + throw new ApiException('没有权限操作该角色数据'); + } + } + $user = SystemUser::create($data); + $user->roles()->detach(); + $user->posts()->detach(); + $user->roles()->saveAll($role_ids); + if (!empty($post_ids)) { + $user->posts()->save($post_ids); + } + return $user; + }); + } + + /** + * 修改数据 + * @param $id + * @param $data + * @return mixed + */ + public function edit($id, $data): mixed + { + unset($data['password']); + return $this->transaction(function () use ($data, $id) { + $role_ids = $data['role_ids'] ?? []; + $post_ids = $data['post_ids'] ?? []; + // 仅可修改当前部门和子部门的用户 + $query = $this->model->where('id', $id); + $query->auth($this->adminInfo['deptList']); + $user = $query->findOrEmpty(); + if ($user->isEmpty()) { + throw new ApiException('没有权限操作该数据'); + } + if ($this->adminInfo['id'] > 1) { + // 部门保护 + if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) { + throw new ApiException('没有权限操作该部门数据'); + } + // 越权保护 + if (!$this->roleProtect($this->adminInfo['roleList'], $role_ids)) { + throw new ApiException('没有权限操作该角色数据'); + } + } + $result = parent::edit($id, $data); + if ($result) { + $user->roles()->detach(); + $user->posts()->detach(); + $user->roles()->saveAll($role_ids); + if (!empty($post_ids)) { + $user->posts()->save($post_ids); + } + UserInfoCache::clearUserInfo($id); + UserAuthCache::clearUserAuth($id); + UserMenuCache::clearUserMenu($id); + } + return $result; + }); + } + + /** + * 删除数据 + * @param $ids + * @return bool + */ + public function destroy($ids): bool + { + if (is_array($ids)) { + if (count($ids) > 1) { + throw new ApiException('禁止批量删除操作'); + } + $ids = $ids[0]; + } + if ($ids == 1) { + throw new ApiException('超级管理员禁止删除'); + } + $query = $this->model->where('id', $ids); + $query->auth($this->adminInfo['deptList']); + $user = $query->findOrEmpty(); + if ($user->isEmpty()) { + throw new ApiException('没有权限操作该数据'); + } + if ($this->adminInfo['id'] > 1) { + $role_ids = $user->roles->toArray() ?: []; + if (!empty($role_ids)) { + // 越权保护 + if (!$this->roleProtect($this->adminInfo['roleList'], array_column($role_ids, 'id'))) { + throw new ApiException('没有权限操作该角色数据'); + } + } + } + UserInfoCache::clearUserInfo($ids); + UserAuthCache::clearUserAuth($ids); + UserMenuCache::clearUserMenu($ids); + return parent::destroy($ids); + } + + /** + * 用户登录 + * @param string $username + * @param string $password + * @param string $type + * @return array + */ + public function login(string $username, string $password, string $type): array + { + $adminInfo = $this->model->where('username', $username)->findOrEmpty(); + $status = 1; + $message = '登录成功'; + if ($adminInfo->isEmpty()) { + $message = '账号或密码错误,请重新输入!'; + throw new ApiException($message); + } + if ($adminInfo->status === 2) { + $status = 0; + $message = '您已被禁止登录!'; + } + if (!password_verify($password, $adminInfo->password)) { + $status = 0; + $message = '账号或密码错误,请重新输入!'; + } + if ($status === 0) { + // 登录事件 + Event::emit('user.login', compact('username', 'status', 'message')); + throw new ApiException($message); + } + $adminInfo->login_time = date('Y-m-d H:i:s'); + $adminInfo->login_ip = request()->getRealIp(); + $adminInfo->save(); + + $access_exp = config('plugin.saiadmin.saithink.access_exp', 3 * 3600); + $token = JwtToken::generateToken([ + 'access_exp' => $access_exp, + 'id' => $adminInfo->id, + 'username' => $adminInfo->username, + 'type' => $type, + 'plat' => 'saiadmin', + ]); + // 登录事件 + $admin_id = $adminInfo->id; + Event::emit('user.login', compact('username', 'status', 'message', 'admin_id')); + return $token; + } + + /** + * 更新资料 + * @param mixed $id + * @param mixed $data + * @return bool + */ + public function updateInfo($id, $data): bool + { + $this->model->update($data, ['id' => $id], ['realname', 'gender', 'phone', 'email', 'avatar', 'signed']); + return true; + } + + /** + * 密码修改 + * @param $adminId + * @param $oldPassword + * @param $newPassword + * @return bool + */ + public function modifyPassword($adminId, $oldPassword, $newPassword): bool + { + $model = $this->model->findOrEmpty($adminId); + if (password_verify($oldPassword, $model->password)) { + $model->password = password_hash($newPassword, PASSWORD_DEFAULT); + return $model->save(); + } else { + throw new ApiException('原密码错误'); + } + } + + /** + * 修改数据 + */ + public function authEdit($id, $data) + { + if ($this->adminInfo['id'] > 1) { + // 判断用户是否可以操作 + $query = SystemUser::where('id', $id); + $query->auth($this->adminInfo['deptList']); + $user = $query->findOrEmpty(); + if ($user->isEmpty()) { + throw new ApiException('没有权限操作该数据'); + } + } + parent::edit($id, $data); + } + + /** + * 部门保护 + * @param $dept + * @param $dept_id + * @return bool + */ + public function deptProtect($dept, $dept_id): bool + { + // 部门保护 + $deptIds = [$dept['id']]; + $deptLevel = $dept['level'] . $dept['id'] . ','; + $dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id'); + $deptIds = array_merge($deptIds, $dept_ids); + if (!in_array($dept_id, $deptIds)) { + return false; + } + return true; + } + + /** + * 越权保护 + * @param $roleList + * @param $role_ids + * @return bool + */ + public function roleProtect($roleList, $role_ids): bool + { + // 越权保护 + $levelArr = array_column($roleList, 'level'); + $maxLevel = max($levelArr); + $currentLevel = SystemRole::whereIn('id', $role_ids)->max('level'); + if ($currentLevel >= $maxLevel) { + return false; + } + return true; + } + +} diff --git a/server/plugin/saiadmin/app/logic/tool/CrontabLogLogic.php b/server/plugin/saiadmin/app/logic/tool/CrontabLogLogic.php new file mode 100644 index 0000000..cae9b10 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/CrontabLogLogic.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use plugin\saiadmin\app\model\tool\CrontabLog; +use plugin\saiadmin\basic\think\BaseLogic; + +/** + * 定时任务日志逻辑层 + */ +class CrontabLogLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new CrontabLog(); + } + +} diff --git a/server/plugin/saiadmin/app/logic/tool/CrontabLogic.php b/server/plugin/saiadmin/app/logic/tool/CrontabLogic.php new file mode 100644 index 0000000..e269866 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/CrontabLogic.php @@ -0,0 +1,247 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Webman\Channel\Client as ChannelClient; +use plugin\saiadmin\app\model\tool\Crontab; +use plugin\saiadmin\app\model\tool\CrontabLog; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; + +/** + * 定时任务逻辑层 + */ +class CrontabLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new Crontab(); + } + + /** + * 添加任务 + */ + public function add($data): bool + { + $second = $data['second']; + $minute = $data['minute']; + $hour = $data['hour']; + $week = $data['week']; + $day = $data['day']; + $month = $data['month']; + + // 规则处理 + $rule = match ($data['task_style']) { + 1 => "0 {$minute} {$hour} * * *", + 2 => "0 {$minute} * * * *", + 3 => "0 {$minute} */{$hour} * * *", + 4 => "0 */{$minute} * * * *", + 5 => "*/{$second} * * * * *", + 6 => "0 {$minute} {$hour} * * {$week}", + 7 => "0 {$minute} {$hour} {$day} * *", + 8 => "0 {$minute} {$hour} {$day} {$month} *", + default => throw new ApiException("任务类型异常"), + }; + + // 定时任务模型新增 + $model = Crontab::create([ + 'name' => $data['name'], + 'type' => $data['type'], + 'task_style' => $data['task_style'], + 'rule' => $rule, + 'target' => $data['target'], + 'parameter' => $data['parameter'], + 'status' => $data['status'], + 'remark' => $data['remark'], + ]); + + $id = $model->getKey(); + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $id]); + + return true; + } + + /** + * 修改任务 + */ + public function edit($id, $data): bool + { + $second = $data['second']; + $minute = $data['minute']; + $hour = $data['hour']; + $week = $data['week']; + $day = $data['day']; + $month = $data['month']; + + // 规则处理 + $rule = match ($data['task_style']) { + 1 => "0 {$minute} {$hour} * * *", + 2 => "0 {$minute} * * * *", + 3 => "0 {$minute} */{$hour} * * *", + 4 => "0 */{$minute} * * * *", + 5 => "*/{$second} * * * * *", + 6 => "0 {$minute} {$hour} * * {$week}", + 7 => "0 {$minute} {$hour} {$day} * *", + 8 => "0 {$minute} {$hour} {$day} {$month} *", + default => throw new ApiException("任务类型异常"), + }; + + // 查询任务数据 + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + + $result = $model->save([ + 'name' => $data['name'], + 'type' => $data['type'], + 'task_style' => $data['task_style'], + 'rule' => $rule, + 'target' => $data['target'], + 'parameter' => $data['parameter'], + 'status' => $data['status'], + 'remark' => $data['remark'], + ]); + if ($result) { + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $id]); + } + + // 修改任务数据 + return $result; + } + + /** + * 删除定时任务 + * @param $ids + * @return bool + * @throws Exception + */ + public function destroy($ids): bool + { + if (is_array($ids)) { + if (count($ids) > 1) { + throw new ApiException('禁止批量删除操作'); + } + $ids = $ids[0]; + } + $result = parent::destroy($ids); + if ($result) { + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $ids]); + } + return $result; + } + + /** + * 修改状态 + * @param $id + * @param $status + * @return bool + */ + public function changeStatus($id, $status): bool + { + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + $result = $model->save(['status' => $status]); + if ($result) { + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $id]); + } + return $result; + } + + /** + * 执行定时任务 + * @param $id + * @return bool + */ + public function run($id): bool + { + $info = $this->model->where('status', 1)->findOrEmpty($id); + if ($info->isEmpty()) { + return false; + } + $data['crontab_id'] = $info->id; + $data['name'] = $info->name; + $data['target'] = $info->target; + $data['parameter'] = $info->parameter; + switch ($info->type) { + case 1: + // URL任务GET + $httpClient = new Client([ + 'timeout' => 5, + 'verify' => false, + ]); + try { + $httpClient->request('GET', $info->target); + $data['status'] = 1; + CrontabLog::create($data); + return true; + } catch (GuzzleException $e) { + $data['status'] = 2; + $data['exception_info'] = $e->getMessage(); + CrontabLog::create($data); + return false; + } + case 2: + // URL任务POST + $httpClient = new Client([ + 'timeout' => 5, + 'verify' => false, + ]); + try { + $res = $httpClient->request('POST', $info->target, [ + 'form_params' => json_decode($info->parameter ?? '', true) + ]); + $data['status'] = 1; + $data['exception_info'] = $res->getBody(); + CrontabLog::create($data); + return true; + } catch (GuzzleException $e) { + $data['status'] = 2; + $data['exception_info'] = $e->getMessage(); + CrontabLog::create($data); + return false; + } + case 3: + // 类任务 + $class_name = $info->target; + $method_name = 'run'; + $class = new $class_name; + if (method_exists($class, $method_name)) { + $return = $class->$method_name($info->parameter); + $data['status'] = 1; + $data['exception_info'] = $return; + CrontabLog::create($data); + return true; + } else { + $data['status'] = 2; + $data['exception_info'] = '类:' . $class_name . ',方法:run,未找到'; + CrontabLog::create($data); + return false; + + } + default: + return false; + } + } + +} diff --git a/server/plugin/saiadmin/app/logic/tool/GenerateColumnsLogic.php b/server/plugin/saiadmin/app/logic/tool/GenerateColumnsLogic.php new file mode 100644 index 0000000..a22e2bf --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/GenerateColumnsLogic.php @@ -0,0 +1,213 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use plugin\saiadmin\app\model\tool\GenerateColumns; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; + +/** + * 代码生成业务字段逻辑层 + */ +class GenerateColumnsLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new GenerateColumns(); + } + + public function saveExtra($data) + { + $default_column = ['create_time', 'update_time', 'created_by', 'updated_by', 'delete_time', 'remark']; + // 组装数据 + foreach ($data as $k => $item) { + + if ($item['column_name'] == 'delete_time') { + continue; + } + + $column = [ + 'table_id' => $item['table_id'], + 'column_name' => $item['column_name'], + 'column_comment' => $item['column_comment'], + 'column_type' => $item['column_type'], + 'default_value' => $item['default_value'], + 'is_pk' => ($item['column_key'] == 'PRI') ? 2 : 1, + 'is_required' => $item['is_nullable'] == 'NO' ? 2 : 1, + 'query_type' => 'eq', + 'view_type' => 'input', + 'sort' => count($data) - $k, + 'options' => $item['options'] ?? null + ]; + + // 设置默认选项 + if (!in_array($item['column_name'], $default_column) && empty($item['column_key'])) { + $column = array_merge( + $column, + [ + 'is_insert' => 2, + 'is_edit' => 2, + 'is_list' => 2, + 'is_query' => 1, + 'is_sort' => 1, + ] + ); + } + $keyList = [ + 'column_comment', + 'column_type', + 'default_value', + 'is_pk', + 'is_required', + 'is_insert', + 'is_edit', + 'is_list', + 'is_query', + 'is_sort', + 'query_type', + 'view_type', + 'dict_type', + 'options', + 'sort', + 'is_cover' + ]; + foreach ($keyList as $key) { + if (isset($item[$key])) + $column[$key] = $item[$key]; + } + GenerateColumns::create($this->fieldDispose($column)); + } + } + + public function update($data, $where) + { + $data['is_insert'] = $data['is_insert'] ? 2 : 1; + $data['is_edit'] = $data['is_edit'] ? 2 : 1; + $data['is_list'] = $data['is_list'] ? 2 : 1; + $data['is_query'] = $data['is_query'] ? 2 : 1; + $data['is_sort'] = $data['is_sort'] ? 2 : 1; + $data['is_required'] = $data['is_required'] ? 2 : 1; + $this->model->update($data, $where); + } + + private function fieldDispose(array $column): array + { + $object = new class { + public function viewTypeDispose(&$column): void + { + switch ($column['column_type']) { + case 'varchar': + $column['view_type'] = 'input'; + break; + // 富文本 + case 'text': + case 'longtext': + $column['is_list'] = 1; + $column['is_query'] = 1; + $column['view_type'] = 'editor'; + $options = [ + 'height' => 400, + ]; + $column['options'] = $options; + break; + // 日期字段 + case 'datetime': + $column['view_type'] = 'date'; + $options = [ + 'mode' => 'datetime' + ]; + $column['options'] = $options; + $column['query_type'] = 'between'; + break; + case 'date': + $column['view_type'] = 'date'; + $options = [ + 'mode' => 'date' + ]; + $column['options'] = $options; + $column['query_type'] = 'between'; + break; + } + } + + public function columnName(&$column): void + { + if (stristr($column['column_name'], 'name')) { + $column['is_query'] = 2; + $column['is_required'] = 2; + $column['query_type'] = 'like'; + } + + if (stristr($column['column_name'], 'title')) { + $column['is_query'] = 2; + $column['is_required'] = 2; + $column['query_type'] = 'like'; + } + + if (stristr($column['column_name'], 'type')) { + $column['is_query'] = 2; + $column['is_required'] = 2; + $column['query_type'] = 'eq'; + } + + if (stristr($column['column_name'], 'image')) { + $column['is_query'] = 1; + $column['view_type'] = 'uploadImage'; + $options = [ + 'multiple' => false, + 'limit' => 1, + ]; + $column['options'] = $options; + } + + if (stristr($column['column_name'], 'file')) { + $column['is_query'] = 1; + $column['view_type'] = 'uploadFile'; + $options = [ + 'multiple' => false, + 'limit' => 1, + ]; + $column['options'] = $options; + } + + if (stristr($column['column_name'], 'attach')) { + $column['is_query'] = 1; + $column['view_type'] = 'uploadFile'; + $options = [ + 'multiple' => false, + 'limit' => 1, + ]; + $column['options'] = $options; + } + + if ($column['column_name'] === 'sort') { + $column['view_type'] = 'inputNumber'; + } + + if ($column['column_name'] === 'status') { + $column['view_type'] = 'radio'; + $column['dict_type'] = 'data_status'; + } + + if (stristr($column['column_name'], 'is_')) { + $column['view_type'] = 'radio'; + $column['dict_type'] = 'yes_or_no'; + } + } + }; + + if (!$column['is_cover']) { + $object->viewTypeDispose($column); + $object->columnName($column); + } + $column['options'] = json_encode($column['options'], JSON_UNESCAPED_UNICODE); + return $column; + } +} diff --git a/server/plugin/saiadmin/app/logic/tool/GenerateTablesLogic.php b/server/plugin/saiadmin/app/logic/tool/GenerateTablesLogic.php new file mode 100644 index 0000000..67a6dd4 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/GenerateTablesLogic.php @@ -0,0 +1,478 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\logic\system\DatabaseLogic; +use plugin\saiadmin\app\model\system\SystemMenu; +use plugin\saiadmin\app\model\tool\GenerateTables; +use plugin\saiadmin\app\model\tool\GenerateColumns; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; +use plugin\saiadmin\utils\code\CodeZip; +use plugin\saiadmin\utils\code\CodeEngine; + +/** + * 代码生成业务逻辑层 + */ +class GenerateTablesLogic extends BaseLogic +{ + protected $columnLogic = null; + + protected $dataLogic = null; + + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new GenerateTables(); + $this->columnLogic = new GenerateColumnsLogic(); + $this->dataLogic = new DatabaseLogic(); + } + + /** + * 删除表和字段信息 + * @param $ids + * @return bool + */ + public function destroy($ids): bool + { + return $this->transaction(function () use ($ids) { + parent::destroy($ids); + GenerateColumns::destroy(function ($query) use ($ids) { + $query->where('table_id', 'in', $ids); + }); + return true; + }); + } + + /** + * 装载表信息 + * @param $names + * @param $source + * @return void + */ + public function loadTable($names, $source): void + { + $data = config('think-orm.connections'); + $config = $data[$source]; + if (!$config) { + throw new ApiException('数据库配置读取失败'); + } + + $prefix = $config['prefix'] ?? ''; + foreach ($names as $item) { + $class_name = $item['name']; + if (!empty($prefix)) { + $class_name = Helper::str_replace_once($prefix, '', $class_name); + } + $class_name = Helper::camel($class_name); + $tableInfo = [ + 'table_name' => $item['name'], + 'table_comment' => $item['comment'], + 'class_name' => $class_name, + 'business_name' => Helper::get_business($item['name']), + 'belong_menu_id' => 80, + 'menu_name' => $item['comment'], + 'tpl_category' => 'single', + 'template' => 'app', + 'stub' => 'think', + 'namespace' => '', + 'package_name' => '', + 'source' => $source, + 'generate_menus' => 'index,save,update,read,destroy', + ]; + $model = GenerateTables::create($tableInfo); + $columns = $this->dataLogic->getColumnList($item['name'], $source); + foreach ($columns as &$column) { + $column['table_id'] = $model->id; + $column['is_cover'] = false; + } + $this->columnLogic->saveExtra($columns); + } + } + + /** + * 同步表字段信息 + * @param $id + * @return void + */ + public function sync($id) + { + $model = $this->model->findOrEmpty($id); + // 拉取已有数据表信息 + $queryModel = $this->columnLogic->model->where('table_id', $id); + $columnLogicData = $this->columnLogic->getAll($queryModel); + $columnLogicList = []; + foreach ($columnLogicData as $item) { + $columnLogicList[$item['column_name']] = $item; + } + GenerateColumns::destroy(function ($query) use ($id) { + $query->where('table_id', $id); + }); + $columns = $this->dataLogic->getColumnList($model->table_name, $model->source ?? ''); + foreach ($columns as &$column) { + $column['table_id'] = $model->id; + $column['is_cover'] = false; + if (isset($columnLogicList[$column['column_name']])) { + // 存在历史信息的情况 + $getcolumnLogicItem = $columnLogicList[$column['column_name']]; + if ($getcolumnLogicItem['column_type'] == $column['column_type']) { + $column['is_cover'] = true; + foreach ($getcolumnLogicItem as $key => $item) { + $array = [ + 'column_comment', + 'column_type', + 'default_value', + 'is_pk', + 'is_required', + 'is_insert', + 'is_edit', + 'is_list', + 'is_query', + 'is_sort', + 'query_type', + 'view_type', + 'dict_type', + 'options', + 'sort', + 'is_cover' + ]; + if (in_array($key, $array)) { + $column[$key] = $item; + } + } + } + } + } + $this->columnLogic->saveExtra($columns); + } + + /** + * 代码预览 + * @param $id + * @return array + */ + public function preview($id): array + { + $data = $this->renderData($id); + + $codeEngine = new CodeEngine($data); + $controllerContent = $codeEngine->renderContent('php', 'controller.stub'); + $logicContent = $codeEngine->renderContent('php', 'logic.stub'); + $modelContent = $codeEngine->renderContent('php', 'model.stub'); + $validateContent = $codeEngine->renderContent('php', 'validate.stub'); + $sqlContent = $codeEngine->renderContent('sql', 'sql.stub'); + $indexContent = $codeEngine->renderContent('vue', 'index.stub'); + $editContent = $codeEngine->renderContent('vue', 'edit-dialog.stub'); + $searchContent = $codeEngine->renderContent('vue', 'table-search.stub'); + $apiContent = $codeEngine->renderContent('ts', 'api.stub'); + + // 返回生成内容 + return [ + [ + 'tab_name' => 'controller.php', + 'name' => 'controller', + 'lang' => 'php', + 'code' => $controllerContent + ], + [ + 'tab_name' => 'logic.php', + 'name' => 'logic', + 'lang' => 'php', + 'code' => $logicContent + ], + [ + 'tab_name' => 'model.php', + 'name' => 'model', + 'lang' => 'php', + 'code' => $modelContent + ], + [ + 'tab_name' => 'validate.php', + 'name' => 'validate', + 'lang' => 'php', + 'code' => $validateContent + ], + [ + 'tab_name' => 'sql.sql', + 'name' => 'sql', + 'lang' => 'sql', + 'code' => $sqlContent + ], + [ + 'tab_name' => 'index.vue', + 'name' => 'index', + 'lang' => 'html', + 'code' => $indexContent + ], + [ + 'tab_name' => 'edit-dialog.vue', + 'name' => 'edit-dialog', + 'lang' => 'html', + 'code' => $editContent + ], + [ + 'tab_name' => 'table-search.vue', + 'name' => 'table-search', + 'lang' => 'html', + 'code' => $searchContent + ], + [ + 'tab_name' => 'api.ts', + 'name' => 'api', + 'lang' => 'javascript', + 'code' => $apiContent + ] + ]; + } + + /** + * 生成到模块 + * @param $id + */ + public function genModule($id) + { + $data = $this->renderData($id); + + // 生成文件到模块 + $codeEngine = new CodeEngine($data); + $codeEngine->generateBackend('controller', $codeEngine->renderContent('php', 'controller.stub')); + $codeEngine->generateBackend('logic', $codeEngine->renderContent('php', 'logic.stub')); + $codeEngine->generateBackend('model', $codeEngine->renderContent('php', 'model.stub')); + $codeEngine->generateBackend('validate', $codeEngine->renderContent('php', 'validate.stub')); + $codeEngine->generateFrontend('index', $codeEngine->renderContent('vue', 'index.stub')); + $codeEngine->generateFrontend('edit-dialog', $codeEngine->renderContent('vue', 'edit-dialog.stub')); + $codeEngine->generateFrontend('table-search', $codeEngine->renderContent('vue', 'table-search.stub')); + $codeEngine->generateFrontend('api', $codeEngine->renderContent('ts', 'api.stub')); + } + + /** + * 处理数据 + * @param $id + * @return array + */ + protected function renderData($id): array + { + $table = $this->model->findOrEmpty($id); + if (!in_array($table['template'], ["plugin", "app"])) { + throw new ApiException('应用类型必须为plugin或者app'); + } + if (empty($table['namespace'])) { + throw new ApiException('请先设置应用名称'); + } + + $columns = $this->columnLogic->where('table_id', $id) + ->order('sort', 'desc') + ->select() + ->toArray(); + $pk = 'id'; + foreach ($columns as &$column) { + if ($column['is_pk'] == 2) { + $pk = $column['column_name']; + } + if ($column['column_name'] == 'delete_time') { + unset($column['column_name']); + } + } + + // 处理特殊变量 + if ($table['template'] == 'plugin') { + $namespace_start = "plugin\\" . $table['namespace'] . "\\app\\admin\\"; + $namespace_start_model = "plugin\\" . $table['namespace'] . "\\app\\"; + $namespace_end = "\\" . $table['package_name']; + $url_path = 'app/' . $table['namespace'] . '/admin/' . $table['package_name'] . '/' . $table['class_name']; + $route = 'app/'; + } else { + $namespace_start = "app\\" . $table['namespace'] . "\\"; + $namespace_start_model = "app\\" . $table['namespace'] . "\\"; + $namespace_end = "\\" . $table['package_name']; + $url_path = $table['namespace'] . '/' . $table['package_name'] . '/' . $table['class_name']; + $route = ''; + } + + $config = config('think-orm'); + + $data = $table->toArray(); + $data['pk'] = $pk; + $data['namespace_start'] = $namespace_start; + $data['namespace_start_model'] = $namespace_start_model; + $data['namespace_end'] = $namespace_end; + $data['url_path'] = $url_path; + $data['route'] = $route; + $data['tables'] = [$data]; + $data['columns'] = $columns; + $data['db_source'] = $config['default'] ?? 'mysql'; + + return $data; + } + + /** + * 生成到模块 + */ + public function generateFile($id) + { + $table = $this->model->where('id', $id)->findOrEmpty(); + if ($table->isEmpty()) { + throw new ApiException('请选择要生成的表'); + } + $debug = config('app.debug', true); + if (!$debug) { + throw new ApiException('非调试模式下,不允许生成文件'); + } + $this->updateMenu($table); + $this->genModule($id); + UserMenuCache::clearMenuCache(); + } + + /** + * 代码生成下载 + */ + public function generate($idsArr): array + { + $zip = new CodeZip(); + $tables = $this->model->where('id', 'in', $idsArr)->select()->toArray(); + foreach ($idsArr as $table_id) { + $data = $this->renderData($table_id); + $data['tables'] = $tables; + $codeEngine = new CodeEngine($data); + $codeEngine->generateTemp(); + } + + $filename = 'saiadmin.zip'; + $download = $zip->compress(); + + return compact('filename', 'download'); + } + + /** + * 处理菜单列表 + * @param $tables + */ + public function updateMenu($tables) + { + /*不存在的情况下进行新建操作*/ + $url_path = $tables['namespace'] . ":" . $tables['package_name'] . ':' . $tables['business_name']; + $code = $tables['namespace'] . "/" . $tables['package_name'] . '/' . $tables['business_name']; + $path = $tables['package_name'] . '/' . $tables['business_name']; + $component = $tables['namespace'] . "/" . $tables['package_name'] . '/' . $tables['business_name']; + + /*先获取一下已有的路由中是否包含当前ID的路由的核心信息*/ + $model = new SystemMenu(); + $tableMenu = $model->where('generate_id', $tables['id'])->findOrEmpty(); + $fistMenu = [ + 'parent_id' => $tables['belong_menu_id'], + 'name' => $tables['menu_name'], + 'code' => $code, + 'path' => $path, + 'icon' => 'ri:home-2-line', + 'component' => "/plugin/$component/index", + 'type' => 2, + 'sort' => 100, + 'is_iframe' => 2, + 'is_keep_alive' => 2, + 'is_hidden' => 2, + 'is_fixed_tab' => 2, + 'is_full_page' => 2, + 'generate_id' => $tables['id'] + ]; + if ($tableMenu->isEmpty()) { + $temp = SystemMenu::create($fistMenu); + $fistMenuId = $temp->id; + } else { + $fistMenu['id'] = $tableMenu['id']; + $tableMenu->save($fistMenu); + $fistMenuId = $tableMenu['id']; + } + /*开始进行子权限的判定操作*/ + $childNodes = [ + ['name' => '列表', 'key' => 'index'], + ['name' => '保存', 'key' => 'save'], + ['name' => '更新', 'key' => 'update'], + ['name' => '读取', 'key' => 'read'], + ['name' => '删除', 'key' => 'destroy'], + ]; + + foreach ($childNodes as $node) { + $nodeData = $model->where('parent_id', $fistMenuId)->where('generate_key', $node['key'])->findOrEmpty(); + $childNodeData = [ + 'parent_id' => $fistMenuId, + 'name' => $node['name'], + 'slug' => "$url_path:{$node['key']}", + 'type' => 3, + 'sort' => 100, + 'is_iframe' => 2, + 'is_keep_alive' => 2, + 'is_hidden' => 2, + 'is_fixed_tab' => 2, + 'is_full_page' => 2, + 'generate_key' => $node['key'] + ]; + if (!empty($nodeData)) { + $childNodeData['id'] = $nodeData['id']; + $nodeData->save($childNodeData); + } else { + $menuModel = new SystemMenu(); + $menuModel->save($childNodeData); + } + } + } + + /** + * 获取数据表字段信息 + * @param $table_id + * @return mixed + */ + public function getTableColumns($table_id): mixed + { + $query = $this->columnLogic->where('table_id', $table_id); + return $this->columnLogic->getAll($query); + } + + /** + * 编辑数据 + * @param $id + * @param $data + * @return mixed + */ + public function edit($id, $data): mixed + { + $columns = $data['columns']; + + unset($data['columns']); + + if (!empty($data['belong_menu_id'])) { + $data['belong_menu_id'] = is_array($data['belong_menu_id']) ? array_pop($data['belong_menu_id']) : $data['belong_menu_id']; + } else { + $data['belong_menu_id'] = 0; + } + + $data['generate_menus'] = implode(',', $data['generate_menus']); + + if (empty($data['options'])) { + unset($data['options']); + } + + $data['options'] = json_encode($data['options'], JSON_UNESCAPED_UNICODE); + + // 更新业务表 + $this->update($data, ['id' => $id]); + + // 更新业务字段表 + foreach ($columns as $column) { + if ($column['options']) { + $column['options'] = json_encode($column['options'], JSON_NUMERIC_CHECK); + } + $this->columnLogic->update($column, ['id' => $column['id']]); + } + + return true; + } + +} diff --git a/server/plugin/saiadmin/app/middleware/CheckAuth.php b/server/plugin/saiadmin/app/middleware/CheckAuth.php new file mode 100644 index 0000000..0ccea7b --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/CheckAuth.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\ReflectionCache; +use plugin\saiadmin\exception\SystemException; + +/** + * 权限检查中间件 + */ +class CheckAuth implements MiddlewareInterface +{ + + public function process(Request $request, callable $handler) : Response + { + $controller = $request->controller; + $action = $request->action; + + // 通过反射获取控制器哪些方法不需要登录 + $noNeedLogin = ReflectionCache::getNoNeedLogin($controller); + + // 不登录访问,无需权限验证 + if (in_array($action, $noNeedLogin)) { + return $handler($request); + } + + // 登录信息 + $token = getCurrentInfo(); + if ($token === false) { + throw new SystemException('用户信息读取失败,无法访问或操作'); + } + + // 系统默认超级管理员,无需权限验证 + if ($token['id'] === 1) { + return $handler($request); + } + + // 2. 获取接口权限属性 (使用缓存类) + $permissions = ReflectionCache::getPermissionAttributes($controller, $action); + + if (!empty($permissions) && !empty($permissions['slug'])) { + // 用户权限缓存 + $auth = UserAuthCache::getUserAuth($token['id']); + + if (!$this->checkPermissions($permissions, $auth)) { + throw new SystemException('权限不足,无法访问或操作'); + } + } + + return $handler($request); + } + + /** + * 检查权限 + */ + private function checkPermissions(array $attr, array $userPermissions): bool + { + // 直接对比 slug + return in_array($attr['slug'], $userPermissions); + } + +} diff --git a/server/plugin/saiadmin/app/middleware/CheckLogin.php b/server/plugin/saiadmin/app/middleware/CheckLogin.php new file mode 100644 index 0000000..9e2c6b4 --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/CheckLogin.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; +use Tinywan\Jwt\JwtToken; +use plugin\saiadmin\app\cache\ReflectionCache; +use plugin\saiadmin\exception\ApiException; + +/** + * 登录检查中间件 + */ +class CheckLogin implements MiddlewareInterface +{ + public function process(Request $request, callable $handler): Response + { + // 通过反射获取控制器哪些方法不需要登录 + $noNeedLogin = ReflectionCache::getNoNeedLogin($request->controller); + // 访问的方法需要登录 + if (!in_array($request->action, $noNeedLogin)) { + try { + $token = JwtToken::getExtend(); + } catch (\Throwable $e) { + throw new ApiException('您的登录凭证错误或者已过期,请重新登录', 401); + } + if ($token['plat'] !== 'saiadmin') { + throw new ApiException('登录凭证校验失败'); + } + $request->setHeader('check_login', true); + $request->setHeader('check_admin', $token); + } + return $handler($request); + } +} diff --git a/server/plugin/saiadmin/app/middleware/CrossDomain.php b/server/plugin/saiadmin/app/middleware/CrossDomain.php new file mode 100644 index 0000000..967f131 --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/CrossDomain.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; + +/** + * 跨域中间件 + */ +class CrossDomain implements MiddlewareInterface +{ + public function process(Request $request, callable $handler) : Response + { + // 如果是options请求则返回一个空响应,否则继续向洋葱芯穿越,并得到一个响应 + $response = $request->method() == 'OPTIONS' ? response('') : $handler($request); + + // 给响应添加跨域相关的http头 + $response->withHeaders([ + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Allow-Origin' => $request->header('origin', '*'), + 'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'), + 'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'), + ]); + + return $response; + } +} diff --git a/server/plugin/saiadmin/app/middleware/SystemLog.php b/server/plugin/saiadmin/app/middleware/SystemLog.php new file mode 100644 index 0000000..3bd9ad4 --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/SystemLog.php @@ -0,0 +1,38 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Event\Event; +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\cache\ReflectionCache; + +class SystemLog implements MiddlewareInterface +{ + /** + * @param Request $request + * @param callable $handler + * @return Response + */ + public function process(Request $request, callable $handler): Response + { + // 通过反射获取控制器哪些方法不需要登录 + $noNeedLogin = ReflectionCache::getNoNeedLogin($request->controller); + // 访问的方法需要登录 + if (!in_array($request->action, $noNeedLogin)) { + try { + // 记录日志 + Event::emit('user.operateLog', true); + } catch (\Throwable $e) { + throw new ApiException('登录凭获取失败,请检查'); + } + } + return $handler($request); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemAttachment.php b/server/plugin/saiadmin/app/model/system/SystemAttachment.php new file mode 100644 index 0000000..7d31696 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemAttachment.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 附件模型 + * + * sa_system_attachment 附件信息表 + * + * @property $id 主键 + * @property $category_id 文件分类 + * @property $storage_mode 存储模式 + * @property $origin_name 原文件名 + * @property $object_name 新文件名 + * @property $hash 文件hash + * @property $mime_type 资源类型 + * @property $storage_path 存储目录 + * @property $suffix 文件后缀 + * @property $size_byte 字节数 + * @property $size_info 文件大小 + * @property $url url地址 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemAttachment extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_attachment'; + + /** + * 原文件名搜索 + */ + public function searchOriginNameAttr($query, $value) + { + $query->where('origin_name', 'like', '%' . $value . '%'); + } + + /** + * 文件类型搜索 + */ + public function searchMimeTypeAttr($query, $value) + { + $query->where('mime_type', 'like', $value . '/%'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemCategory.php b/server/plugin/saiadmin/app/model/system/SystemCategory.php new file mode 100644 index 0000000..005fcba --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemCategory.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 附件分类模型 + * + * sa_system_category 附件分类表 + * + * @property $id 分类ID + * @property $parent_id 父id + * @property $level 组集关系 + * @property $category_name 分类名称 + * @property $sort 排序 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemCategory extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_category'; + + /** + * 分类名称搜索 + */ + public function searchCategoryNameAttr($query, $value) + { + $query->where('category_name', 'like', '%' . $value . '%'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemConfig.php b/server/plugin/saiadmin/app/model/system/SystemConfig.php new file mode 100644 index 0000000..aab99cd --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemConfig.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 参数配置模型 + * + * sa_system_config 参数配置信息表 + * + * @property $id 编号 + * @property $group_id 组id + * @property $key 配置键名 + * @property $value 配置值 + * @property $name 配置名称 + * @property $input_type 数据输入类型 + * @property $config_select_data 配置选项数据 + * @property $sort 排序 + * @property $remark 备注 + * @property $created_by 创建人 + * @property $updated_by 更新人 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemConfig extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_config'; + + public function getConfigSelectDataAttr($value) + { + return json_decode($value ?? '', true); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemConfigGroup.php b/server/plugin/saiadmin/app/model/system/SystemConfigGroup.php new file mode 100644 index 0000000..56e9964 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemConfigGroup.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 参数配置分组模型 + * + * sa_system_config_group 参数配置分组表 + * + * @property $id 主键 + * @property $name 字典名称 + * @property $code 字典标示 + * @property $remark 备注 + * @property $created_by 创建人 + * @property $updated_by 更新人 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemConfigGroup extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_config_group'; + + /** + * 关联配置列表 + */ + public function configs() + { + return $this->hasMany(SystemConfig::class, 'group_id', 'id'); + } + + /** + * 名称搜索 + */ + public function searchNameAttr($query, $value) + { + return $query->where('name', 'like', '%' . $value . '%'); + } + + /** + * 编码搜索 + */ + public function searchCodeAttr($query, $value) + { + return $query->where('code', 'like', '%' . $value . '%'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemDept.php b/server/plugin/saiadmin/app/model/system/SystemDept.php new file mode 100644 index 0000000..99fc923 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemDept.php @@ -0,0 +1,62 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 部门模型 + * + * sa_system_dept 部门表 + * + * @property $id 编号 + * @property $parent_id 父级ID,0为根节点 + * @property $name 部门名称 + * @property $code 部门编码 + * @property $leader_id 部门负责人ID + * @property $level 祖级列表,格式: 0,1,5, + * @property $sort 排序,数字越小越靠前 + * @property $status 状态: 1启用, 0禁用 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemDept extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_dept'; + + /** + * 权限范围 + */ + public function scopeAuth($query, $value) + { + if (!empty($value)) { + $deptIds = [$value['id']]; + $deptLevel = $value['level'] . $value['id'] . ','; + $ids = static::whereLike('level', $deptLevel . '%')->column('id'); + $deptIds = array_merge($deptIds, $ids); + $query->whereIn('id', $deptIds); + } + } + + /** + * 部门领导 + */ + public function leader() + { + return $this->hasOne(SystemUser::class, 'id', 'leader_id'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemDictData.php b/server/plugin/saiadmin/app/model/system/SystemDictData.php new file mode 100644 index 0000000..27f66ad --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemDictData.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 字典数据模型 + * + * sa_system_dict_data 字典数据表 + * + * @property $id 主键 + * @property $type_id 字典类型ID + * @property $label 字典标签 + * @property $value 字典值 + * @property $color 字典颜色 + * @property $code 字典标示 + * @property $sort 排序 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemDictData extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_dict_data'; + + /** + * 关键字搜索 + */ + public function searchKeywordsAttr($query, $value) + { + $query->where('label|code', 'LIKE', "%$value%"); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemDictType.php b/server/plugin/saiadmin/app/model/system/SystemDictType.php new file mode 100644 index 0000000..b7990ab --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemDictType.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 字典类型模型 + * + * sa_system_dict_type 字典类型表 + * + * @property $id 主键 + * @property $name 字典名称 + * @property $code 字典标示 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemDictType extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_dict_type'; + + /** + * 关联字典数据 + */ + public function dicts() + { + return $this->hasMany(SystemDictData::class, 'type_id', 'id'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemLoginLog.php b/server/plugin/saiadmin/app/model/system/SystemLoginLog.php new file mode 100644 index 0000000..1aaba87 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemLoginLog.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 登录日志模型 + * + * sa_system_login_log 登录日志表 + * + * @property $id 主键 + * @property $username 用户名 + * @property $ip 登录IP地址 + * @property $ip_location IP所属地 + * @property $os 操作系统 + * @property $browser 浏览器 + * @property $status 登录状态 + * @property $message 提示消息 + * @property $login_time 登录时间 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 更新时间 + */ +class SystemLoginLog extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_login_log'; + + /** + * 时间范围搜索 + */ + public function searchLoginTimeAttr($query, $value) + { + $query->whereTime('login_time', 'between', $value); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemMail.php b/server/plugin/saiadmin/app/model/system/SystemMail.php new file mode 100644 index 0000000..367c2c1 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemMail.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 邮件记录模型 + * + * sa_system_mail 邮件记录 + * + * @property $id 编号 + * @property $gateway 网关 + * @property $from 发送人 + * @property $email 接收人 + * @property $code 验证码 + * @property $content 邮箱内容 + * @property $status 发送状态 + * @property $response 返回结果 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemMail extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_mail'; + + /** + * 发送人搜索 + */ + public function searchFromAttr($query, $value) + { + $query->where('from', 'like', '%' . $value . '%'); + } + + /** + * 接收人搜索 + */ + public function searchEmailAttr($query, $value) + { + $query->where('email', 'like', '%' . $value . '%'); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemMenu.php b/server/plugin/saiadmin/app/model/system/SystemMenu.php new file mode 100644 index 0000000..e26261b --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemMenu.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 菜单模型 + * + * sa_system_menu 菜单权限表 + * + * @property $id + * @property $parent_id 父级ID + * @property $name 菜单名称 + * @property $code 组件名称 + * @property $slug 权限标识,如 user:list, user:add + * @property $type 类型: 1目录, 2菜单, 3按钮/API + * @property $path 路由地址或API路径 + * @property $component 前端组件路径,如 layout/User + * @property $method 请求方式 + * @property $icon 图标 + * @property $sort 排序 + * @property $link_url 外部链接 + * @property $is_iframe 是否iframe + * @property $is_keep_alive 是否缓存 + * @property $is_hidden 是否隐藏 + * @property $is_fixed_tab 是否固定标签页 + * @property $is_full_page 是否全屏 + * @property $generate_id 生成id + * @property $generate_key 生成key + * @property $status 状态 + * @property $remark + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemMenu extends BaseModel +{ + // 完整数据库表名称 + protected $table = 'sa_system_menu'; + // 主键 + protected $pk = 'id'; + + /** + * Id搜索 + */ + public function searchIdAttr($query, $value) + { + $query->whereIn('id', $value); + } + + public function searchNameAttr($query, $value) + { + $query->where('name', 'like', '%' . $value . '%'); + } + + public function searchPathAttr($query, $value) + { + $query->where('path', 'like', '%' . $value . '%'); + } + + public function searchMenuAttr($query, $value) + { + if (!empty($value)) { + $query->whereIn('type', [1, 2]); + } + } + + /** + * Type搜索 + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('type', $value); + } else { + $query->where('type', $value); + } + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemOperLog.php b/server/plugin/saiadmin/app/model/system/SystemOperLog.php new file mode 100644 index 0000000..4c95df3 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemOperLog.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 操作日志模型 + * + * sa_system_oper_log 操作日志表 + * + * @property $id 主键 + * @property $username 用户名 + * @property $app 应用名称 + * @property $method 请求方式 + * @property $router 请求路由 + * @property $service_name 业务名称 + * @property $ip 请求IP地址 + * @property $ip_location IP所属地 + * @property $request_data 请求数据 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 更新时间 + */ +class SystemOperLog extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_oper_log'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemPost.php b/server/plugin/saiadmin/app/model/system/SystemPost.php new file mode 100644 index 0000000..5f26cdc --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemPost.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 岗位模型 + * + * sa_system_post 岗位信息表 + * + * @property $id 主键 + * @property $name 岗位名称 + * @property $code 岗位代码 + * @property $sort 排序 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemPost extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_post'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemRole.php b/server/plugin/saiadmin/app/model/system/SystemRole.php new file mode 100644 index 0000000..f5ac328 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemRole.php @@ -0,0 +1,78 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 角色模型 + * + * sa_system_role 角色表 + * + * @property $id + * @property $name 角色名称 + * @property $code 角色标识,如: hr_manager + * @property $level 角色级别:用于行政控制,不可操作级别大于自己的角色 + * @property $data_scope 数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义 + * @property $remark 备注 + * @property $sort + * @property $status 状态: 1启用, 0禁用 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemRole extends BaseModel +{ + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 数据表完整名称 + * @var string + */ + protected $table = 'sa_system_role'; + + /** + * 权限范围 + */ + public function scopeAuth($query, $value) + { + $id = $value['id']; + $roles = $value['roles']; + if ($id > 1) { + $ids = []; + foreach ($roles as $item) { + $ids[] = $item['id']; + $temp = static::whereRaw('FIND_IN_SET("' . $item['id'] . '", level) > 0')->column('id'); + $ids = array_merge($ids, $temp); + } + $query->where('id', 'in', array_unique($ids)); + } + } + + /** + * 通过中间表获取菜单 + */ + public function menus() + { + return $this->belongsToMany(SystemMenu::class, SystemRoleMenu::class, 'menu_id', 'role_id'); + } + + /** + * 通过中间表获取部门 + */ + public function depts() + { + return $this->belongsToMany(SystemDept::class, SystemRoleDept::class, 'dept_id', 'role_id'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemRoleDept.php b/server/plugin/saiadmin/app/model/system/SystemRoleDept.php new file mode 100644 index 0000000..e6590d5 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemRoleDept.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 角色部门关联模型 + * + * sa_system_role_dept 角色-自定义数据权限关联 + * + * @property $id + * @property $role_id + * @property $dept_id + */ +class SystemRoleDept extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_role_dept'; +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemRoleMenu.php b/server/plugin/saiadmin/app/model/system/SystemRoleMenu.php new file mode 100644 index 0000000..10c16bf --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemRoleMenu.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 角色菜单关联模型 + * + * sa_system_role_menu 角色权限关联 + * + * @property $id + * @property $role_id + * @property $menu_id + */ +class SystemRoleMenu extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_role_menu'; +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemUser.php b/server/plugin/saiadmin/app/model/system/SystemUser.php new file mode 100644 index 0000000..44475f9 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemUser.php @@ -0,0 +1,96 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 用户信息模型 + * + * sa_system_user 用户表 + * + * @property $id + * @property $username 登录账号 + * @property $password 加密密码 + * @property $realname 真实姓名 + * @property $gender 性别 + * @property $avatar 头像 + * @property $email 邮箱 + * @property $phone 手机号 + * @property $signed 个性签名 + * @property $dashboard 工作台 + * @property $dept_id 主归属部门 + * @property $is_super 是否超级管理员: 1是 + * @property $status 状态: 1启用, 2禁用 + * @property $remark 备注 + * @property $login_time 最后登录时间 + * @property $login_ip 最后登录IP + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemUser extends BaseModel +{ + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 数据表完整名称 + * @var string + */ + protected $table = 'sa_system_user'; + + public function searchKeywordAttr($query, $value) + { + if ($value) { + $query->where('username|realname|phone', 'like', '%' . $value . '%'); + } + } + + /** + * 权限范围 - 过滤部门用户 + */ + public function scopeAuth($query, $value) + { + if (!empty($value)) { + $deptIds = [$value['id']]; + $deptLevel = $value['level'] . $value['id'] . ','; + $dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id'); + $deptIds = array_merge($deptIds, $dept_ids); + $query->whereIn('dept_id', $deptIds); + } + } + + /** + * 通过中间表关联角色 + */ + public function roles() + { + return $this->belongsToMany(SystemRole::class, SystemUserRole::class, 'role_id', 'user_id'); + } + + /** + * 通过中间表关联岗位 + */ + public function posts() + { + return $this->belongsToMany(SystemPost::class, SystemUserPost::class, 'post_id', 'user_id'); + } + + /** + * 通过中间表关联部门 + */ + public function depts() + { + return $this->belongsTo(SystemDept::class, 'dept_id', 'id'); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemUserPost.php b/server/plugin/saiadmin/app/model/system/SystemUserPost.php new file mode 100644 index 0000000..0866ef3 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemUserPost.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 用户岗位关联模型 + * + * sa_system_user_post 用户与岗位关联表 + * + * @property $id 主键 + * @property $user_id 用户主键 + * @property $post_id 岗位主键 + */ +class SystemUserPost extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_user_post'; +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemUserRole.php b/server/plugin/saiadmin/app/model/system/SystemUserRole.php new file mode 100644 index 0000000..d88febe --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemUserRole.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 用户角色关联模型 + * + * sa_system_user_role 用户角色关联 + * + * @property $id + * @property $user_id + * @property $role_id + */ +class SystemUserRole extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_user_role'; + + /** + * 获取角色id + * @param mixed $user_id + * @return array + */ + public static function getRoleIds($user_id): array + { + return static::where('user_id', $user_id)->column('role_id'); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/Crontab.php b/server/plugin/saiadmin/app/model/tool/Crontab.php new file mode 100644 index 0000000..4adba78 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/Crontab.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 定时任务模型 + * + * sa_tool_crontab 定时任务信息表 + * + * @property $id 主键 + * @property $name 任务名称 + * @property $type 任务类型 + * @property $target 调用任务字符串 + * @property $parameter 调用任务参数 + * @property $task_style 执行类型 + * @property $rule 任务执行表达式 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class Crontab extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_crontab'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/CrontabLog.php b/server/plugin/saiadmin/app/model/tool/CrontabLog.php new file mode 100644 index 0000000..22b91b3 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/CrontabLog.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 定时任务日志模型 + * + * sa_tool_crontab_log 定时任务执行日志表 + * + * @property $id 主键 + * @property $crontab_id 任务ID + * @property $name 任务名称 + * @property $target 任务调用目标字符串 + * @property $parameter 任务调用参数 + * @property $exception_info 异常信息 + * @property $status 执行状态 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class CrontabLog extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_crontab_log'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/GenerateColumns.php b/server/plugin/saiadmin/app/model/tool/GenerateColumns.php new file mode 100644 index 0000000..e824518 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/GenerateColumns.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 代码生成业务字段模型 + */ +class GenerateColumns extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_generate_columns'; + + public function getOptionsAttr($value) + { + return json_decode($value ?? '', true); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/GenerateTables.php b/server/plugin/saiadmin/app/model/tool/GenerateTables.php new file mode 100644 index 0000000..edb03b8 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/GenerateTables.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 代码生成业务模型 + */ +class GenerateTables extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_generate_tables'; + + public function getOptionsAttr($value) + { + return json_decode($value ?? '', true); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/validate/system/SystemCategoryValidate.php b/server/plugin/saiadmin/app/validate/system/SystemCategoryValidate.php new file mode 100644 index 0000000..6266d46 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemCategoryValidate.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 附件分类验证器 + */ +class SystemCategoryValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'category_name' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'category_name' => '分类名称必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'add' => [ + 'category_name', + ], + 'edit' => [ + 'category_name', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemConfigGroupValidate.php b/server/plugin/saiadmin/app/validate/system/SystemConfigGroupValidate.php new file mode 100644 index 0000000..45ec372 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemConfigGroupValidate.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemConfigGroup; + +/** + * 字典类型验证器 + */ +class SystemConfigGroupValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'code' => 'require|alphaDash|unique:' . SystemConfigGroup::class, + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '组名称必须填写', + 'name.max' => '组名称最多不能超过16个字符', + 'name.chs' => '组名称必须是中文', + 'code.require' => '组标识必须填写', + 'code.alphaDash' => '组标识只能由英文字母组成', + 'code.unique' => '配置组标识不能重复', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'code', + ], + 'update' => [ + 'name', + 'code', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemConfigValidate.php b/server/plugin/saiadmin/app/validate/system/SystemConfigValidate.php new file mode 100644 index 0000000..6c44477 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemConfigValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典类型验证器 + */ +class SystemConfigValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'key' => 'require', + 'group_id' => 'require', + 'input_type' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '配置标题必须填写', + 'key' => '配置标识必须填写', + 'group_id' => '所属组必须填写', + 'input_type' => '输入组件必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'key', + 'group_id', + 'input_type', + ], + 'update' => [ + 'name', + 'key', + 'group_id', + 'input_type', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemCrontabValidate.php b/server/plugin/saiadmin/app/validate/system/SystemCrontabValidate.php new file mode 100644 index 0000000..1c203a5 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemCrontabValidate.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典类型验证器 + */ +class SystemCrontabValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'type' => 'require', + 'rule' => 'require', + 'target' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '任务名称必须填写', + 'type' => '任务类型必须填写', + 'rule' => '任务规则必须填写', + 'target' => '调用目标必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'type', + 'rule', + 'target', + 'status', + ], + 'update' => [ + 'name', + 'type', + 'rule', + 'target', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemDeptValidate.php b/server/plugin/saiadmin/app/validate/system/SystemDeptValidate.php new file mode 100644 index 0000000..88a9940 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemDeptValidate.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 部门验证器 + */ +class SystemDeptValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '部门名称必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'add' => [ + 'name', + 'status', + ], + 'edit' => [ + 'name', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemDictDataValidate.php b/server/plugin/saiadmin/app/validate/system/SystemDictDataValidate.php new file mode 100644 index 0000000..2f068ea --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemDictDataValidate.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典数据验证器 + */ +class SystemDictDataValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'label' => 'require', + 'value' => 'require', + 'status' => 'require', + 'type_id' => 'require', + 'code' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'label' => '字典标签必须填写', + 'value' => '字典键值必须填写', + 'status' => '状态必须填写', + 'type_id' => '字典类型必须填写', + 'code' => '字典标识必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'label', + 'value', + 'status', + 'type_id', + ], + 'update' => [ + 'label', + 'value', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemDictTypeValidate.php b/server/plugin/saiadmin/app/validate/system/SystemDictTypeValidate.php new file mode 100644 index 0000000..24c52b9 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemDictTypeValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemDictType; + +/** + * 字典类型验证器 + */ +class SystemDictTypeValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'code' => 'require|alphaDash|unique:' . SystemDictType::class, + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '字典名称必须填写', + 'name.max' => '字典名称最多不能超过16个字符', + 'code.require' => '字典标识必须填写', + 'code.alphaDash' => '字典标识只能由英文字母组成', + 'code.unique' => '字典标识已存在', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'code', + 'status', + ], + 'update' => [ + 'name', + 'code', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemMailValidate.php b/server/plugin/saiadmin/app/validate/system/SystemMailValidate.php new file mode 100644 index 0000000..5fd729c --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemMailValidate.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 邮件验证器 + */ +class SystemMailValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'gateway' => 'require', + 'from' => 'require', + 'email' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'gateway' => '网关必须填写', + 'from' => '发件人必须填写', + 'email' => '接收人必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'gateway', + 'from', + 'email', + ], + 'update' => [ + 'gateway', + 'from', + 'email', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemMenuValidate.php b/server/plugin/saiadmin/app/validate/system/SystemMenuValidate.php new file mode 100644 index 0000000..ab65bc2 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemMenuValidate.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 菜单验证器 + */ +class SystemMenuValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '菜单名称必须填写', + 'name.max' => '菜单名称最多不能超过16个字符', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'status', + ], + 'update' => [ + 'name', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemNoticeValidate.php b/server/plugin/saiadmin/app/validate/system/SystemNoticeValidate.php new file mode 100644 index 0000000..cb164c8 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemNoticeValidate.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 系统公告验证器 + */ +class SystemNoticeValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'title' => 'require|min:4', + 'content' => 'require', + 'type' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'title.require' => '公告标题必须填写', + 'title.min' => '公告标题必须大于4个字符', + 'content' => '公告内容必须填写', + 'type' => '公告类型必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'title', + 'content', + 'type', + ], + 'update' => [ + 'title', + 'content', + 'type', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemPostValidate.php b/server/plugin/saiadmin/app/validate/system/SystemPostValidate.php new file mode 100644 index 0000000..64e52c0 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemPostValidate.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 用户角色验证器 + */ +class SystemPostValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'code' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '岗位名称必须填写', + 'code' => '岗位标识必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'code', + 'status', + ], + 'update' => [ + 'name', + 'code', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemRoleValidate.php b/server/plugin/saiadmin/app/validate/system/SystemRoleValidate.php new file mode 100644 index 0000000..2bf7149 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemRoleValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemRole; + +/** + * 用户角色验证器 + */ +class SystemRoleValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'code' => 'require|alphaDash|unique:' . SystemRole::class, + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '角色名称必须填写', + 'name.max' => '角色名称最多不能超过16个字符', + 'code.require' => '角色标识必须填写', + 'code.alphaDash' => '角色标识只能由英文字母组成', + 'code.unique' => '角色标识不能重复', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'add' => [ + 'name', + 'code', + 'status', + ], + 'edit' => [ + 'name', + 'code', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemUserValidate.php b/server/plugin/saiadmin/app/validate/system/SystemUserValidate.php new file mode 100644 index 0000000..e681d80 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemUserValidate.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemUser; + +/** + * 用户信息验证器 + */ +class SystemUserValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'username' => 'require|max:16|unique:' . SystemUser::class, + 'password' => 'require|min:6|max:16', + 'role_ids' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'username.require' => '用户名必须填写', + 'username.max' => '用户名最多不能超过16个字符', + 'username.unique' => '用户名不能重复', + 'password.require' => '密码必须填写', + 'password.min' => '密码最少为6位', + 'password.max' => '密码长度不能超过16位', + 'role_ids' => '角色必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'username', + 'password', + 'role_ids', + ], + 'update' => [ + 'username', + 'role_ids', + ], + ]; +} diff --git a/server/plugin/saiadmin/app/validate/tool/CrontabValidate.php b/server/plugin/saiadmin/app/validate/tool/CrontabValidate.php new file mode 100644 index 0000000..4548adb --- /dev/null +++ b/server/plugin/saiadmin/app/validate/tool/CrontabValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\tool; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典类型验证器 + */ +class CrontabValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'type' => 'require', + 'target' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '任务名称必须填写', + 'type' => '任务类型必须填写', + 'target' => '调用目标必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'type', + 'target', + 'status', + ], + 'update' => [ + 'name', + 'type', + 'target', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/tool/GenerateTablesValidate.php b/server/plugin/saiadmin/app/validate/tool/GenerateTablesValidate.php new file mode 100644 index 0000000..5b8943a --- /dev/null +++ b/server/plugin/saiadmin/app/validate/tool/GenerateTablesValidate.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\tool; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 用户角色验证器 + */ +class GenerateTablesValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'table_name' => 'require', + 'table_comment' => 'require', + 'class_name' => 'require|alphaDash', + 'business_name' => 'require|alphaDash', + 'template' => 'require', + 'namespace' => 'require', + 'menu_name' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'table_name' => '表名称必须填写', + 'table_comment' => '表描述必须填写', + 'class_name.require' => '实体类必须填写', + 'class_name.alphaDash' => '实体类必须是英文', + 'business_name.require' => '实体别名必须填写', + 'business_name.alphaDash' => '实体别名必须是英文', + 'template' => '模板必须填写', + 'namespace' => '命名空间必须填写', + 'menu_name' => '菜单名称必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'table_name', + 'table_comment', + 'class_name', + 'business_name', + 'template', + 'namespace', + 'menu_name', + ], + 'update' => [ + 'table_name', + 'table_comment', + 'class_name', + 'business_name', + 'template', + 'namespace', + 'menu_name', + ] + ]; + +} diff --git a/server/plugin/saiadmin/app/view/install/error.html b/server/plugin/saiadmin/app/view/install/error.html new file mode 100644 index 0000000..b93f1b4 --- /dev/null +++ b/server/plugin/saiadmin/app/view/install/error.html @@ -0,0 +1,253 @@ + + + + + + 安装错误 + + + + + +
+
+ +
+

【{{app}}-{{version}}】安装配置向导

+
+ +
+
+
+
+ + + + +
+

安装失败

+

错误提示:{{error}}

+ 重新安装 +
+
+
+ + diff --git a/server/plugin/saiadmin/app/view/install/index.html b/server/plugin/saiadmin/app/view/install/index.html new file mode 100644 index 0000000..4522dcf --- /dev/null +++ b/server/plugin/saiadmin/app/view/install/index.html @@ -0,0 +1,1023 @@ + + + + + + 应用安装向导 + + + + + + + + +
+ +
+
+ Logo +
+

【{{app}}-{{version}}】安装配置向导

+
+ + +
+
+ +
+
+
+
+
+
+
+ +
+
数据库配置
+
+
+
+ +
+
执行安装
+
+
+
+ +
+
安装完成
+
+
+
+ + +
+ +
+

数据库配置

+
+
+
+
+ +
+ + +
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+
+
+ + +
+

执行安装

+
+
+
+
+
+
+ +
+
+
准备开始安装...
+
刚刚
+
+
+
+
+ + +
+
+
+ +
+

安装成功

+

恭喜!已成功安装系统,请重启webman,启动前端项目进行访问。

+ +
+
+
+
+
+
+ + + + + + diff --git a/server/plugin/saiadmin/basic/AbstractLogic.php b/server/plugin/saiadmin/basic/AbstractLogic.php new file mode 100644 index 0000000..f1e5167 --- /dev/null +++ b/server/plugin/saiadmin/basic/AbstractLogic.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic; + +use plugin\saiadmin\basic\contracts\LogicInterface; + +/** + * 抽象逻辑层基类 + * 定义通用属性和方法签名,具体实现由各 ORM 驱动完成 + */ +abstract class AbstractLogic implements LogicInterface +{ + /** + * 模型注入 + * @var object + */ + protected $model; + + /** + * 管理员信息 + * @var array + */ + protected array $adminInfo; + + /** + * 排序字段 + * @var string + */ + protected string $orderField = ''; + + /** + * 排序方式 + * @var string + */ + protected string $orderType = 'ASC'; + + /** + * 初始化 + * @param $user + * @return void + */ + public function init($user): void + { + $this->adminInfo = $user; + } + + /** + * 设置排序字段 + * @param string $field + * @return static + */ + public function setOrderField(string $field): static + { + $this->orderField = $field; + return $this; + } + + /** + * 设置排序方式 + * @param string $type + * @return static + */ + public function setOrderType(string $type): static + { + $this->orderType = $type; + return $this; + } + + /** + * 获取模型实例 + * @return object + */ + public function getModel(): object + { + return $this->model; + } + + /** + * 获取上传的导入文件 + * @param $file + * @return string + */ + public function getImport($file): string + { + $full_dir = runtime_path() . '/resource/'; + if (!is_dir($full_dir)) { + mkdir($full_dir, 0777, true); + } + $ext = $file->getUploadExtension() ?: null; + $full_path = $full_dir . md5(time()) . '.' . $ext; + $file->move($full_path); + return $full_path; + } + + /** + * 方法调用代理到模型 + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call(string $name, array $arguments): mixed + { + return call_user_func_array([$this->model, $name], $arguments); + } +} diff --git a/server/plugin/saiadmin/basic/BaseController.php b/server/plugin/saiadmin/basic/BaseController.php new file mode 100644 index 0000000..0ddc4a6 --- /dev/null +++ b/server/plugin/saiadmin/basic/BaseController.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic; + +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\exception\ApiException; + +/** + * 基类 控制器继承此类 + */ +class BaseController extends OpenController +{ + + /** + * 当前登陆管理员信息 + */ + protected $adminInfo; + + /** + * 当前登陆管理员ID + */ + protected int $adminId; + + /** + * 当前登陆管理员账号 + */ + protected string $adminName; + + /** + * 逻辑层注入 + */ + protected $logic; + + /** + * 验证器注入 + */ + protected $validate; + + /** + * 初始化 + */ + protected function init(): void + { + // 登录模式赋值 + $isLogin = request()->header('check_login', false); + if ($isLogin) { + $result = request()->header('check_admin'); + $this->adminId = $result['id']; + $this->adminName = $result['username']; + $this->adminInfo = UserInfoCache::getUserInfo($result['id']); + + // 用户数据传递给逻辑层 + $this->logic && $this->logic->init($this->adminInfo); + } + } + + /** + * 验证器调用 + */ + protected function validate(string $scene, $data): bool + { + if ($this->validate) { + if (!$this->validate->scene($scene)->check($data)) { + throw new ApiException($this->validate->getError()); + } + } + return true; + } + +} diff --git a/server/plugin/saiadmin/basic/BaseValidate.php b/server/plugin/saiadmin/basic/BaseValidate.php new file mode 100644 index 0000000..80304af --- /dev/null +++ b/server/plugin/saiadmin/basic/BaseValidate.php @@ -0,0 +1,77 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic; + +use think\Validate; + +/** + * 验证器基类 + */ +class BaseValidate extends Validate +{ + + /** + * 验证是否唯一 + * @access public + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 + * @param array $data 数据 + * @param string $field 验证字段名 + * @return bool + */ + public function unique($value, $rule, array $data = [], string $field = ''): bool + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + + if (str_contains($rule[0], '\\')) { + // 指定模型类 + $db = new $rule[0]; + } else { + return false; + } + + $key = $rule[1] ?? $field; + $map = []; + + if (str_contains($key, '^')) { + // 支持多个字段验证 + $fields = explode('^', $key); + foreach ($fields as $key) { + if (isset($data[$key])) { + $map[] = [$key, '=', $data[$key]]; + } + } + } elseif (strpos($key, '=')) { + // 支持复杂验证 + parse_str($key, $array); + foreach ($array as $k => $val) { + $map[] = [$k, '=', $data[$k] ?? $val]; + } + } elseif (isset($data[$field])) { + $map[] = [$key, '=', $data[$field]]; + } + + $pk = !empty($rule[3]) ? $rule[3] : $db->getPrimaryKeyName(); + + if (is_string($pk)) { + if (isset($rule[2])) { + $map[] = [$pk, '<>', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[] = [$pk, '<>', $data[$pk]]; + } + } + + if ($db->where($map)->count() > 0) { + return false; + } + + return true; + } + +} diff --git a/server/plugin/saiadmin/basic/OpenController.php b/server/plugin/saiadmin/basic/OpenController.php new file mode 100644 index 0000000..764ebe9 --- /dev/null +++ b/server/plugin/saiadmin/basic/OpenController.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic; + +use support\Request; +use support\Response; + +/** + * 基类 控制器继承此类 + */ +class OpenController +{ + /** + * 构造方法 + * @access public + */ + public function __construct() + { + // 控制器初始化 + $this->init(); + } + + /** + * 成功返回json内容 + * @param array|string $data + * @param string $msg + * @param int $option + * @return Response + */ + public function success(array | string $data = [], string $msg = 'success', int $option = JSON_UNESCAPED_UNICODE): Response + { + if (is_string($data)) { + $msg = $data; + } + return json(['code' => 200, 'message' => $msg, 'data' => $data], $option); + } + + /** + * 失败返回json内容 + * @param string $msg + * @return Response + */ + public function fail(string $msg = 'fail'): Response + { + return json(['code' => 400, 'message' => $msg]); + } + + /** + * 初始化 + */ + protected function init(): void + { + // TODO + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/basic/contracts/LogicInterface.php b/server/plugin/saiadmin/basic/contracts/LogicInterface.php new file mode 100644 index 0000000..125b5db --- /dev/null +++ b/server/plugin/saiadmin/basic/contracts/LogicInterface.php @@ -0,0 +1,79 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic\contracts; + +/** + * Logic 接口定义 + * 所有 Logic 基类必须实现此接口 + */ +interface LogicInterface +{ + /** + * 初始化 + * @param mixed $user 用户信息 + * @return void + */ + public function init($user): void; + + /** + * 添加数据 + * @param array $data + * @return mixed + */ + public function add(array $data): mixed; + + /** + * 修改数据 + * @param mixed $id + * @param array $data + * @return mixed + */ + public function edit($id, array $data): mixed; + + /** + * 读取数据 + * @param mixed $id + * @return mixed + */ + public function read($id): mixed; + + /** + * 删除数据 + * @param mixed $ids + * @return bool + */ + public function destroy($ids): bool; + + /** + * 搜索器搜索 + * @param array $searchWhere + * @return mixed + */ + public function search(array $searchWhere = []): mixed; + + /** + * 分页查询数据 + * @param mixed $query + * @return mixed + */ + public function getList($query): mixed; + + /** + * 获取全部数据 + * @param mixed $query + * @return mixed + */ + public function getAll($query): mixed; + + /** + * 数据库事务操作 + * @param callable $closure + * @param bool $isTran + * @return mixed + */ + public function transaction(callable $closure, bool $isTran = true): mixed; +} diff --git a/server/plugin/saiadmin/basic/contracts/ModelInterface.php b/server/plugin/saiadmin/basic/contracts/ModelInterface.php new file mode 100644 index 0000000..a0060ab --- /dev/null +++ b/server/plugin/saiadmin/basic/contracts/ModelInterface.php @@ -0,0 +1,27 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic\contracts; + +/** + * Model 接口定义 + * 所有 Model 基类必须实现此接口 + */ +interface ModelInterface +{ + /** + * 获取表名 + * @return string + */ + public function getTableName(): string; + + /** + * 获取主键名 + * @return string + */ + public function getPrimaryKeyName(): string; + +} diff --git a/server/plugin/saiadmin/basic/eloquent/BaseLogic.php b/server/plugin/saiadmin/basic/eloquent/BaseLogic.php new file mode 100644 index 0000000..e66e91d --- /dev/null +++ b/server/plugin/saiadmin/basic/eloquent/BaseLogic.php @@ -0,0 +1,152 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic\eloquent; + +use support\Db; +use plugin\saiadmin\basic\AbstractLogic; +use plugin\saiadmin\exception\ApiException; + +/** + * Laravel Eloquent 逻辑层基类 + */ +class BaseLogic extends AbstractLogic +{ + /** + * 数据库事务操作 + * @param callable $closure + * @param bool $isTran + * @return mixed + */ + public function transaction(callable $closure, bool $isTran = true): mixed + { + return $isTran ? Db::transaction($closure) : $closure(); + } + + /** + * 添加数据 + * @param array $data + * @return mixed + */ + public function add(array $data): mixed + { + $model = $this->model->create($data); + return $model->getKey(); + } + + /** + * 修改数据 + * @param mixed $id + * @param array $data + * @return mixed + */ + public function edit($id, array $data): mixed + { + $model = $this->model->find($id); + if (!$model) { + throw new ApiException('数据不存在'); + } + return $model->update($data); + } + + /** + * 读取数据 + * @param mixed $id + * @return mixed + */ + public function read($id): mixed + { + $model = $this->model->find($id); + if (!$model) { + throw new ApiException('数据不存在'); + } + return $model; + } + + /** + * 删除数据 + * @param mixed $ids + * @return bool + */ + public function destroy($ids): bool + { + return $this->model->destroy($ids); + } + + /** + * 搜索器搜索 + * @param array $searchWhere + * @return mixed + */ + public function search(array $searchWhere = []): mixed + { + $withSearch = array_keys($searchWhere); + $data = []; + foreach ($searchWhere as $key => $value) { + if ($value !== '' && $value !== null && $value !== []) { + $data[$key] = $value; + } + } + $withSearch = array_keys($data); + return $this->model->withSearch($withSearch, $data); + } + + /** + * 分页查询数据 + * @param mixed $query + * @return mixed + */ + public function getList($query): mixed + { + $request = request(); + $saiType = $request ? $request->input('saiType', 'list') : 'list'; + $page = $request ? $request->input('page', 1) : 1; + $limit = $request ? $request->input('limit', 10) : 10; + $orderField = $request ? $request->input('orderField', '') : ''; + $orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType; + + if (empty($orderField)) { + $orderField = $this->orderField !== '' ? $this->orderField : $this->model->getKeyName(); + } + + $query->orderBy($orderField, $orderType); + + if ($saiType === 'all') { + return $query->get()->toArray(); + } + + $list = $query->paginate($limit, ['*'], 'page', $page); + + return [ + 'current_page' => $list->currentPage(), + 'per_page' => $list->perPage(), + 'last_page' => $list->lastPage(), + 'has_more' => $list->hasMorePages(), + 'total' => $list->total(), + 'data' => $list->items(), + ]; + } + + /** + * 获取全部数据 + * @param mixed $query + * @return mixed + */ + public function getAll($query): mixed + { + $request = request(); + $orderField = $request ? $request->input('orderField', '') : ''; + $orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType; + + if (empty($orderField)) { + $orderField = $this->orderField !== '' ? $this->orderField : $this->model->getKeyName(); + } + + $query->orderBy($orderField, $orderType); + return $query->get()->toArray(); + } + +} diff --git a/server/plugin/saiadmin/basic/eloquent/BaseModel.php b/server/plugin/saiadmin/basic/eloquent/BaseModel.php new file mode 100644 index 0000000..e0dafec --- /dev/null +++ b/server/plugin/saiadmin/basic/eloquent/BaseModel.php @@ -0,0 +1,171 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic\eloquent; + +use support\Model; +use Illuminate\Database\Eloquent\SoftDeletes; +use plugin\saiadmin\basic\contracts\ModelInterface; + +/** + * Laravel Eloquent 模型基类 + */ +class BaseModel extends Model implements ModelInterface +{ + use SoftDeletes; + + /** + * 创建时间字段 + */ + const CREATED_AT = 'create_time'; + + /** + * 更新时间字段 + */ + const UPDATED_AT = 'update_time'; + + /** + * 删除时间字段 + */ + const DELETED_AT = 'delete_time'; + + /** + * 隐藏字段 + * @var array + */ + protected $hidden = ['delete_time']; + + /** + * 不可批量赋值的属性 (为空表示全部可赋值) + * @var array + */ + protected $guarded = []; + + /** + * 类型转换 + * @return array + */ + protected function casts(): array + { + return [ + 'create_time' => 'datetime:Y-m-d H:i:s', + 'update_time' => 'datetime:Y-m-d H:i:s', + ]; + } + + /** + * 处理时区问题 + * @param \DateTimeInterface $date + * @return string + */ + protected function serializeDate(\DateTimeInterface $date): string + { + return $date->format($this->dateFormat ?: 'Y-m-d H:i:s'); + } + + /** + * 获取表名 + * @return string + */ + public function getTableName(): string + { + return $this->getTable(); + } + + /** + * 获取主键名 + * @return string + */ + public function getPrimaryKeyName(): string + { + return $this->getKeyName(); + } + + /** + * 搜索器搜索 + * @param array $fields + * @param array $data + * @return mixed + */ + public function withSearch(array $fields, array $data): mixed + { + $query = $this->newQuery(); + foreach ($fields as $field) { + $method = 'search' . ucfirst($this->toCamelCase($field)) . 'Attr'; + if (method_exists($this, $method) && isset($data[$field]) && $data[$field] !== '') { + $this->$method($query, $data[$field]); + } else { + $query->where($field, $data[$field]); + } + } + return $query; + } + + /** + * 将下划线命名转换为驼峰命名 + * @param string $str + * @return string + */ + protected function toCamelCase(string $str): string + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $str)))); + } + + /** + * 添加时间范围搜索 + * @param $query + * @param $value + */ + public function searchCreateTimeAttr($query, $value) + { + if (is_array($value)) { + $query->whereBetween('create_time', $value); + } else { + $query->where('create_time', '=', $value); + } + } + + /** + * 更新时间范围搜索 + * @param mixed $query + * @param mixed $value + */ + public function searchUpdateTimeAttr($query, $value) + { + if (is_array($value)) { + $query->whereBetween('update_time', $value); + } else { + $query->where('update_time', '=', $value); + } + } + + /** + * 模型启动事件 + * @return void + */ + protected static function boot(): void + { + parent::boot(); + + // 创建前事件 + static::creating(function ($model) { + $info = getCurrentInfo(); + $schema = $model->getConnection()->getSchemaBuilder(); + if ($info && $schema->hasColumn($model->getTable(), 'created_by')) { + $model->created_by = $info['id']; + } + }); + + // 保存前事件 + static::saving(function ($model) { + $info = getCurrentInfo(); + $schema = $model->getConnection()->getSchemaBuilder(); + if ($info && $schema->hasColumn($model->getTable(), 'updated_by')) { + $model->updated_by = $info['id']; + } + }); + } +} diff --git a/server/plugin/saiadmin/basic/think/BaseLogic.php b/server/plugin/saiadmin/basic/think/BaseLogic.php new file mode 100644 index 0000000..2c6ba28 --- /dev/null +++ b/server/plugin/saiadmin/basic/think/BaseLogic.php @@ -0,0 +1,142 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic\think; + +use support\think\Db; +use plugin\saiadmin\basic\AbstractLogic; +use plugin\saiadmin\exception\ApiException; + +/** + * ThinkORM 逻辑层基类 + */ +class BaseLogic extends AbstractLogic +{ + /** + * 数据库事务操作 + * @param callable $closure + * @param bool $isTran + * @return mixed + */ + public function transaction(callable $closure, bool $isTran = true): mixed + { + return $isTran ? Db::transaction($closure) : $closure(); + } + + /** + * 添加数据 + * @param array $data + * @return mixed + */ + public function add(array $data): mixed + { + $model = $this->model->create($data); + return $model->getKey(); + } + + /** + * 修改数据 + * @param mixed $id + * @param array $data + * @return mixed + */ + public function edit($id, array $data): mixed + { + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + return $model->save($data); + } + + /** + * 读取数据 + * @param mixed $id + * @return mixed + */ + public function read($id): mixed + { + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + return $model; + } + + /** + * 删除数据 + * @param mixed $ids + * @return bool + */ + public function destroy($ids): bool + { + return $this->model->destroy($ids); + } + + /** + * 搜索器搜索 + * @param array $searchWhere + * @return mixed + */ + public function search(array $searchWhere = []): mixed + { + $withSearch = array_keys($searchWhere); + $data = []; + foreach ($searchWhere as $key => $value) { + if ($value !== '' && $value !== null && $value !== []) { + $data[$key] = $value; + } + } + $withSearch = array_keys($data); + return $this->model->withSearch($withSearch, $data); + } + + /** + * 分页查询数据 + * @param mixed $query + * @return mixed + */ + public function getList($query): mixed + { + $request = request(); + $saiType = $request ? $request->input('saiType', 'list') : 'list'; + $page = $request ? $request->input('page', 1) : 1; + $limit = $request ? $request->input('limit', 10) : 10; + $orderField = $request ? $request->input('orderField', '') : ''; + $orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType; + + if (empty($orderField)) { + $orderField = $this->orderField !== '' ? $this->orderField : $this->model->getPk(); + } + + $query->order($orderField, $orderType); + + if ($saiType === 'all') { + return $query->select()->toArray(); + } + + return $query->paginate($limit, false, ['page' => $page])->toArray(); + } + + /** + * 获取全部数据 + * @param mixed $query + * @return mixed + */ + public function getAll($query): mixed + { + $request = request(); + $orderField = $request ? $request->input('orderField', '') : ''; + $orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType; + + if (empty($orderField)) { + $orderField = $this->orderField !== '' ? $this->orderField : $this->model->getPk(); + } + + $query->order($orderField, $orderType); + return $query->select()->toArray(); + } +} diff --git a/server/plugin/saiadmin/basic/think/BaseModel.php b/server/plugin/saiadmin/basic/think/BaseModel.php new file mode 100644 index 0000000..b3cc4d5 --- /dev/null +++ b/server/plugin/saiadmin/basic/think/BaseModel.php @@ -0,0 +1,117 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\basic\think; + +use support\think\Model; +use think\model\concern\SoftDelete; +use plugin\saiadmin\basic\contracts\ModelInterface; + +/** + * ThinkORM 模型基类 + */ +class BaseModel extends Model implements ModelInterface +{ + use SoftDelete; + + /** + * 删除时间字段 + * @var string + */ + protected $deleteTime = 'delete_time'; + + /** + * 创建时间字段 + * @var string + */ + protected $createTime = 'create_time'; + + /** + * 更新时间字段 + * @var string + */ + protected $updateTime = 'update_time'; + + /** + * 隐藏字段 + * @var array + */ + protected $hidden = ['delete_time']; + + /** + * 只读字段 + * @var array + */ + protected $readonly = ['created_by', 'create_time']; + + /** + * 获取表名 + * @return string + */ + public function getTableName(): string + { + return $this->getTable(); + } + + /** + * 获取主键名 + * @return string + */ + public function getPrimaryKeyName(): string + { + return $this->getPk(); + } + + /** + * 添加时间范围搜索 + * @param $query + * @param $value + */ + public function searchCreateTimeAttr($query, $value) + { + if (is_array($value)) { + $query->whereBetween('create_time', $value); + } else { + $query->where('create_time', '=', $value); + } + } + + /** + * 更新时间范围搜索 + * @param mixed $query + * @param mixed $value + */ + public function searchUpdateTimeAttr($query, $value) + { + if (is_array($value)) { + $query->whereBetween('update_time', $value); + } else { + $query->where('update_time', '=', $value); + } + } + + /** + * 新增前事件 + * @param Model $model + * @return void + */ + public static function onBeforeInsert($model): void + { + $info = getCurrentInfo(); + $info && $model->setAttr('created_by', $info['id']); + } + + /** + * 写入前事件 + * @param Model $model + * @return void + */ + public static function onBeforeWrite($model): void + { + $info = getCurrentInfo(); + $info && $model->setAttr('updated_by', $info['id']); + } +} diff --git a/server/plugin/saiadmin/command/SaiOrm.php b/server/plugin/saiadmin/command/SaiOrm.php new file mode 100644 index 0000000..40fa335 --- /dev/null +++ b/server/plugin/saiadmin/command/SaiOrm.php @@ -0,0 +1,182 @@ + '1. ThinkORM (TopThink)', + 'eloquent' => '2. Eloquent ORM (Laravel)', + 'exit' => '3. 退出,什么也不做', + ]; + + protected function configure(): void + { + $this->setName('sai:orm') + ->setDescription('切换 SaiAdmin 使用的 ORM'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->title('SaiAdmin ORM 切换工具'); + $io->text('此命令只切换 saiadmin 框架核心使用的 ORM, 不影响其他模块功能, 多种 ORM 可以同时使用!'); + $io->newLine(); + + // 创建选择问题(编号从1开始) + $helper = $this->getHelper('question'); + $choices = [ + 1 => '1. ThinkORM (TopThink)', + 2 => '2. Eloquent ORM (Laravel)', + 3 => '3. 退出,什么也不做', + ]; + $question = new ChoiceQuestion( + '请选择要使用的 ORM 框架:', + $choices, + 1 // 默认选中第一个 + ); + $question->setErrorMessage('选项 %s 无效'); + + // 获取用户选择 + $selected = $helper->ask($input, $output, $question); + + // 根据选择的文本反查 key + $selectedKey = array_search($selected, $choices); + + // 如果选择退出 + if ($selectedKey == 3) { + $io->newLine(); + $io->info('已退出,什么也没做。'); + return Command::SUCCESS; + } + + // 映射选项到 ORM 类型 + $ormMap = [1 => 'think', 2 => 'eloquent']; + $orm = $ormMap[$selectedKey]; + + $io->newLine(); + $io->section("您选择了: {$selected}"); + + // 确认操作 + if (!$io->confirm('确定要切换吗?这将覆盖 saiadmin 核心代码文件', true)) { + $io->warning('操作已取消'); + return Command::SUCCESS; + } + + // 设置路径 + $this->ormSourcePath = BASE_PATH . '/vendor/saithink/saiadmin/src/orm/' . $orm . '/app'; + $this->pluginAppPath = BASE_PATH . '/plugin/saiadmin/app'; + + // 检查源目录是否存在 + if (!is_dir($this->ormSourcePath)) { + $io->error("ORM 源目录不存在: {$this->ormSourcePath}"); + return Command::FAILURE; + } + + $io->section('开始复制文件...'); + + try { + $copiedFiles = $this->copyDirectory($this->ormSourcePath, $this->pluginAppPath, $io); + + $io->newLine(); + $io->success([ + "ORM 切换成功!", + "已切换到: {$selected}", + "复制文件数: {$copiedFiles}" + ]); + + $io->note([ + '请重启 webman 服务使更改生效', + '命令: php webman restart 或 php windows.php' + ]); + + return Command::SUCCESS; + } catch (\Exception $e) { + $io->error("切换失败: " . $e->getMessage()); + return Command::FAILURE; + } + } + + /** + * 递归复制目录 + * @param string $source 源目录 + * @param string $dest 目标目录 + * @param SymfonyStyle $io 输出接口 + * @return int 复制的文件数量 + */ + protected function copyDirectory(string $source, string $dest, SymfonyStyle $io): int + { + $count = 0; + + if (!is_dir($dest)) { + mkdir($dest, 0755, true); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + + // 先计算文件总数用于进度条 + $files = []; + foreach ($iterator as $item) { + if (!$item->isDir()) { + $files[] = $item; + } + } + + // 创建进度条 + $io->progressStart(count($files)); + + // 重新遍历并复制 + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $item) { + $destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); + + if ($item->isDir()) { + if (!is_dir($destPath)) { + mkdir($destPath, 0755, true); + } + } else { + copy($item->getPathname(), $destPath); + $count++; + $io->progressAdvance(); + } + } + + $io->progressFinish(); + + return $count; + } +} diff --git a/server/plugin/saiadmin/command/SaiPlugin.php b/server/plugin/saiadmin/command/SaiPlugin.php new file mode 100644 index 0000000..983cc45 --- /dev/null +++ b/server/plugin/saiadmin/command/SaiPlugin.php @@ -0,0 +1,297 @@ +addArgument('name', InputArgument::REQUIRED, 'App plugin name'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $io->title('SaiAdmin 插件创建工具'); + $io->text('此命令用于创建基于webman的 saiadmin 插件, 用于扩展 saiadmin 框架功能!'); + $io->newLine(); + + $name = $input->getArgument('name'); + $io->text("创建 SaiAdmin 插件 $name"); + + if (strpos($name, '/') !== false) { + $io->error('名称错误,名称必须不包含字符 \'/\''); + return self::FAILURE; + } + + // Create dir config/plugin/$name + if (is_dir($plugin_config_path = base_path() . "/plugin/$name")) { + $io->error("目录 $plugin_config_path 已存在"); + return self::FAILURE; + } + + $this->createAll($name); + + $io->newLine(); + $io->success("SaiAdmin 插件创建成功!"); + + return self::SUCCESS; + } + + /** + * @param $name + * @return void + */ + protected function createAll($name) + { + $base_path = base_path(); + $this->mkdir("$base_path/plugin/$name/app", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/admin/controller", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/admin/logic", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/api/controller", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/api/logic", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/cache", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/event", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/model", 0777, true); + $this->mkdir("$base_path/plugin/$name/app/middleware", 0777, true); + $this->mkdir("$base_path/plugin/$name/config", 0777, true); + $this->createControllerFile("$base_path/plugin/$name/app/api/controller/IndexController.php", $name); + $this->createFunctionsFile("$base_path/plugin/$name/app/functions.php"); + $this->createConfigFiles("$base_path/plugin/$name/config", $name); + } + + /** + * @param $path + * @return void + */ + protected function mkdir($path, $mode = 0777, $recursive = false) + { + if (is_dir($path)) { + return; + } + echo "Create $path\r\n"; + mkdir($path, $mode, $recursive); + } + + /** + * @param $path + * @param $name + * @return void + */ + protected function createControllerFile($path, $name) + { + $content = <<success([ + 'app' => '$name', + 'version' => '1.0.0', + ]); + } + +} + + +EOF; + file_put_contents($path, $content); + + } + + /** + * @param $file + * @return void + */ + protected function createFunctionsFile($file) + { + $content = << true, + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, + 'version' => '1.0.0' +]; + +EOF; + file_put_contents("$base/app.php", $content); + + // autoload.php + $content = << [ + base_path() . '/plugin/$name/app/functions.php', + ] +]; +EOF; + file_put_contents("$base/autoload.php", $content); + + // container.php + $content = << \\plugin\\saiadmin\\app\\exception\\Handler::class, +]; + +EOF; + file_put_contents("$base/exception.php", $content); + + // log.php + $content = << [ + 'handlers' => [ + [ + 'class' => Monolog\\Handler\\RotatingFileHandler::class, + 'constructor' => [ + runtime_path() . '/logs/$name.log', + 7, + Monolog\\Logger::DEBUG, + ], + 'formatter' => [ + 'class' => Monolog\\Formatter\\LineFormatter::class, + 'constructor' => [null, 'Y-m-d H:i:s', true], + ], + ] + ], + ], +]; + +EOF; + file_put_contents("$base/log.php", $content); + + // middleware.php + $content = << [ + CheckLogin::class, + CheckAuth::class, + SystemLog::class, + ], + 'api' => [ + ] +]; + +EOF; + file_put_contents("$base/middleware.php", $content); + + // process.php + $content = << true, + 'middleware' => [], // Static file Middleware +]; + +EOF; + file_put_contents("$base/static.php", $content); + + // translation.php + $content = << 'zh_CN', + // Fallback language + 'fallback_locale' => ['zh_CN', 'en'], + // Folder where language files are stored + 'path' => base_path() . "/plugin/$name/resource/translations", +]; + +EOF; + file_put_contents("$base/translation.php", $content); + + } + +} diff --git a/server/plugin/saiadmin/command/SaiUpgrade.php b/server/plugin/saiadmin/command/SaiUpgrade.php new file mode 100644 index 0000000..5e57831 --- /dev/null +++ b/server/plugin/saiadmin/command/SaiUpgrade.php @@ -0,0 +1,198 @@ +setName('sai:upgrade') + ->setDescription('升级 SaiAdmin 插件到最新版本'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->title('SaiAdmin 升级工具'); + $io->text([ + '此命令将从 vendor 目录复制最新版本的 saiadmin 插件文件到 plugin 目录', + '源目录: vendor/saithink/saiadmin/src/plugin/saiadmin', + '目标目录: plugin/saiadmin', + ]); + $io->newLine(); + + // 设置路径 + $this->sourcePath = BASE_PATH . '/vendor/saithink/saiadmin/src/plugin/saiadmin'; + $this->targetPath = BASE_PATH . '/plugin/saiadmin'; + + // 检查源目录是否存在 + if (!is_dir($this->sourcePath)) { + $io->error([ + "升级源目录不存在: {$this->sourcePath}", + "请确保已通过 composer 安装了 saithink/saiadmin 包", + ]); + return Command::FAILURE; + } + + // 获取版本信息 + $currentVersion = $this->getVersion($this->targetPath); + $latestVersion = $this->getVersion($this->sourcePath); + + // 显示版本信息 + $io->section('版本信息'); + $io->table( + ['项目', '版本'], + [ + ['当前版本', $currentVersion ?: '未知'], + ['最新版本', $latestVersion ?: '未知'], + ] + ); + + // 版本对比提示 + if ($currentVersion && $latestVersion) { + if (version_compare($currentVersion, $latestVersion, '>=')) { + $io->success('当前已是最新版本!'); + if (!$io->confirm('是否仍要继续覆盖安装?', false)) { + $io->info('操作已取消'); + return Command::SUCCESS; + } + } else { + $io->info("发现新版本: {$currentVersion} → {$latestVersion}"); + } + } + + // 警告信息 + $io->warning([ + "注意:此操作将覆盖 {$this->targetPath} 目录的现有文件!", + "建议在执行前备份您的自定义修改。", + ]); + + // 确认操作 + if (!$io->confirm('确定要执行升级操作吗?', false)) { + $io->info('操作已取消'); + return Command::SUCCESS; + } + + $io->section('开始升级...'); + + try { + $copiedFiles = $this->copyDirectory($this->sourcePath, $this->targetPath, $io); + + $io->newLine(); + $io->success([ + "SaiAdmin 升级成功!", + "复制文件数: {$copiedFiles}", + ]); + + $io->note([ + '请重启 webman 服务使更改生效', + '命令: php webman restart 或 php windows.php', + ]); + + return Command::SUCCESS; + } catch (\Exception $e) { + $io->error("升级失败: " . $e->getMessage()); + return Command::FAILURE; + } + } + + /** + * 递归复制目录 + * @param string $source 源目录 + * @param string $dest 目标目录 + * @param SymfonyStyle $io 输出接口 + * @return int 复制的文件数量 + */ + protected function copyDirectory(string $source, string $dest, SymfonyStyle $io): int + { + $count = 0; + + if (!is_dir($dest)) { + mkdir($dest, 0755, true); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + + // 先计算文件总数用于进度条 + $files = []; + foreach ($iterator as $item) { + if (!$item->isDir()) { + $files[] = $item; + } + } + + // 创建进度条 + $io->progressStart(count($files)); + + // 重新遍历并复制 + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $item) { + $destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); + + if ($item->isDir()) { + if (!is_dir($destPath)) { + mkdir($destPath, 0755, true); + } + } else { + copy($item->getPathname(), $destPath); + $count++; + $io->progressAdvance(); + } + } + + $io->progressFinish(); + + return $count; + } + + /** + * 从目录中获取版本号 + * @param string $path 插件目录路径 + * @return string|null 版本号 + */ + protected function getVersion(string $path): ?string + { + $configFile = $path . '/config/app.php'; + + if (!file_exists($configFile)) { + return null; + } + + try { + $config = include $configFile; + return $config['version'] ?? null; + } catch (\Exception $e) { + return null; + } + } +} diff --git a/server/plugin/saiadmin/config/app.php b/server/plugin/saiadmin/config/app.php new file mode 100644 index 0000000..98aa880 --- /dev/null +++ b/server/plugin/saiadmin/config/app.php @@ -0,0 +1,10 @@ + true, + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, + 'version' => '6.0.7' +]; diff --git a/server/plugin/saiadmin/config/autoload.php b/server/plugin/saiadmin/config/autoload.php new file mode 100644 index 0000000..6f5bcf2 --- /dev/null +++ b/server/plugin/saiadmin/config/autoload.php @@ -0,0 +1,6 @@ + [ + base_path() . '/plugin/saiadmin/app/functions.php', + ] +]; \ No newline at end of file diff --git a/server/plugin/saiadmin/config/container.php b/server/plugin/saiadmin/config/container.php new file mode 100644 index 0000000..34d7f5c --- /dev/null +++ b/server/plugin/saiadmin/config/container.php @@ -0,0 +1,2 @@ + [ + [plugin\saiadmin\app\event\SystemUser::class, 'login'], + ], + 'user.operateLog' => [ + [plugin\saiadmin\app\event\SystemUser::class, 'operateLog'], + ] +]; \ No newline at end of file diff --git a/server/plugin/saiadmin/config/exception.php b/server/plugin/saiadmin/config/exception.php new file mode 100644 index 0000000..07a5333 --- /dev/null +++ b/server/plugin/saiadmin/config/exception.php @@ -0,0 +1,5 @@ + \plugin\saiadmin\app\exception\Handler::class, +]; diff --git a/server/plugin/saiadmin/config/log.php b/server/plugin/saiadmin/config/log.php new file mode 100644 index 0000000..2ddea1b --- /dev/null +++ b/server/plugin/saiadmin/config/log.php @@ -0,0 +1,20 @@ + [ + 'handlers' => [ + [ + 'class' => Monolog\Handler\RotatingFileHandler::class, + 'constructor' => [ + runtime_path() . '/logs/saiadmin.log', + 7, + Monolog\Logger::DEBUG, + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + 'constructor' => [null, 'Y-m-d H:i:s', true], + ], + ] + ], + ], +]; diff --git a/server/plugin/saiadmin/config/middleware.php b/server/plugin/saiadmin/config/middleware.php new file mode 100644 index 0000000..b63eaaf --- /dev/null +++ b/server/plugin/saiadmin/config/middleware.php @@ -0,0 +1,13 @@ + [ + CheckLogin::class, + CheckAuth::class, + SystemLog::class, + ] +]; diff --git a/server/plugin/saiadmin/config/process.php b/server/plugin/saiadmin/config/process.php new file mode 100644 index 0000000..44a49b7 --- /dev/null +++ b/server/plugin/saiadmin/config/process.php @@ -0,0 +1,6 @@ + [ + 'handler' => plugin\saiadmin\process\Task::class + ] +]; diff --git a/server/plugin/saiadmin/config/route.php b/server/plugin/saiadmin/config/route.php new file mode 100644 index 0000000..013a604 --- /dev/null +++ b/server/plugin/saiadmin/config/route.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- +return [ + + 'access_exp' => 8 * 60 * 60, // 登录token有效期,默认8小时 + + // 验证码存储模式 + 'captcha' => [ + // 验证码存储模式 session或者cache + 'mode' => getenv('CAPTCHA_MODE'), + // 验证码过期时间 (秒) + 'expire' => 300, + ], + + // excel模板下载路径 + 'template' => base_path(). '/plugin/saiadmin/public/template', + + // excel导出文件路径 + 'export_path' => base_path() . '/plugin/saiadmin/public/export/', + + // 文件开启hash验证,开启后上传文件将会判断数据库中是否存在,如果存在直接获取 + 'file_hash' => false, + + // 用户信息缓存 + 'user_cache' => [ + 'prefix' => 'saiadmin:user_cache:info_', + 'expire' => 60 * 60 * 4, + 'dept' => 'saiadmin:user_cache:dept_', + 'role' => 'saiadmin:user_cache:role_', + 'post' => 'saiadmin:user_cache:post_', + ], + + // 用户权限缓存 + 'button_cache' => [ + 'prefix' => 'saiadmin:button_cache:user_', + 'expire' => 60 * 60 * 2, + 'all' => 'saiadmin:button_cache:all', + 'role' => 'saiadmin:button_cache:role_', + 'tag' => 'saiadmin:button_cache', + ], + + // 用户菜单缓存 + 'menu_cache' => [ + 'prefix' => 'saiadmin:menu_cache:user_', + 'expire' => 60 * 60 * 24 * 7, + 'tag' => 'saiadmin:menu_cache', + ], + + // 字典缓存 + 'dict_cache' => [ + 'expire' => 60 * 60 * 24 * 365, + 'tag' => 'saiadmin:dict_cache', + ], + + // 配置数据缓存 + 'config_cache' => [ + 'expire' => 60 * 60 * 24 * 365, + 'prefix' => 'saiadmin:config_cache:config_', + 'tag' => 'saiadmin:config_cache' + ], + + // 反射缓存 + 'reflection_cache' => [ + 'tag' => 'saiadmin:reflection', + 'expire' => 60 * 60 * 24 * 365, + 'no_need' => 'saiadmin:reflection_cache:no_need_', + 'attr' => 'saiadmin:reflection_cache:attr_', + ], + +]; diff --git a/server/plugin/saiadmin/config/static.php b/server/plugin/saiadmin/config/static.php new file mode 100644 index 0000000..1d5331f --- /dev/null +++ b/server/plugin/saiadmin/config/static.php @@ -0,0 +1,6 @@ + true, + 'middleware' => [], // Static file Middleware +]; diff --git a/server/plugin/saiadmin/config/translation.php b/server/plugin/saiadmin/config/translation.php new file mode 100644 index 0000000..df4f0b3 --- /dev/null +++ b/server/plugin/saiadmin/config/translation.php @@ -0,0 +1,10 @@ + 'zh_CN', + // Fallback language + 'fallback_locale' => ['zh_CN', 'en'], + // Folder where language files are stored + 'path' => base_path() . "/plugin/saiadmin/resource/translations", +]; diff --git a/server/plugin/saiadmin/config/view.php b/server/plugin/saiadmin/config/view.php new file mode 100644 index 0000000..098e077 --- /dev/null +++ b/server/plugin/saiadmin/config/view.php @@ -0,0 +1,10 @@ + Twig::class +]; diff --git a/server/plugin/saiadmin/db/plugin.sql b/server/plugin/saiadmin/db/plugin.sql new file mode 100644 index 0000000..c860cfa --- /dev/null +++ b/server/plugin/saiadmin/db/plugin.sql @@ -0,0 +1,4 @@ +-- ---------------------------- +-- Records of sa_system_menu +-- ---------------------------- +INSERT INTO `sa_system_menu` SELECT NULL, 0, '插件市场', 'Plugin', '', 2, '/plugin', '/plugin/saipackage/install/index', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `code` = 'Plugin' AND `create_time` = '2026-01-01 00:00:00' AND ISNULL(`delete_time`)); diff --git a/server/plugin/saiadmin/db/saiadmin-6.0.sql b/server/plugin/saiadmin/db/saiadmin-6.0.sql new file mode 100644 index 0000000..92907fc --- /dev/null +++ b/server/plugin/saiadmin/db/saiadmin-6.0.sql @@ -0,0 +1,898 @@ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sa_system_attachment +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_attachment`; +CREATE TABLE `sa_system_attachment` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `category_id` int(11) NULL DEFAULT 0 COMMENT '文件分类', + `storage_mode` smallint(6) NULL DEFAULT 1 COMMENT '存储模式 (1 本地 2 阿里云 3 七牛云 4 腾讯云)', + `origin_name` varchar(255) NULL DEFAULT NULL COMMENT '原文件名', + `object_name` varchar(50) NULL DEFAULT NULL COMMENT '新文件名', + `hash` varchar(64) NULL DEFAULT NULL COMMENT '文件hash', + `mime_type` varchar(255) NULL DEFAULT NULL COMMENT '资源类型', + `storage_path` varchar(100) NULL DEFAULT NULL COMMENT '存储目录', + `suffix` varchar(10) NULL DEFAULT NULL COMMENT '文件后缀', + `size_byte` bigint(20) NULL DEFAULT NULL COMMENT '字节数', + `size_info` varchar(50) NULL DEFAULT NULL COMMENT '文件大小', + `url` varchar(255) NULL DEFAULT NULL COMMENT 'url地址', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `hash`(`hash`) USING BTREE, + INDEX `idx_url`(`url`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE, + INDEX `idx_category_id`(`category_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '附件信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_attachment +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_category +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_category`; +CREATE TABLE `sa_system_category` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '分类ID', + `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父id', + `level` varchar(255) NULL DEFAULT NULL COMMENT '组集关系', + `category_name` varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `pid`(`parent_id`) USING BTREE, + INDEX `sort`(`sort`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 COMMENT = '附件分类表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_category +-- ---------------------------- +INSERT INTO `sa_system_category` VALUES (1, 0, '0,', '全部分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (2, 1, '0,1,', '图片分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (3, 1, '0,1,', '文件分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (4, 1, '0,1,', '系统图片', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (5, 1, '0,1,', '其他分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_config +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_config`; +CREATE TABLE `sa_system_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `group_id` int(11) NULL DEFAULT NULL COMMENT '组id', + `key` varchar(32) NOT NULL COMMENT '配置键名', + `value` text NULL COMMENT '配置值', + `name` varchar(255) NULL DEFAULT NULL COMMENT '配置名称', + `input_type` varchar(32) NULL DEFAULT NULL COMMENT '数据输入类型', + `config_select_data` varchar(500) NULL DEFAULT NULL COMMENT '配置选项数据', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建人', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`, `key`) USING BTREE, + INDEX `group_id`(`group_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 302 COMMENT = '参数配置信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_config +-- ---------------------------- +INSERT INTO `sa_system_config` VALUES (1, 1, 'site_copyright', 'Copyright © 2024 saithink', '版权信息', 'textarea', NULL, 96, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (2, 1, 'site_desc', '基于vue3 + webman 的极速开发框架', '网站描述', 'textarea', NULL, 97, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (3, 1, 'site_keywords', '后台管理系统', '网站关键字', 'input', NULL, 98, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (4, 1, 'site_name', 'SaiAdmin', '网站名称', 'input', NULL, 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (5, 1, 'site_record_number', '9527', '网站备案号', 'input', NULL, 95, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (6, 2, 'upload_allow_file', 'txt,doc,docx,xls,xlsx,ppt,pptx,rar,zip,7z,gz,pdf,wps,md,jpg,png,jpeg,mp4,pem,crt', '文件类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (7, 2, 'upload_allow_image', 'jpg,jpeg,png,gif,svg,bmp', '图片类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (8, 2, 'upload_mode', '1', '上传模式', 'select', '[{\"label\":\"本地上传\",\"value\":\"1\"},{\"label\":\"阿里云OSS\",\"value\":\"2\"},{\"label\":\"七牛云\",\"value\":\"3\"},{\"label\":\"腾讯云COS\",\"value\":\"4\"},{\"label\":\"亚马逊S3\",\"value\":\"5\"}]', 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (10, 2, 'upload_size', '52428800', '上传大小', 'input', NULL, 88, '单位Byte,1MB=1024*1024Byte', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (11, 2, 'local_root', 'public/storage/', '本地存储路径', 'input', NULL, 0, '本地存储文件路径', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (12, 2, 'local_domain', 'http://127.0.0.1:8787', '本地存储域名', 'input', NULL, 0, 'http://127.0.0.1:8787', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (13, 2, 'local_uri', '/storage/', '本地访问路径', 'input', NULL, 0, '访问是通过domain + uri', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (14, 2, 'qiniu_accessKey', '', '七牛key', 'input', NULL, 0, '七牛云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (15, 2, 'qiniu_secretKey', '', '七牛secret', 'input', NULL, 0, '七牛云存储secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (16, 2, 'qiniu_bucket', '', '七牛bucket', 'input', NULL, 0, '七牛云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (17, 2, 'qiniu_dirname', '', '七牛dirname', 'input', NULL, 0, '七牛云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (18, 2, 'qiniu_domain', '', '七牛domain', 'input', NULL, 0, '七牛云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (19, 2, 'cos_secretId', '', '腾讯Id', 'input', NULL, 0, '腾讯云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (20, 2, 'cos_secretKey', '', '腾讯key', 'input', NULL, 0, '腾讯云secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (21, 2, 'cos_bucket', '', '腾讯bucket', 'input', NULL, 0, '腾讯云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (22, 2, 'cos_dirname', '', '腾讯dirname', 'input', NULL, 0, '腾讯云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (23, 2, 'cos_domain', '', '腾讯domain', 'input', NULL, 0, '腾讯云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (24, 2, 'cos_region', '', '腾讯region', 'input', NULL, 0, '腾讯云存储region', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (25, 2, 'oss_accessKeyId', '', '阿里Id', 'input', NULL, 0, '阿里云存储accessKeyId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (26, 2, 'oss_accessKeySecret', '', '阿里Secret', 'input', NULL, 0, '阿里云存储accessKeySecret', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (27, 2, 'oss_bucket', '', '阿里bucket', 'input', NULL, 0, '阿里云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (28, 2, 'oss_dirname', '', '阿里dirname', 'input', NULL, 0, '阿里云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (29, 2, 'oss_domain', '', '阿里domain', 'input', NULL, 0, '阿里云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (30, 2, 'oss_endpoint', '', '阿里endpoint', 'input', NULL, 0, '阿里云存储endpoint', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (31, 3, 'Host', 'smtp.qq.com', 'SMTP服务器', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (32, 3, 'Port', '465', 'SMTP端口', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (33, 3, 'Username', '', 'SMTP用户名', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (34, 3, 'Password', '', 'SMTP密码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (35, 3, 'SMTPSecure', 'ssl', 'SMTP验证方式', 'radio', '[\r\n {\"label\":\"ssl\",\"value\":\"ssl\"},\r\n {\"label\":\"tsl\",\"value\":\"tsl\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (36, 3, 'From', '', '默认发件人', 'input', '', 100, '默认发件的邮箱地址', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (37, 3, 'FromName', '账户注册', '默认发件名称', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (38, 3, 'CharSet', 'UTF-8', '编码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (39, 3, 'SMTPDebug', '0', '调试模式', 'radio', '[\r\n {\"label\":\"关闭\",\"value\":\"0\"},\r\n {\"label\":\"client\",\"value\":\"1\"},\r\n {\"label\":\"server\",\"value\":\"2\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (40, 2, 's3_key', '', 'key', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (41, 2, 's3_secret', '', 'secret', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (42, 2, 's3_bucket', '', 'bucket', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (43, 2, 's3_dirname', '', 'dirname', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (44, 2, 's3_domain', '', 'domain', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (45, 2, 's3_region', '', 'region', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (46, 2, 's3_version', '', 'version', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (47, 2, 's3_use_path_style_endpoint', '', 'path_style_endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (48, 2, 's3_endpoint', '', 'endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (49, 2, 's3_acl', '', 'acl', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_config_group +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_config_group`; +CREATE TABLE `sa_system_config_group` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建人', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 COMMENT = '参数配置分组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_config_group +-- ---------------------------- +INSERT INTO `sa_system_config_group` VALUES (1, '站点配置', 'site_config', '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config_group` VALUES (2, '上传配置', 'upload_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config_group` VALUES (3, '邮件服务', 'email_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dept`; +CREATE TABLE `sa_system_dept` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID,0为根节点', + `name` varchar(64) NOT NULL COMMENT '部门名称', + `code` varchar(64) NULL DEFAULT NULL COMMENT '部门编码', + `leader_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '部门负责人ID', + `level` varchar(255) NULL DEFAULT '' COMMENT '祖级列表,格式: 0,1,5, (便于查询子孙节点)', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_path`(`level`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1114 COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dept +-- ---------------------------- +INSERT INTO `sa_system_dept` VALUES (1, 0, '腾讯集团', 'GROUP', 1, '0,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (2, 1, '总办', 'GMO', NULL, '0,1,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (10, 1, '微信事业群', 'WXG', NULL, '0,1,', 200, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (11, 1, '互动娱乐事业群', 'IEG', NULL, '0,1,', 300, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (12, 1, '云与智慧产业事业群', 'CSIG', NULL, '0,1,', 400, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (101, 10, '微信基础产品部', 'WX_BASE', NULL, '0,1,10,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (102, 10, '微信支付线', 'WX_PAY', NULL, '0,1,10,', 200, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (111, 11, '天美工作室群', 'TIMI', NULL, '0,1,11,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (112, 11, '光子工作室群', 'LIGHT', NULL, '0,1,11,', 200, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (121, 12, '腾讯云事业部', 'CLOUD', NULL, '0,1,12,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (1111, 111, '王者荣耀项目组', 'HOK', NULL, '0,1,11,111,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dept` VALUES (1112, 111, 'QQ飞车项目组', 'QQ_SPEED', NULL, '0,1,11,111,', 200, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dict_data +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dict_data`; +CREATE TABLE `sa_system_dict_data` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `type_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '字典类型ID', + `label` varchar(50) NULL DEFAULT NULL COMMENT '字典标签', + `value` varchar(100) NULL DEFAULT NULL COMMENT '字典值', + `color` varchar(50) NULL DEFAULT NULL COMMENT '字典颜色', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `type_id`(`type_id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 50 COMMENT = '字典数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dict_data +-- ---------------------------- +INSERT INTO `sa_system_dict_data` VALUES (2, 2, '本地存储', '1', '#5d87ff', 'upload_mode', 99, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (3, 2, '阿里云OSS', '2', '#f9901f', 'upload_mode', 98, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (4, 2, '七牛云', '3', '#00ced1', 'upload_mode', 97, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (5, 2, '腾讯云COS', '4', '#1d84ff', 'upload_mode', 96, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (6, 2, '亚马逊S3', '5', '#ff80c8', 'upload_mode', 95, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (7, 3, '正常', '1', '#13deb9', 'data_status', 0, 1, '1为正常', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (8, 3, '停用', '2', '#ff4d4f', 'data_status', 0, 1, '2为停用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (9, 4, '统计页面', 'statistics', '#00ced1', 'dashboard', 100, 1, '管理员用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (10, 4, '工作台', 'work', '#ff8c00', 'dashboard', 50, 1, '员工使用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (11, 5, '男', '1', '#5d87ff', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (12, 5, '女', '2', '#ff4500', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (13, 5, '未知', '3', '#b48df3', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (16, 12, '图片', 'image', '#60c041', 'attachment_type', 10, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (17, 12, '文档', 'text', '#1d84ff', 'attachment_type', 9, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (18, 12, '音频', 'audio', '#00ced1', 'attachment_type', 8, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (19, 12, '视频', 'video', '#ff4500', 'attachment_type', 7, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (20, 12, '应用程序', 'application', '#ff8c00', 'attachment_type', 6, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (21, 13, '目录', '1', '#909399', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (22, 13, '菜单', '2', '#1e90ff', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (23, 13, '按钮', '3', '#ff4500', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (24, 13, '外链', '4', '#00ced1', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (25, 14, '是', '1', '#60c041', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (26, 14, '否', '2', '#ff4500', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (47, 20, 'URL任务GET', '1', '#5d87ff', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (48, 20, 'URL任务POST', '2', '#00ced1', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (49, 20, '类任务', '3', '#ff8c00', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dict_type`; +CREATE TABLE `sa_system_dict_type` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24 COMMENT = '字典类型表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dict_type +-- ---------------------------- +INSERT INTO `sa_system_dict_type` VALUES (2, '存储模式', 'upload_mode', 1, '上传文件存储模式', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (3, '数据状态', 'data_status', 1, '通用数据状态', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (4, '后台首页', 'dashboard', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (5, '性别', 'gender', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (12, '附件类型', 'attachment_type', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (13, '菜单类型', 'menu_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (14, '是否', 'yes_or_no', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (20, '定时任务类型', 'crontab_task_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_login_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_login_log`; +CREATE TABLE `sa_system_login_log` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` varchar(20) NULL DEFAULT NULL COMMENT '用户名', + `ip` varchar(45) NULL DEFAULT NULL COMMENT '登录IP地址', + `ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地', + `os` varchar(50) NULL DEFAULT NULL COMMENT '操作系统', + `browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器', + `status` smallint(6) NULL DEFAULT 1 COMMENT '登录状态 (1成功 2失败)', + `message` varchar(50) NULL DEFAULT NULL COMMENT '提示消息', + `login_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '登录时间', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `username`(`username`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE, + INDEX `idx_login_time`(`login_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '登录日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_login_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_mail +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_mail`; +CREATE TABLE `sa_system_mail` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `gateway` varchar(50) NULL DEFAULT NULL COMMENT '网关', + `from` varchar(50) NULL DEFAULT NULL COMMENT '发送人', + `email` varchar(50) NULL DEFAULT NULL COMMENT '接收人', + `code` varchar(20) NULL DEFAULT NULL COMMENT '验证码', + `content` varchar(500) NULL DEFAULT NULL COMMENT '邮箱内容', + `status` varchar(20) NULL DEFAULT NULL COMMENT '发送状态', + `response` varchar(500) NULL DEFAULT NULL COMMENT '返回结果', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '邮件记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_mail +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_menu`; +CREATE TABLE `sa_system_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID', + `name` varchar(64) NOT NULL COMMENT '菜单名称', + `code` varchar(64) NULL DEFAULT NULL COMMENT '组件名称', + `slug` varchar(100) NULL DEFAULT NULL COMMENT '权限标识,如 user:list, user:add', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型: 1目录, 2菜单, 3按钮/API', + `path` varchar(255) NULL DEFAULT NULL COMMENT '路由地址(前端)或API路径(后端)', + `component` varchar(255) NULL DEFAULT NULL COMMENT '前端组件路径,如 layout/User', + `method` varchar(10) NULL DEFAULT NULL COMMENT '请求方式', + `icon` varchar(64) NULL DEFAULT NULL COMMENT '图标', + `sort` int(11) NULL DEFAULT 100 COMMENT '排序', + `link_url` varchar(255) NULL DEFAULT NULL COMMENT '外部链接', + `is_iframe` tinyint(1) NULL DEFAULT 2 COMMENT '是否iframe', + `is_keep_alive` tinyint(1) NULL DEFAULT 2 COMMENT '是否缓存', + `is_hidden` tinyint(1) NULL DEFAULT 2 COMMENT '是否隐藏', + `is_fixed_tab` tinyint(1) NULL DEFAULT 2 COMMENT '是否固定标签页', + `is_full_page` tinyint(1) NULL DEFAULT 2 COMMENT '是否全屏', + `generate_id` int(11) NULL DEFAULT 0 COMMENT '生成id', + `generate_key` varchar(255) NULL DEFAULT NULL COMMENT '生成key', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(255) NULL DEFAULT NULL, + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_slug`(`slug`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1000 COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_menu +-- ---------------------------- +INSERT INTO `sa_system_menu` VALUES (1, 0, '仪表盘', 'Dashboard', NULL, 1, '/dashboard', NULL, NULL, 'ri:pie-chart-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (2, 1, '工作台', 'Console', NULL, 2, 'console', '/dashboard/console', NULL, 'ri:home-smile-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (3, 0, '系统管理', 'System', NULL, 1, '/system', NULL, NULL, 'ri:user-3-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (4, 3, '用户管理', 'User', NULL, 2, 'user', '/system/user', NULL, 'ri:user-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (5, 3, '部门管理', 'Dept', NULL, 2, 'dept', '/system/dept', NULL, 'ri:node-tree', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (6, 3, '角色管理', 'Role', NULL, 2, 'role', '/system/role', NULL, 'ri:admin-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (7, 3, '岗位管理', 'Post', '', 2, 'post', '/system/post', NULL, 'ri:signpost-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (8, 3, '菜单管理', 'Menu', NULL, 2, 'menu', '/system/menu', NULL, 'ri:menu-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (10, 0, '运维管理', 'Safeguard', NULL, 1, '/safeguard', '', NULL, 'ri:shield-check-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (11, 10, '缓存管理', 'Cache', '', 2, 'cache', '/safeguard/cache', NULL, 'ri:keyboard-box-line', 80, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (12, 10, '数据字典', 'Dict', NULL, 2, 'dict', '/safeguard/dict', NULL, 'ri:database-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (13, 10, '附件管理', 'Attachment', '', 2, 'attachment', '/safeguard/attachment', NULL, 'ri:file-cloud-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (14, 10, '数据表维护', 'Database', '', 2, 'database', '/safeguard/database', NULL, 'ri:database-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (15, 10, '登录日志', 'LoginLog', '', 2, 'login-log', '/safeguard/login-log', NULL, 'ri:login-circle-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (16, 10, '操作日志', 'OperLog', '', 2, 'oper-log', '/safeguard/oper-log', NULL, 'ri:shield-keyhole-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (17, 10, '邮件日志', 'EmailLog', '', 2, 'email-log', '/safeguard/email-log', NULL, 'ri:mail-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (18, 3, '系统设置', 'Config', NULL, 2, 'config', '/system/config', NULL, 'ri:settings-4-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (19, 0, '官方文档', 'Document', '', 4, '', '', NULL, 'ri:file-copy-2-fill', 90, 'https://saithink.top', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (20, 4, '数据列表', '', 'core:user:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (21, 1, '个人中心', 'UserCenter', '', 2, 'user-center', '/dashboard/user-center/index', NULL, 'ri:user-2-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (22, 4, '添加', '', 'core:user:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (23, 4, '修改', '', 'core:user:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (24, 4, '读取', '', 'core:user:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (25, 4, '删除', '', 'core:user:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (26, 4, '重置密码', '', 'core:user:password', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (27, 4, '清理缓存', '', 'core:user:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (28, 4, '设置工作台', '', 'core:user:home', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (29, 5, '数据列表', '', 'core:dept:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (30, 5, '添加', '', 'core:dept:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (31, 5, '修改', '', 'core:dept:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (32, 5, '读取', '', 'core:dept:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (33, 5, '删除', '', 'core:dept:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (34, 6, '添加', '', 'core:role:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (35, 6, '数据列表', '', 'core:role:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (36, 6, '修改', '', 'core:role:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (37, 6, '读取', '', 'core:role:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (38, 6, '删除', '', 'core:role:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (39, 6, '菜单权限', '', 'core:role:menu', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (41, 7, '数据列表', '', 'core:post:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (42, 7, '添加', '', 'core:post:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (43, 7, '修改', '', 'core:post:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (44, 7, '读取', '', 'core:post:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (45, 7, '删除', '', 'core:post:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (46, 7, '导入', '', 'core:post:import', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (47, 7, '导出', '', 'core:post:export', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (48, 8, '数据列表', '', 'core:menu:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (49, 8, '读取', '', 'core:menu:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (50, 8, '添加', '', 'core:menu:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (51, 8, '修改', '', 'core:menu:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (52, 8, '删除', '', 'core:menu:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (53, 18, '数据列表', '', 'core:config:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (54, 18, '管理', '', 'core:config:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (55, 18, '修改', '', 'core:config:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (56, 12, '数据列表', '', 'core:dict:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (57, 12, '管理', '', 'core:dict:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (58, 13, '数据列表', '', 'core:attachment:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (59, 13, '管理', '', 'core:attachment:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (60, 14, '数据表列表', '', 'core:database:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (61, 14, '数据表维护', '', 'core:database:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (62, 14, '回收站数据', '', 'core:recycle:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (63, 14, '回收站管理', '', 'core:recycle:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (64, 15, '数据列表', '', 'core:logs:login', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (65, 15, '删除', '', 'core:logs:deleteLogin', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (66, 16, '数据列表', '', 'core:logs:Oper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (67, 16, '删除', '', 'core:logs:deleteOper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (68, 17, '数据列表', '', 'core:email:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (69, 17, '删除', '', 'core:email:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (70, 10, '服务监控', 'Server', '', 2, 'server', '/safeguard/server', NULL, 'ri:server-line', 90, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (71, 70, '数据列表', '', 'core:server:monitor', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (72, 11, '数据列表', '', 'core:server:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (73, 11, '缓存清理', '', 'core:server:clear', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (74, 2, '登录数据统计', '', 'core:console:list', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (75, 0, '附加权限', 'Permission', '', 1, 'permission', '', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (76, 75, '上传图片', '', 'core:system:uploadImage', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (77, 75, '上传文件', '', 'core:system:uploadFile', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (78, 75, '附件列表', '', 'core:system:resource', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (79, 75, '用户列表', '', 'core:system:user', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (80, 0, '工具', 'Tool', '', 1, '/tool', '', NULL, 'ri:tools-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (81, 80, '代码生成', 'Code', '', 2, 'code', '/tool/code', NULL, 'ri:code-s-slash-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (82, 80, '定时任务', 'Crontab', '', 2, 'crontab', '/tool/crontab', NULL, 'ri:time-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (83, 82, '数据列表', '', 'tool:crontab:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (84, 82, '管理', '', 'tool:crontab:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (85, 82, '运行任务', '', 'tool:crontab:run', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (86, 81, '数据列表', '', 'tool:code:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (87, 81, '管理', '', 'tool:code:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (88, 0, '插件市场', 'Plugin', '', 2, '/plugin', '/plugin/saipackage/install/index', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_oper_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_oper_log`; +CREATE TABLE `sa_system_oper_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` varchar(20) NULL DEFAULT NULL COMMENT '用户名', + `app` varchar(50) NULL DEFAULT NULL COMMENT '应用名称', + `method` varchar(20) NULL DEFAULT NULL COMMENT '请求方式', + `router` varchar(500) NULL DEFAULT NULL COMMENT '请求路由', + `service_name` varchar(30) NULL DEFAULT NULL COMMENT '业务名称', + `ip` varchar(45) NULL DEFAULT NULL COMMENT '请求IP地址', + `ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地', + `request_data` text NULL COMMENT '请求数据', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `username`(`username`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_oper_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_post +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_post`; +CREATE TABLE `sa_system_post` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '岗位名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '岗位代码', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 87 COMMENT = '岗位信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_post +-- ---------------------------- +INSERT INTO `sa_system_post` VALUES (1, '司机岗', 'driver', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_post` VALUES (2, '保安岗', 'security', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_role +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role`; +CREATE TABLE `sa_system_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL COMMENT '角色名称', + `code` varchar(64) NOT NULL COMMENT '角色标识(英文唯一),如: hr_manager', + `level` int(11) NULL DEFAULT 1 COMMENT '角色级别(1-100):用于行政控制,不可操作级别>=自己的角色', + `data_scope` tinyint(4) NULL DEFAULT 1 COMMENT '数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `sort` int(11) NULL DEFAULT 100, + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_slug`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 COMMENT = '角色表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role +-- ---------------------------- +INSERT INTO `sa_system_role` VALUES (1, '超级管理员', 'super_admin', 100, 1, '系统维护者,拥有所有权限', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_role` VALUES (2, '集团总裁', 'ceo', 90, 1, '查看全集团数据', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_role` VALUES (3, 'BG总裁', 'bg_president', 80, 2, '', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_role` VALUES (4, '部门总经理', 'gm', 60, 2, '', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_role` VALUES (5, '组长', 'team_leader', 30, 3, '', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_role` VALUES (6, '普通员工', 'staff', 10, 4, '', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_role_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role_dept`; +CREATE TABLE `sa_system_role_dept` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) UNSIGNED NOT NULL, + `dept_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_dept_id`(`dept_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色-自定义数据权限关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role_dept +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role_menu`; +CREATE TABLE `sa_system_role_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) UNSIGNED NOT NULL, + `menu_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_menu_id`(`menu_id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色权限关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role_menu +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_user +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user`; +CREATE TABLE `sa_system_user` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(64) NOT NULL COMMENT '登录账号', + `password` varchar(255) NOT NULL COMMENT '加密密码', + `realname` varchar(64) NULL DEFAULT NULL COMMENT '真实姓名', + `gender` varchar(10) NULL DEFAULT NULL COMMENT '性别', + `avatar` varchar(255) NULL DEFAULT NULL COMMENT '头像', + `email` varchar(128) NULL DEFAULT NULL COMMENT '邮箱', + `phone` varchar(20) NULL DEFAULT NULL COMMENT '手机号', + `signed` varchar(255) NULL DEFAULT NULL COMMENT '个性签名', + `dashboard` varchar(255) NULL DEFAULT 'work' COMMENT '工作台', + `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '主归属部门', + `is_super` tinyint(1) NULL DEFAULT 0 COMMENT '是否超级管理员: 1是(跳过权限检查), 0否', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `login_time` timestamp(0) NULL DEFAULT NULL COMMENT '最后登录时间', + `login_ip` varchar(45) NULL DEFAULT NULL COMMENT '最后登录IP', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_username`(`username`) USING BTREE, + INDEX `idx_dept_id`(`dept_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 110 COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user +-- ---------------------------- +INSERT INTO `sa_system_user` VALUES (1, 'admin', '$2y$10$wnixh48uDnaW/6D9EygDd.OHJK0vQY/4nHaTjMKBCVDBP2NiTatqS', '祭道之上', '2', 'https://image.saithink.top/saiadmin/avatar.jpg', 'saiadmin@admin.com', '15888888888', 'SaiAdmin是兼具设计美学与高效开发的后台系统!', 'statistics', 1, 1, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (2, 'martin', '$2y$10$J3EkwRH8rNkveaanx1.j.ebRiBpnnVUGWa.i2MS3aNpb9ydAOolmm', '刘炽平', '2', 'https://image.saithink.top/saiadmin/avatar.jpg', 'martin@163.com', '15888888888', NULL, 'work', 1, 0, 1, '', NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (3, 'allen', '$2y$10$H8d7riOjOiwPSopguEQ1fuKZz.fA0A54OvuzTqgJlbG1N3uOxEwM.', '张小龙', '', 'https://image.saithink.top/saiadmin/avatar.jpg', '', '15888888888', NULL, 'work', 10, 0, 1, '', NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (4, 'mark', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '任宇昕', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', NULL, '15888888888', NULL, 'work', 11, 0, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (5, 'dowson', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '汤道生', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', NULL, '15888888888', NULL, 'work', 12, 0, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (10, 'timi_boss', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '姚晓光', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', '', '15888888888', NULL, 'work', 111, 0, 1, '', NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (100, 'dev_wang', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '王程序员', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', NULL, '15888888888', NULL, 'work', 1111, 0, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_user` VALUES (101, 'dev_li', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '李策划', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', NULL, '15888888888', NULL, 'work', 1111, 0, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_user_post +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user_post`; +CREATE TABLE `sa_system_user_post` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户主键', + `post_id` bigint(20) UNSIGNED NOT NULL COMMENT '岗位主键', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id`) USING BTREE, + INDEX `idx_post_id`(`post_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '用户与岗位关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user_post +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user_role`; +CREATE TABLE `sa_system_user_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) UNSIGNED NOT NULL, + `role_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_user_id`(`user_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 55 COMMENT = '用户角色关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user_role +-- ---------------------------- +INSERT INTO `sa_system_user_role` VALUES (1, 1, 1); + +-- ---------------------------- +-- Table structure for sa_tool_crontab +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_crontab`; +CREATE TABLE `sa_tool_crontab` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(100) NULL DEFAULT NULL COMMENT '任务名称', + `type` smallint(6) NULL DEFAULT 4 COMMENT '任务类型', + `target` varchar(500) NULL DEFAULT NULL COMMENT '调用任务字符串', + `parameter` varchar(1000) NULL DEFAULT NULL COMMENT '调用任务参数', + `task_style` tinyint(1) NULL DEFAULT NULL COMMENT '执行类型', + `rule` varchar(32) NULL DEFAULT NULL COMMENT '任务执行表达式', + `singleton` smallint(6) NULL DEFAULT 1 COMMENT '是否单次执行 (1 是 2 不是)', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 COMMENT = '定时任务信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_crontab +-- ---------------------------- +INSERT INTO `sa_tool_crontab` VALUES (1, '访问官网', 1, 'https://saithink.top', '', 1, '0 0 8 * * *', 2, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_tool_crontab` VALUES (2, '登录gitee', 2, 'https://gitee.com/check_user_login', '{\"user_login\": \"saiadmin\"}', 1, '0 0 10 * * *', 2, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_tool_crontab` VALUES (3, '定时执行任务', 3, '\\plugin\\saiadmin\\process\\Test', '{\"type\":\"1\"}', 5, '0 0 */12 * * *', 2, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_tool_crontab_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_crontab_log`; +CREATE TABLE `sa_tool_crontab_log` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `crontab_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '任务ID', + `name` varchar(255) NULL DEFAULT NULL COMMENT '任务名称', + `target` varchar(500) NULL DEFAULT NULL COMMENT '任务调用目标字符串', + `parameter` varchar(1000) NULL DEFAULT NULL COMMENT '任务调用参数', + `exception_info` varchar(2000) NULL DEFAULT NULL COMMENT '异常信息', + `status` smallint(6) NULL DEFAULT 1 COMMENT '执行状态 (1成功 2失败)', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务执行日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_crontab_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_tool_generate_columns +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_generate_columns`; +CREATE TABLE `sa_tool_generate_columns` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `table_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '所属表ID', + `column_name` varchar(200) NULL DEFAULT NULL COMMENT '字段名称', + `column_comment` varchar(255) NULL DEFAULT NULL COMMENT '字段注释', + `column_type` varchar(50) NULL DEFAULT NULL COMMENT '字段类型', + `default_value` varchar(50) NULL DEFAULT NULL COMMENT '默认值', + `is_pk` smallint(6) NULL DEFAULT 1 COMMENT '1 非主键 2 主键', + `is_required` smallint(6) NULL DEFAULT 1 COMMENT '1 非必填 2 必填', + `is_insert` smallint(6) NULL DEFAULT 1 COMMENT '1 非插入字段 2 插入字段', + `is_edit` smallint(6) NULL DEFAULT 1 COMMENT '1 非编辑字段 2 编辑字段', + `is_list` smallint(6) NULL DEFAULT 1 COMMENT '1 非列表显示字段 2 列表显示字段', + `is_query` smallint(6) NULL DEFAULT 1 COMMENT '1 非查询字段 2 查询字段', + `is_sort` smallint(6) NULL DEFAULT 1 COMMENT '1 非排序 2 排序', + `query_type` varchar(100) NULL DEFAULT 'eq' COMMENT '查询方式 eq 等于, neq 不等于, gt 大于, lt 小于, like 范围', + `view_type` varchar(100) NULL DEFAULT 'text' COMMENT '页面控件,text, textarea, password, select, checkbox, radio, date, upload, ma-upload(封装的上传控件)', + `dict_type` varchar(200) NULL DEFAULT NULL COMMENT '字典类型', + `allow_roles` varchar(255) NULL DEFAULT NULL COMMENT '允许查看该字段的角色', + `options` varchar(1000) NULL DEFAULT NULL COMMENT '字段其他设置', + `sort` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务字段表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_generate_columns +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_tool_generate_tables +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_generate_tables`; +CREATE TABLE `sa_tool_generate_tables` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `table_name` varchar(200) NULL DEFAULT NULL COMMENT '表名称', + `table_comment` varchar(500) NULL DEFAULT NULL COMMENT '表注释', + `stub` varchar(50) NULL DEFAULT NULL COMMENT 'stub类型', + `template` varchar(50) NULL DEFAULT NULL COMMENT '模板名称', + `namespace` varchar(255) NULL DEFAULT NULL COMMENT '命名空间', + `package_name` varchar(100) NULL DEFAULT NULL COMMENT '控制器包名', + `business_name` varchar(50) NULL DEFAULT NULL COMMENT '业务名称', + `class_name` varchar(50) NULL DEFAULT NULL COMMENT '类名称', + `menu_name` varchar(100) NULL DEFAULT NULL COMMENT '生成菜单名', + `belong_menu_id` int(11) NULL DEFAULT NULL COMMENT '所属菜单', + `tpl_category` varchar(100) NULL DEFAULT NULL COMMENT '生成类型,single 单表CRUD,tree 树表CRUD,parent_sub父子表CRUD', + `generate_type` smallint(6) NULL DEFAULT 1 COMMENT '1 压缩包下载 2 生成到模块', + `generate_path` varchar(100) NULL DEFAULT 'saiadmin-artd' COMMENT '前端根目录', + `generate_model` smallint(6) NULL DEFAULT 1 COMMENT '1 软删除 2 非软删除', + `generate_menus` varchar(255) NULL DEFAULT NULL COMMENT '生成菜单列表', + `build_menu` smallint(6) NULL DEFAULT 1 COMMENT '是否构建菜单', + `component_type` smallint(6) NULL DEFAULT 1 COMMENT '组件显示方式', + `options` varchar(1500) NULL DEFAULT NULL COMMENT '其他业务选项', + `form_width` int(11) NULL DEFAULT 800 COMMENT '表单宽度', + `is_full` tinyint(1) NULL DEFAULT 1 COMMENT '是否全屏', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `source` varchar(255) NULL DEFAULT NULL COMMENT '数据源', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_generate_tables +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_article +-- ---------------------------- +DROP TABLE IF EXISTS `sa_article`; +CREATE TABLE `sa_article` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '编号', + `category_id` int(10) NOT NULL COMMENT '分类id', + `title` varchar(255) NOT NULL DEFAULT '' COMMENT '文章标题', + `author` varchar(255) NULL DEFAULT NULL COMMENT '文章作者', + `image` varchar(1000) NULL DEFAULT '' COMMENT '文章图片', + `describe` varchar(1000) NOT NULL COMMENT '文章简介', + `content` text NOT NULL COMMENT '文章内容', + `views` int(11) NULL DEFAULT 0 COMMENT '浏览次数', + `sort` int(10) UNSIGNED NULL DEFAULT 100 COMMENT '排序', + `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态', + `is_link` tinyint(1) NULL DEFAULT 2 COMMENT '是否外链', + `link_url` varchar(255) NULL DEFAULT NULL COMMENT '链接地址', + `is_hot` tinyint(1) UNSIGNED NULL DEFAULT 2 COMMENT '是否热门', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_category_id`(`category_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 COMMENT = '文章表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_article +-- ---------------------------- +INSERT INTO `sa_article` VALUES (1, 1, '科技为农业强国建设插上腾飞之翼', '新华网', 'https://www.news.cn/tech/20251203/51066a5dc41545fa849d49423770ad70/2025120351066a5dc41545fa849d49423770ad70_202512037a03214ec26c4f029d6e1599d07c3779.png', '“十四五”规划提出,完善农业科技创新体系,创新农技推广服务方式,建设智慧农业。5年来,在科技创新的强劲支撑下,14亿人的饭碗端得更牢、农业现代化水平显著提升、产业新动能持续增强,农业强国建设迈上新台阶。', '

       “平均亩产1209.1公斤,这标志着全国首个两百万亩玉米‘吨粮田’成功创建。”金秋时节,新疆伊犁哈萨克自治州传来喜讯。这一纪录的诞生,离不开中国农业科学院研发的“玉米密植高产精准调控技术”支撑。依托该技术,位于伊犁的200余万亩玉米高产田亩保苗株数从传统的不足5000株提升到7000—8000株,玉米收获穗数大幅提升。

  这只是我国科技强农、粮食增产增收的一个缩影。“十四五”以来,我国粮食总产量始终保持在1.3万亿斤以上。2024年粮食总产量更是首次突破1.4万亿斤,比2020年增产740亿斤。

  习近平总书记强调,发展现代农业,建设农业强国,必须依靠科技进步,让科技为农业现代化插上腾飞的翅膀。

  “十四五”规划提出,完善农业科技创新体系,创新农技推广服务方式,建设智慧农业。5年来,在科技创新的强劲支撑下,14亿人的饭碗端得更牢、农业现代化水平显著提升、产业新动能持续增强,农业强国建设迈上新台阶。

  科技铸“芯”,夯实大国粮仓之基

  国以农为本,农以种为先,种子被誉为农业的“芯片”。前不久,四川省富顺县水稻百亩超高产攻关片进行实割实测,再生稻亩产达到494.81公斤,加上此前测产中稻亩产807.13公斤,合计亩产突破1300公斤。取得这一成绩的背后,是“甬优4949”等高产突破性品种的选育和“中稻+再生稻”生产模式的推广。

  水稻是我国第一大口粮。“十四五”时期全国多地选育出一批水稻突破性品种:安徽农业大学水稻栽培团队推广自育水稻品种,帮助当地农户水稻亩产增至800公斤;湖南杂交水稻研究中心选育出“西子3号”,推动解决部分受重金属污染地区“镉大米”问题;国家耐盐碱水稻技术创新中心培育出“箐两优3261”,填补了我国华南滨海盐碱区暂无强耐盐、多抗、优质杂交稻品种的空白……

  习近平总书记指出,中国人的饭碗要牢牢端在自己手中,就必须把种子牢牢攥在自己手里。

  作为我国另一大口粮,小麦育种的创新步伐也不断提速。2025年,西北农林科技大学一次性通过国家审定12个新品种,覆盖半冬性、冬性、春性类型,在抗倒伏等方面实现全面突破。这些为不同生态区“量身定制”的品种,在丰富我国小麦品种的同时,也大幅提升了小麦产能潜力。截至目前,西农小麦系列品种累计推广面积已达18亿亩,为保障国家粮食安全提供了坚实的种源支撑。

  “十四五”以来,我国深入实施种业振兴行动,育成了一批生产急需的重大品种,选育出优质高产水稻、节水抗病小麦、机收籽粒玉米、高油高产大豆等急需品种,农作物自主选育品种面积占比超过了95%,做到了“中国粮”主要用“中国种”。

  “去年全国粮食亩产394.7公斤,比‘十三五’末提高了12.5公斤,单产提升对我国粮食产量增长的贡献超过60%,有些年份会超过80%。”农业农村部党组书记、部长韩俊表示,“十四五”以来,农业农村部深入实施国家粮食安全战略,“以我为主、立足国内、确保产能、适度进口、科技支撑”,坚持产量产能、生产生态、增产增收一起抓,强化藏粮于地、藏粮于技,全方位夯实粮食安全根基。

  智慧提“效”,驱动耕作方式变革

  气象墒情传感器、智能虫情测报站等设备如同“千里眼”,与空中无人机巡航、地面机器狗巡检形成立体监测网络。这是日前科技日报记者在北京市昌平区的天汇园果园见到的一幕。

  “目前,该果园环境和土壤墒情覆盖10余项指标,虫情识别准确率达90%,种植生产信息化率超过95%,同时土壤成分快检技术能在30分钟内完成土壤成分‘体检’,辅助实现果园虫情和灾情等早预警、早干预。”北京市智慧农业创新团队岗位专家吴建伟介绍,该果园管理从“经验驱动”转向“数据驱动”,为果树生长提供了全天候守护。

  在四川省成都市新都区稻菜现代农业园区,当地自主研发的农业巡检机器人已代替人工开展巡检工作;在浙江省衢州市龙游县田间地头,一架植保无人机3小时就能完成300亩农田的喷药流程,相当于40多个人整整一天的工作量……

  “十四五”以来,类似的农业新场景新模式不断涌现,现代农业设施装备持续普及应用。我国先后支持建设国家智慧农业创新应用项目116个,深入开展国产化智慧农业技术的中试熟化、推广应用,探索形成了一批信息技术与农机农艺相融合的节本增产增效技术模式。

  习近平总书记指出,农业科技创新要着力提升创新体系整体效能,农业科技工作要突出应用导向,把论文写在大地上。

  5年来,我国农业科技创新体系整体效能显著提升。我国充分利用物联网、大数据、人工智能等现代信息技术发展智慧农业,并研制出一批先进智能适用的农机装备。

  “随着智能农机加快推广,全国安装北斗终端的农机约200万台,植保无人机年作业面积超过4.1亿亩。人工智能、农业机器人等新技术与农业生产经营加速融合,精准播种、变量施肥、智慧灌溉、精准饲喂、环境控制等逐渐普及。”农业农村部市场与信息化司司长雷刘功介绍。

  这些前沿技术的落地应用,正是农业科技现代化推动农业现代化的生动实践。“十四五”以来,我国坚持用现代设施装备武装农业,用现代科学技术服务农业,推动农业现代化水平不断提高。2024年底,农业科技进步贡献率已经达到了63.2%,农作物耕种收综合机械化率超过75%。

  创新延“链”,拓宽食物供给版图

  近日,蒙牛集团携多款产品参加第八届中国国际进口博览会,展示其发展新质生产力的最新成果。“我们打造的全球液态奶行业首座‘灯塔工厂’,已成为全球乳业最高人效比的新标杆,是中国乳业抢占全球智能制造新高地的生动写照。”中粮集团副总经理、蒙牛乳业董事长庆立军介绍。这座“灯塔工厂”通过实施30多项第四次工业革命技术,实现了“百人百亿”的极致人效比——100名员工,年产能达百万吨,创造产值百亿元。

  今天,科研创新已成为发展现代化海洋牧场的强大引擎。南方海洋实验室研发“珠海琴”等多功能融合的新型组合式结构加强型养殖平台,为海洋养殖带来新变革;珠海市海洋集团形成海工型养殖装备设计、建造、施工和运维等全产业链条,成功研发“格盛一号”养殖平台,订单水体总量相当于新开拓28.25万亩耕地。

  习近平总书记指出,要树立大农业观、大食物观,农林牧渔并举,构建多元化食物供给体系。

  “十四五”以来,我国突出科技支撑,强化要素保障,努力向森林要食物,向草原要食物,向江河湖海要食物,向设施农业要食物,向植物动物微生物要热量、要蛋白,多元化食物供给体系加快构建。

  一组数据表明,农业科技创新正通过看得见的方式,让老百姓的餐桌品类变得愈发丰富——2024年,我国肉蛋奶等畜产品总量达到1.75亿吨,比2020年增加2778万吨,增长18.8%;水产品总产量达到7358万吨,比2020年增长12.3%,水产品总产量持续36年居全球第一。

  党的二十届四中全会审议通过的《中共中央关于制定国民经济和社会发展第十五个五年规划的建议》提出,“统筹发展科技农业、绿色农业、质量农业、品牌农业,把农业建成现代化大产业”。科技创新能够催生新产业、新模式、新动能,是发展新质生产力的核心要素。韩俊表示,加快建设农业强国,必须清醒认识到农业科技国际竞争新形势,把农业科技创新放在更加突出的位置,紧盯世界农业科技前沿,加快突破农业关键核心技术,努力抢占农业科技创新制高点,塑造农业农村发展新动能新优势,培育壮大农业新质生产力。

', 5, 100, 1, 2, '', 2, 1, 1, '2024-06-02 22:55:25', '2026-01-10 11:13:25', NULL); +INSERT INTO `sa_article` VALUES (2, 1, '商业航天稳步快跑 “太空旅游”渐行渐近', '新华网', 'https://www.news.cn/tech/20251124/c7cb9d4e405c4c82b78a8f861889cb22/20251124c7cb9d4e405c4c82b78a8f861889cb22_20251124044f95bbab864da2b0c30861aa41279b.png', '业界普遍认为,以可复用火箭为代表的核心技术突破是商业航天提速的关键支撑。据统计,2025年底至2026年初,我国可复用火箭技术将进入密集首飞期,包括蓝箭航天“朱雀三号”、中科宇航“力箭二号”、星际荣耀“双曲线三号”和星河动力“智神星一号”在内的多款可复用火箭将迎来首飞。', '

       可搭载7名乘客穿越卡门线,体验约4分钟失重体验……记者从11月22日在京开幕的第四届中国空间科学大会上了解到我国太空旅游的最新进展。与会专家学者认为,随着产业链条不断完善、核心技术持续突破,我国商业航天已迈入稳步快跑的发展新阶段,曾经遥不可及的“太空旅游”正加速走进现实。

  记者在第四届中国空间科学大会同期举行的“航天新技术、新成果展”上看到,我国首型面向太空旅游的可重复使用飞行器力鸿二号的模型吸引了众多参观者。中科宇航展台工作人员告诉记者,力鸿二号将采用“箭船分离”的方式将乘客送上太空:飞到既定高度之后,载人舱与火箭分离,继续飞越100公里的卡门线,开始约4分钟的失重段,之后返回地面,以伞降的方式着陆,火箭也将垂直着陆回收。“我们的目标是让力鸿二号可重复使用超30次,这样就能把飞行成本降下来,让更多的人体验太空旅游。”

  我国商业航天的快速发展让太空旅游渐行渐近。业界普遍认为,以可复用火箭为代表的核心技术突破是商业航天提速的关键支撑。据统计,2025年底至2026年初,我国可复用火箭技术将进入密集首飞期,包括蓝箭航天“朱雀三号”、中科宇航“力箭二号”、星际荣耀“双曲线三号”和星河动力“智神星一号”在内的多款可复用火箭将迎来首飞。

  不仅火箭研制加速突破,卫星应用也在不断拓展。此次展会上,微纳星空等卫星企业也带来了最新的研发成果。微纳星空品牌总监刘晓光介绍,即将发射的“全天候卫士”MN200S-2(01B)星是公司自主研制的商业X波段相控阵雷达成像领域的技术标杆型卫星,可广泛应用于应急救灾、海洋维权、国土安全、生态监测、智慧城市建设等场景,并可实现多星高密度堆叠发射,为后续卫星规模化组网编队提供关键技术验证与工程实践依据。“随着国家低轨卫星互联网的能力建设牵引,微纳星空已经开启批量化、低成本的卫星制造。”

  业界认为,目前我国已形成覆盖火箭研制、卫星制造、发射服务、地面应用的完整商业航天产业链,产业集群效应逐步显现。在北京,“南箭北星”的产业格局已显露雏形:亦庄新城正在打造全国首个商业航天共性科研生产基地——火箭大街,海淀区作为“北星”的核心承载区,已集聚涵盖商业卫星制造、测运控、运营及数据应用的近200家相关企业。“在此基础上,海淀正全力推进卫星小镇‘两区一平台’的建设:先导区目前已有40余家商业航天企业聚集;紧邻航天城的卫星小镇核心区54万平方米空间预计2026年6月竣备,将重点引入卫星上下游企业;同时,卫星小镇拟建公共服务平台,提供卫星整星及组部件的力学、热真空、抗辐射等多种测试服务。”卫星小镇核心区对接人段叶叶介绍。

  “我国发展商业航天的优势是人多、力量大、竞争强,技术和产品能够快速迭代,紧跟国际趋势。”中国科学院微小卫星创新研究院副院长张永合在接受记者专访时表示,但目前我国商业航天企业和人才大多集中在制造领域,“还需要更多能创造任务的人,有非常前沿的想法,有改变当前航天模式的颠覆性路径。”

  张永合认为,商业航天关键是要创造需求,“比如太空旅游就是商业航天创造的需求,将人们日常生活中的旅游延伸到太空中去,在产业上就属于增量。”未来,低空经济、空间互联网等也将打开想象空间。“有了坚实的技术底座,新的产业形态就会自然而然生长出来。”

  不过,业内专家也指出,我国商业航天发展仍面临体制机制创新不足、部分核心技术有待突破等挑战。从政策层面来看,近年来国家持续加大对商业航天的支持力度,相关扶持政策和行业规范正在逐步完善,旨在优化市场环境、加大核心技术研发支持,为商业航天高质量发展营造良好生态,推动太空旅游等新业态逐步走向成熟。

  业内普遍认为,商业航天已成为航天强国建设的重要增长点。从运载火箭重复使用技术突破到卫星应用场景拓展,随着技术持续成熟、产业链不断完善和政策环境优化,未来“上太空”有望从专业探索逐步走向大众体验,中国商业航天也将在全球太空经济格局中占据重要地位。


', 1, 100, 1, 2, '', 2, 1, 1, '2024-06-02 22:56:47', '2026-01-10 11:13:47', NULL); +INSERT INTO `sa_article` VALUES (3, 2, '以数字经济为引擎加快推进中国式现代化', '新华网', 'https://www.news.cn/tech/20251023/0cb8f0bcb7874992b8d431abdd7331a9/202510230cb8f0bcb7874992b8d431abdd7331a9_2025102332abb363b12744eb9f725ce395f16e4a.png', 'The Athletic报道,阿森纳理疗师乔丹-里斯即将加盟曼联,成为红魔的首席理疗师。曼联首席理疗师罗宾-萨德勒已于今年一月离开俱乐部', '

       随着中国式现代化不断向前推进,中国迎来了数字经济发展的新机遇。在数字经济快速发展的背景下,中国式现代化的内涵得以拓展,现代化动力得以重塑,现代化新动能得以培育,现代化新优势得以形成。数字技术创新、实体经济与数字经济融合、产业数字化、数字产业化成为推进中国式现代化的重要驱动力量。

  在数字经济推动下,现代化由工业经济时代的现代化向数字经济时代的现代化转变,在这一大背景下需要在理论上研究数字经济赋能中国式现代化的逻辑和机制,需要深入探讨中国式现代化如何紧紧抓住数字经济发展带来的新机遇,以数字化的知识和信息作为关键生产要素,以数字技术为核心驱动力,在数据要素和数字技术的双轮驱动下推动中国式现代化走上新征程。

  南京大学数字经济与管理学院任保平教授的专著《数字经济赋能中国式现代化》于2025年在江苏人民出版社出版,全书共17章,35.8万字。该书立足世界范围内数字化浪潮下的经济现代化背景,从理论与实践两个方面研究了数字经济发展对中国式现代化的赋能作用。

  在理论层面,该书研究了数字经济发展对中国式现代化的影响、数字经济与中国式现代化的有机衔接,数字经济背景下中国式现代化目标的重塑、数字经济与中国式现代化深度融合的逻辑机制,数字经济背景下中国式现代化的延伸和拓展。在实践层面,从中国式现代化的不同方面具体研究了数字经济的赋能作用,具体包括数字经济赋能中国式新型工业化、新型城镇化、科技现代化、农业农村现代化、产业现代化和科技现代化。

  该书的核心观点主要有以下方面。一是,中国式现代化战略在数字化转型背景下发生的一系列拓展。促进工业化与信息化的融合发展,以数字化带动工业化发展,加大数字技术研发力度,大力发展数字产业。以数字化带动农业现代化,补足中国式现代化短板。协同匹配数字经济时代的创新供求,提升产业技术创新能力。促进企业数字化转型,引领数字经济发展。协调产业数字化与数字产业化,推进产业基础现代化。加快新型基础设施建设,提升基础设施支撑能力。构建数字平台体系,打造现代化经济新形态。

  二是,以数字经济发展培育中国式现代化新优势。针对数字经济带来的现代化新变化,研究了数字经济对中国式现代化的引擎作用,认为目前中国式现代化正处于数字经济蓬勃发展带来无数新机遇的时代,我们要抓住数字经济发展带来的新机遇,以数字经济推动中国式现代化的新发展。

  三是,阐释数字经济赋能中国式现代化的逻辑。在理论上深刻阐释数字经济如何成为中国式现代化的新引擎,数字经济作为新引擎对中国式现代化赋能的驱动机制和路径,论证数字经济发展赋能中国式现代化在目标、路径和战略上的延伸和拓展,为数字经济赋能中国式现代化提供了一个理论框架。

  四是,研究数字经济全面赋能中国式现代化的机制。中国式经济现代化涉及多方面内容,包括科技现代化、工业现代化、农业现代化、服务业现代化、产业链现代化、城市现代化、区域现代化、城市现代化、生态现代化、企业现代化、人的现代化和治理现代化,数字经济应该从上述方面赋能中国式现代化。

  五是,提出了以数字经济培育中国式现代化新优势的路径。数字经济培育中国式现代化的新优势包括需求端的动力新优势、供给端的效率新优势等。需要从数字化转型的创新能力、基础设施的供给能力、数字化转型的战略支撑能力,数字化转型的保障能力等方面研究数字经济发展培育中国式现代化新优势的实现路径。而且,需要从效率变革机制、动力变革机制和质量变革机制等方面研究数字经济赋能中国式现代化新优势培育的机制,从数字产业化、产业数字化、产学研协同创新、劳动力质量和相关配套制度等方面实现数字经济培育中国式现代化的新优势,全面展示数字经济赋能中国式现代化中的应用场景。

', 2, 100, 1, 2, '', 2, 1, 1, '2024-06-02 22:58:41', '2026-01-10 11:13:01', NULL); +INSERT INTO `sa_article` VALUES (4, 2, '2025腾讯全球数字生态大会在深圳举行', '新华网', 'https://www.news.cn/tech/20250918/a8a0f6e1a6d740188db7752e247518bb/20250918a8a0f6e1a6d740188db7752e247518bb_202509184f78f2904fa2456db9537d878cb89166.jpg', '5月26日晚上18:00,中超第14轮,深圳新鹏城主场迎战上海申花,上半场马莱莱补射斩获赛季第6球,半场战罢,申花暂1-0新鹏城', '


\n\n

       9月16日,2025腾讯全球数字生态大会在深圳举行,会上公布多项AI技术和产品最新进展,并宣布全面开放腾讯AI落地能力及优势场景,助力“好用的AI”在千行百业中加速落地。


', 3, 100, 1, 2, '', 2, 1, 1, '2024-06-02 22:59:41', '2026-01-10 13:42:34', NULL); +INSERT INTO `sa_article` VALUES (5, 3, '秀我中国丨中国小机器人“勇闯”美国CES', '新华网', 'https://www.news.cn/tech/20260109/b2c43e2b0d1e43a98840c33e37fbbc73/20260109896bd0b56c18435987243f0f5dc01d67_202601099d0953f9999949a9b55e9d212d7bf773.jpg', '2026年美国拉斯维加斯消费电子展(CES)6日至9日举行,首次亮相海外展会的中国小机器人“启元Q1”刚一登场就成为焦点,凭借其出色表现“圈粉”海外。', '


\n\n

       2026年美国拉斯维加斯消费电子展(CES)6日至9日举行,首次亮相海外展会的中国小机器人“启元Q1”刚一登场就成为焦点,凭借其出色表现“圈粉”海外。

', 3, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:01:17', '2026-01-10 13:42:24', NULL); +INSERT INTO `sa_article` VALUES (6, 3, 'AI助力药物虚拟筛选提速百万倍 开启后AlphaFold时代创新药', '新华网', 'https://www.news.cn/tech/20260109/2e0f65d6733a4e2588a97dfe96593a09/202601092e0f65d6733a4e2588a97dfe96593a09_202601090012b088f5604e22a77ae70f8656f466.jpg', '团队与清华大学闫创业教授团队合作,在去甲肾上腺素转运体(NET)的临床相关靶点上开展了系列生物实验验证。', '

       1月9日,清华大学智能产业研究院(AIR)联合清华大学生命学院、清华大学化学系在《科学》杂志发表论文《深度对比学习实现基因组级别药物虚拟筛选》。该论文研发了一个AI驱动的超高通量药物虚拟筛选平台DrugCLIP, 筛选速度对比传统方法实现百万倍提升,同时在预测准确率上也取得显著突破。依托该平台,团队打通了从AlphaFold结构预测到药物发现的关键通道,首次完成了覆盖人类基因组规模的药物虚拟筛选,为后AlphaFold时代的创新药物发现带来新可能性。

\"\"

   长期以来,药物研发面临“高风险、高投入、低成功率”的难题,在靶点发现与先导化合物筛选阶段,受限于传统工具的计算能力,绝大多数潜在靶点和化合物仍未被充分探索。如何在广阔的生物与化学空间中精准高效地发现活性化合物,是当前创新药物研发面临的核心挑战。

  据了解,为突破虚拟筛选规模瓶颈,DrugCLIP创新性地构建了蛋白口袋与小分子的“向量化结合空间”,将传统基于物理对接的筛选流程转化为高效的向量检索问题。该模型结合对比学习、3D结构预训练与多模态编码技术,能在三维结构层面精准建模蛋白-配体间的相互作用。训练后的高潜力分子将自然聚集于目标蛋白口袋的向量邻域,能够有效支撑快速的大规模虚拟筛选。依托这一机制,DrugCLIP在128核CPU+8张GPU的计算节点上,能实现毫秒级打分与万亿级日吞吐能力,筛选100万个候选分子仅需0.02秒,日处理能力达31万亿次,对比传统方法实现了百万倍提升。

\"\"

   团队与清华大学闫创业教授团队合作,在去甲肾上腺素转运体(NET)的临床相关靶点上开展了系列生物实验验证。团队使用DrugCLIP模型从160万个候选分子中筛选出约100个高评分分子,同位素配体转运实验检测显示,其中15%为有效抑制剂,其中12个分子结合能力优于现有抗抑郁药物安非他酮。相关复合物结构已通过冷冻电镜解析,进一步验证了DrugCLIP筛选结果的生物学可信度。

  值得关注的是,DrugCLIP支持对AlphaFold预测的蛋白结构和apo(无配体)状态下的蛋白口袋进行筛选,扩大了其在真实药物发现场景中的适用性。团队和清华大学刘磊教授团队合作,针对E3泛素连接酶TRIP12(thyroid hormone receptor interactor 12)进行了虚拟筛选与实验验证。过往研究发现,TRIP12是多种肿瘤、帕金森综合征的潜在靶点,但是TRIP12缺少已知的小分子配体和复合物结构。团队使用DrugCLIP模型,从160万个候选分子中高通量筛选出约50个高评分分子,SPR实验证实,其中10个分子与TRIP12有结合能力,两个亲和力较高的分子也对TRIP12的泛素连接酶活性有一定的抑制活性。

  此外,依托DrugCLIP,团队首次完成了人类基因组规模的虚拟筛选项目,覆盖约1万个蛋白靶点、2万个结合口袋,分析超过5亿个小分子,富集出200万余个高潜力活性分子,构建了目前已知最大规模的蛋白-配体筛选数据库。该数据库已面向全球科研社区开放,为基础研究与早期药物发现提供了强大数据支持。

  DrugCLIP平台现已免费开放,用户无需本地部署,通过网页上传蛋白结构即可启动筛选任务。平台集成口袋/分子编码、向量检索、可视化与结果分析等功能,支持多种分子库调用与自定义上传,广泛适用于科研机构与企业用户。

  未来,DrugCLIP将与科研产业生态合作伙伴深度合作,在抗癌、传染病、罕见病等方向加速新靶点与First-in-class药物的发现。团队将持续优化引擎性能、拓展支持模态,助力构建一个更智能、高效与普惠的全球药物创新生态。

', 4, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:02:40', '2026-01-10 13:38:51', NULL); +INSERT INTO `sa_article` VALUES (7, 4, '高度重视低空经济为哪般', '新华网', 'https://www.news.cn/tech/20250312/c0453593a495424780c5424c054a1d4d/20250312c0453593a495424780c5424c054a1d4d_2025031215d8945b560d4d169997f7745d0ef56f.jpg', '当前,我国低空经济正处于市场培育初期,关键技术的实用性和商业价值仅得到初步验证,但已彰显出广阔的增长空间', '

       近年来,低空经济成为全球发达经济体角逐的重要方向。虽然世界范围内低空经济还处于培育初期阶段,但是美国、日本、欧盟等国家和地区已经重点围绕场景开发应用、交通管理能力、运行技术验证、系统标准体系等方面积极出台和完善相关政策,加快发展低空经济。

  低空经济是依托低空飞行活动牵引串联的一系列相互关联的产业经济活动,不仅包括上游生产制造飞行器所必需的材料、零部件及分系统的行业企业,还包括中下游低空飞行器组装集成制造和测试试飞、设施配套及低空服务等领域。低空经济产业链条长、产业关联性强、应用场景丰富,具有战略引领性、高增长潜力等显著特征,既可以推动现代农牧业、先进制造业、现代服务业深度融合发展,又能够扩大有效投资、提振消费需求、提升创新能力。世界主要国家高度重视低空经济发展,就是因为看好其发展前景。

  当前,我国低空经济正处于市场培育初期,关键技术的实用性和商业价值仅得到初步验证,但已彰显出广阔的增长空间。未来随着技术迭代升级和商业模式逐步成熟,低空经济的高增长潜力将会进一步释放,更容易实现相关产业企业的群体性爆发成长,有望成为拉动经济增长的新引擎。

  一方面,低空飞行器的产业规模体量加快增长、产业生态持续完善。目前,我国无人机制造国际竞争力逐步增强,消费级无人机世界领先优势突出。截至2023年底,我国民用无人机研制企业已超过2300家,量产的无人机产品超过1000款。2023年,我国民用无人机产业规模达到1174.3亿元,同比增长32%。同时,新一代信息技术、新材料、新能源加速与航空科学技术融合发展,推动低空飞行器动力装备及系统、传感器、飞控系统等相关技术加速迭代,绿色高效、安全低噪的飞行器设计、制造与验证技术也持续更新升级。

  另一方面,体量巨大、类型多样的应用场景持续涌现,牵引低空服务快速释放动能。运营航空器大幅增加,《2023—2024中国民用无人驾驶航空发展报告》显示,截至2024年8月底,我国无人机实名登记数达198.7万架,比2023年底增加72万架;共颁发无人机驾驶员执照22万本,比2023年底增加13.9%。随着影视航拍、航空运动、空中观光游览等低空文旅应用场景快速发展,低空经济能为满足人民群众美好生活需求提供新供给。2023年,横店“航空+影视+旅游”交旅融合案例入选第一批交通运输与旅游融合发展十佳案例;2024年,敦煌“飞天”通用航空项目等航空旅游产品案例入选第二批交通运输与旅游融合发展示范案例。低空旅游市场潜力开始显现。

  同时,低空经济在农业植保、现代物流等行业领域的发展应用不断深入。随着无人机应用技术不断成熟和应用场景持续丰富,“农林牧副渔”多场景作业不断拓展,农业无人机服务市场规模呈蓬勃发展态势。2024年,全国植保无人机的保有量达到25.1万架,作业面积更是高达26.7亿亩次,同比增长近25%。从全球看,上世纪80年代以来,美国农业植保无人机作业渗透率超过50%,日本60%的稻田采用无人机进行植保作业。相较而言,我国农业无人机作业渗透率还比较低,有很大发展空间。在低空物流领域,以无人机为载运工具的无人化配送成为优化城市物流的重要方向,这能有效解决传统物流配送模式面临的劳动力成本、运输成本大幅攀升以及物资配送流通效率低下等诸多问题。在“低空+”领域,低空经济赋能社会治理成效突出,促进巡检、应急救援、城市管理、森林防火、医疗救护等公共服务快速发展。实践中,北京延庆、湖北武汉等地已采用电力线路无人机智能巡检,有效降低了巡检成本,提升了巡检效率。

  但也要看到,我国低空经济发展还存在一些问题,如统筹发展和安全有短板、产业融合化发展不足、空域管理协同机制尚不健全、基础设施建设相对滞后等。对此,要从突出集群融合、强化科技创新、加强设施建设等方面综合施策,将低空经济的发展潜力充分释放出来。

  一是突出集群融合,加快培育壮大低空经济产业集群,以市场需求为牵引、以科技创新为驱动,积极完善产业生态、谋划应用场景,推进低空制造业集群化发展。二是强化科技创新,聚焦低空经济创新链薄弱环节,加大科技创新投入,加快提升低空技术支撑能力。三是加强设施建设,构建低空经济基础设施综合保障体系,坚持绿色发展、节约集约,统筹推进通用机场、电动垂直起降飞行器起降场、固定运营基地、飞行服务站等地面配套基础设施建设,推进低空飞行通信、导航、气象监测等信息基础设施建设,加速低空经济智联网络设施建设。此外,还要统筹发展和安全,加强低空飞行器监控防护,强化低空安全技术攻关,提升空域精细化管理能力。坚持包容审慎的安全风险管控理念,建设监管服务体系,建立灵活调配、动态高效的低空空域管理使用机制,增强管理的协同性与联动性。

', 11, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:04:23', '2026-01-10 13:43:44', NULL); +INSERT INTO `sa_article` VALUES (8, 4, '国家发改委成立低空经济发展司', '新华网', 'https://www.news.cn/tech/20241231/3f5396024a9749ee863292c04c7119dc/202412313f5396024a9749ee863292c04c7119dc_2024123101c42d384b83467f835ffd286af095d4.jpg', '近日,低空经济发展司召开推动低空基础设施建设座谈会和推动低空智能网联系统建设专题座谈会', '

      记者从国家发展和改革委员会官方网站获悉,低空经济发展司已正式成立。

  低空经济发展司的具体职责是拟订并组织实施低空经济发展战略、中长期发展规划,提出有关政策建议,协调有关重大问题等。

  近日,低空经济发展司召开推动低空基础设施建设座谈会和推动低空智能网联系统建设专题座谈会。

  在推动低空基础设施建设座谈会上,低空经济发展司负责同志同自然资源部、生态环境部等部委和有关中央企业进行座谈,了解相关领域低空经济典型场景应用和相关基础设施建设发展情况,并就推动低空基础设施有序规划建设进行交流。

  在推动低空智能网联系统建设专题座谈会上,低空经济发展司负责同志与通信、导航方面有关专家进行座谈,就低空智能网联系统建设进行交流。

', 6, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:04:23', '2026-01-10 13:42:32', NULL); + +-- ---------------------------- +-- Table structure for sa_article_banner +-- ---------------------------- +DROP TABLE IF EXISTS `sa_article_banner`; +CREATE TABLE `sa_article_banner` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', + `banner_type` int(11) NULL DEFAULT NULL COMMENT '类型', + `image` varchar(1000) NULL DEFAULT NULL COMMENT '图片地址', + `is_href` tinyint(1) NULL DEFAULT 1 COMMENT '是否链接', + `url` varchar(255) NULL DEFAULT NULL COMMENT '链接地址', + `title` varchar(255) NULL DEFAULT NULL COMMENT '标题', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '描述', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 COMMENT = '文章轮播图' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_article_banner +-- ---------------------------- +INSERT INTO `sa_article_banner` VALUES (1, 1, 'https://picsum.photos/id/490/640/360', 1, '/blog/1', '探索亚洲的烹饪奇迹', 1, 100, '有一系列名为“新加坡传统烹饪”的食谱,探索了新加坡的美食和文化。它包括新加坡华人、马来人、印度人、欧亚人和土生华人(海峡华人)的美食', 1, 1, '2024-06-02 23:06:37', '2026-01-09 21:51:50', NULL); +INSERT INTO `sa_article_banner` VALUES (2, 1, 'https://picsum.photos/id/29/640/360', 1, '/blog/2', '探索雄伟的山峰', 1, 100, '攀登这座风景如画的山峰的最佳方式是乘坐御在所索道,乘坐15 分钟即可将游客带入空中,欣赏周围一览无余的景观', 1, 1, '2024-06-02 23:06:49', '2026-01-09 21:51:54', NULL); +INSERT INTO `sa_article_banner` VALUES (3, 1, 'https://picsum.photos/id/903/640/360', 1, '/blog/3', '揭秘奇迹', 1, 100, '极光是地球磁场与太阳风相互作用的产物,当太阳风中的带电粒子与地球高层大气中的原子、分子碰撞时,会产生发光现象,形成美丽的极光', 1, 1, '2024-06-02 23:06:56', '2026-01-09 21:53:32', NULL); + +-- ---------------------------- +-- Table structure for sa_article_category +-- ---------------------------- +DROP TABLE IF EXISTS `sa_article_category`; +CREATE TABLE `sa_article_category` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父级ID', + `category_name` varchar(255) NOT NULL COMMENT '分类标题', + `describe` varchar(255) NULL DEFAULT NULL COMMENT '分类简介', + `image` varchar(255) NULL DEFAULT NULL COMMENT '分类图片', + `sort` int(10) UNSIGNED NULL DEFAULT 100 COMMENT '排序', + `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 COMMENT = '文章分类表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_article_category +-- ---------------------------- +INSERT INTO `sa_article_category` VALUES (1, 0, '大国科技', '', NULL, 100, 1, 1, 1, '2024-06-02 22:50:51', '2026-01-06 18:03:07', NULL); +INSERT INTO `sa_article_category` VALUES (2, 0, '数字经济', '', NULL, 100, 1, 1, 1, '2024-06-02 22:50:56', '2026-01-09 16:54:05', NULL); +INSERT INTO `sa_article_category` VALUES (3, 0, '科技快讯', '', NULL, 100, 1, 1, 1, '2024-06-02 22:51:01', '2026-01-07 01:03:37', NULL); +INSERT INTO `sa_article_category` VALUES (4, 0, '低空经济', '', NULL, 100, 1, 1, 1, '2024-06-02 22:51:16', '2026-01-06 18:03:14', NULL); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/plugin/saiadmin/db/saiadmin-pure.sql b/server/plugin/saiadmin/db/saiadmin-pure.sql new file mode 100644 index 0000000..20f1ad5 --- /dev/null +++ b/server/plugin/saiadmin/db/saiadmin-pure.sql @@ -0,0 +1,766 @@ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sa_system_attachment +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_attachment`; +CREATE TABLE `sa_system_attachment` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `category_id` int(11) NULL DEFAULT 0 COMMENT '文件分类', + `storage_mode` smallint(6) NULL DEFAULT 1 COMMENT '存储模式 (1 本地 2 阿里云 3 七牛云 4 腾讯云)', + `origin_name` varchar(255) NULL DEFAULT NULL COMMENT '原文件名', + `object_name` varchar(50) NULL DEFAULT NULL COMMENT '新文件名', + `hash` varchar(64) NULL DEFAULT NULL COMMENT '文件hash', + `mime_type` varchar(255) NULL DEFAULT NULL COMMENT '资源类型', + `storage_path` varchar(100) NULL DEFAULT NULL COMMENT '存储目录', + `suffix` varchar(10) NULL DEFAULT NULL COMMENT '文件后缀', + `size_byte` bigint(20) NULL DEFAULT NULL COMMENT '字节数', + `size_info` varchar(50) NULL DEFAULT NULL COMMENT '文件大小', + `url` varchar(255) NULL DEFAULT NULL COMMENT 'url地址', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `hash`(`hash`) USING BTREE, + INDEX `idx_url`(`url`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE, + INDEX `idx_category_id`(`category_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '附件信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_attachment +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_category +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_category`; +CREATE TABLE `sa_system_category` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '分类ID', + `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父id', + `level` varchar(255) NULL DEFAULT NULL COMMENT '组集关系', + `category_name` varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `pid`(`parent_id`) USING BTREE, + INDEX `sort`(`sort`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 COMMENT = '附件分类表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_category +-- ---------------------------- +INSERT INTO `sa_system_category` VALUES (1, 0, '0,', '全部分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (2, 1, '0,1,', '图片分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (3, 1, '0,1,', '文件分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (4, 1, '0,1,', '系统图片', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (5, 1, '0,1,', '其他分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_config +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_config`; +CREATE TABLE `sa_system_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `group_id` int(11) NULL DEFAULT NULL COMMENT '组id', + `key` varchar(32) NOT NULL COMMENT '配置键名', + `value` text NULL COMMENT '配置值', + `name` varchar(255) NULL DEFAULT NULL COMMENT '配置名称', + `input_type` varchar(32) NULL DEFAULT NULL COMMENT '数据输入类型', + `config_select_data` varchar(500) NULL DEFAULT NULL COMMENT '配置选项数据', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建人', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`, `key`) USING BTREE, + INDEX `group_id`(`group_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 302 COMMENT = '参数配置信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_config +-- ---------------------------- +INSERT INTO `sa_system_config` VALUES (1, 1, 'site_copyright', 'Copyright © 2024 saithink', '版权信息', 'textarea', NULL, 96, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (2, 1, 'site_desc', '基于vue3 + webman 的极速开发框架', '网站描述', 'textarea', NULL, 97, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (3, 1, 'site_keywords', '后台管理系统', '网站关键字', 'input', NULL, 98, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (4, 1, 'site_name', 'SaiAdmin', '网站名称', 'input', NULL, 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (5, 1, 'site_record_number', '9527', '网站备案号', 'input', NULL, 95, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (6, 2, 'upload_allow_file', 'txt,doc,docx,xls,xlsx,ppt,pptx,rar,zip,7z,gz,pdf,wps,md,jpg,png,jpeg,mp4,pem,crt', '文件类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (7, 2, 'upload_allow_image', 'jpg,jpeg,png,gif,svg,bmp', '图片类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (8, 2, 'upload_mode', '1', '上传模式', 'select', '[{\"label\":\"本地上传\",\"value\":\"1\"},{\"label\":\"阿里云OSS\",\"value\":\"2\"},{\"label\":\"七牛云\",\"value\":\"3\"},{\"label\":\"腾讯云COS\",\"value\":\"4\"},{\"label\":\"亚马逊S3\",\"value\":\"5\"}]', 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (10, 2, 'upload_size', '52428800', '上传大小', 'input', NULL, 88, '单位Byte,1MB=1024*1024Byte', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (11, 2, 'local_root', 'public/storage/', '本地存储路径', 'input', NULL, 0, '本地存储文件路径', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (12, 2, 'local_domain', 'http://127.0.0.1:8787', '本地存储域名', 'input', NULL, 0, 'http://127.0.0.1:8787', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (13, 2, 'local_uri', '/storage/', '本地访问路径', 'input', NULL, 0, '访问是通过domain + uri', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (14, 2, 'qiniu_accessKey', '', '七牛key', 'input', NULL, 0, '七牛云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (15, 2, 'qiniu_secretKey', '', '七牛secret', 'input', NULL, 0, '七牛云存储secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (16, 2, 'qiniu_bucket', '', '七牛bucket', 'input', NULL, 0, '七牛云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (17, 2, 'qiniu_dirname', '', '七牛dirname', 'input', NULL, 0, '七牛云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (18, 2, 'qiniu_domain', '', '七牛domain', 'input', NULL, 0, '七牛云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (19, 2, 'cos_secretId', '', '腾讯Id', 'input', NULL, 0, '腾讯云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (20, 2, 'cos_secretKey', '', '腾讯key', 'input', NULL, 0, '腾讯云secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (21, 2, 'cos_bucket', '', '腾讯bucket', 'input', NULL, 0, '腾讯云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (22, 2, 'cos_dirname', '', '腾讯dirname', 'input', NULL, 0, '腾讯云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (23, 2, 'cos_domain', '', '腾讯domain', 'input', NULL, 0, '腾讯云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (24, 2, 'cos_region', '', '腾讯region', 'input', NULL, 0, '腾讯云存储region', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (25, 2, 'oss_accessKeyId', '', '阿里Id', 'input', NULL, 0, '阿里云存储accessKeyId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (26, 2, 'oss_accessKeySecret', '', '阿里Secret', 'input', NULL, 0, '阿里云存储accessKeySecret', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (27, 2, 'oss_bucket', '', '阿里bucket', 'input', NULL, 0, '阿里云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (28, 2, 'oss_dirname', '', '阿里dirname', 'input', NULL, 0, '阿里云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (29, 2, 'oss_domain', '', '阿里domain', 'input', NULL, 0, '阿里云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (30, 2, 'oss_endpoint', '', '阿里endpoint', 'input', NULL, 0, '阿里云存储endpoint', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (31, 3, 'Host', 'smtp.qq.com', 'SMTP服务器', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (32, 3, 'Port', '465', 'SMTP端口', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (33, 3, 'Username', '', 'SMTP用户名', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (34, 3, 'Password', '', 'SMTP密码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (35, 3, 'SMTPSecure', 'ssl', 'SMTP验证方式', 'radio', '[\r\n {\"label\":\"ssl\",\"value\":\"ssl\"},\r\n {\"label\":\"tsl\",\"value\":\"tsl\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (36, 3, 'From', '', '默认发件人', 'input', '', 100, '默认发件的邮箱地址', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (37, 3, 'FromName', '账户注册', '默认发件名称', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (38, 3, 'CharSet', 'UTF-8', '编码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (39, 3, 'SMTPDebug', '0', '调试模式', 'radio', '[\r\n {\"label\":\"关闭\",\"value\":\"0\"},\r\n {\"label\":\"client\",\"value\":\"1\"},\r\n {\"label\":\"server\",\"value\":\"2\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (40, 2, 's3_key', '', 'key', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (41, 2, 's3_secret', '', 'secret', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (42, 2, 's3_bucket', '', 'bucket', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (43, 2, 's3_dirname', '', 'dirname', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (44, 2, 's3_domain', '', 'domain', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (45, 2, 's3_region', '', 'region', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (46, 2, 's3_version', '', 'version', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (47, 2, 's3_use_path_style_endpoint', '', 'path_style_endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (48, 2, 's3_endpoint', '', 'endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (49, 2, 's3_acl', '', 'acl', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_config_group +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_config_group`; +CREATE TABLE `sa_system_config_group` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建人', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 COMMENT = '参数配置分组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_config_group +-- ---------------------------- +INSERT INTO `sa_system_config_group` VALUES (1, '站点配置', 'site_config', '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config_group` VALUES (2, '上传配置', 'upload_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config_group` VALUES (3, '邮件服务', 'email_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dept`; +CREATE TABLE `sa_system_dept` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID,0为根节点', + `name` varchar(64) NOT NULL COMMENT '部门名称', + `code` varchar(64) NULL DEFAULT NULL COMMENT '部门编码', + `leader_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '部门负责人ID', + `level` varchar(255) NULL DEFAULT '' COMMENT '祖级列表,格式: 0,1,5, (便于查询子孙节点)', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_path`(`level`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1114 COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dept +-- ---------------------------- +INSERT INTO `sa_system_dept` VALUES (1, 0, '腾讯集团', 'GROUP', 1, '0,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dict_data +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dict_data`; +CREATE TABLE `sa_system_dict_data` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `type_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '字典类型ID', + `label` varchar(50) NULL DEFAULT NULL COMMENT '字典标签', + `value` varchar(100) NULL DEFAULT NULL COMMENT '字典值', + `color` varchar(50) NULL DEFAULT NULL COMMENT '字典颜色', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `type_id`(`type_id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 50 COMMENT = '字典数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dict_data +-- ---------------------------- +INSERT INTO `sa_system_dict_data` VALUES (2, 2, '本地存储', '1', '#5d87ff', 'upload_mode', 99, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (3, 2, '阿里云OSS', '2', '#f9901f', 'upload_mode', 98, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (4, 2, '七牛云', '3', '#00ced1', 'upload_mode', 97, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (5, 2, '腾讯云COS', '4', '#1d84ff', 'upload_mode', 96, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (6, 2, '亚马逊S3', '5', '#ff80c8', 'upload_mode', 95, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (7, 3, '正常', '1', '#13deb9', 'data_status', 0, 1, '1为正常', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (8, 3, '停用', '2', '#ff4d4f', 'data_status', 0, 1, '2为停用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (9, 4, '统计页面', 'statistics', '#00ced1', 'dashboard', 100, 1, '管理员用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (10, 4, '工作台', 'work', '#ff8c00', 'dashboard', 50, 1, '员工使用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (11, 5, '男', '1', '#5d87ff', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (12, 5, '女', '2', '#ff4500', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (13, 5, '未知', '3', '#b48df3', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (16, 12, '图片', 'image', '#60c041', 'attachment_type', 10, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (17, 12, '文档', 'text', '#1d84ff', 'attachment_type', 9, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (18, 12, '音频', 'audio', '#00ced1', 'attachment_type', 8, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (19, 12, '视频', 'video', '#ff4500', 'attachment_type', 7, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (20, 12, '应用程序', 'application', '#ff8c00', 'attachment_type', 6, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (21, 13, '目录', '1', '#909399', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (22, 13, '菜单', '2', '#1e90ff', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (23, 13, '按钮', '3', '#ff4500', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (24, 13, '外链', '4', '#00ced1', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (25, 14, '是', '1', '#60c041', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (26, 14, '否', '2', '#ff4500', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (47, 20, 'URL任务GET', '1', '#5d87ff', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (48, 20, 'URL任务POST', '2', '#00ced1', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (49, 20, '类任务', '3', '#ff8c00', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dict_type`; +CREATE TABLE `sa_system_dict_type` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24 COMMENT = '字典类型表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dict_type +-- ---------------------------- +INSERT INTO `sa_system_dict_type` VALUES (2, '存储模式', 'upload_mode', 1, '上传文件存储模式', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (3, '数据状态', 'data_status', 1, '通用数据状态', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (4, '后台首页', 'dashboard', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (5, '性别', 'gender', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (12, '附件类型', 'attachment_type', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (13, '菜单类型', 'menu_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (14, '是否', 'yes_or_no', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (20, '定时任务类型', 'crontab_task_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_login_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_login_log`; +CREATE TABLE `sa_system_login_log` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` varchar(20) NULL DEFAULT NULL COMMENT '用户名', + `ip` varchar(45) NULL DEFAULT NULL COMMENT '登录IP地址', + `ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地', + `os` varchar(50) NULL DEFAULT NULL COMMENT '操作系统', + `browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器', + `status` smallint(6) NULL DEFAULT 1 COMMENT '登录状态 (1成功 2失败)', + `message` varchar(50) NULL DEFAULT NULL COMMENT '提示消息', + `login_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '登录时间', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `username`(`username`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE, + INDEX `idx_login_time`(`login_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '登录日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_login_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_mail +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_mail`; +CREATE TABLE `sa_system_mail` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `gateway` varchar(50) NULL DEFAULT NULL COMMENT '网关', + `from` varchar(50) NULL DEFAULT NULL COMMENT '发送人', + `email` varchar(50) NULL DEFAULT NULL COMMENT '接收人', + `code` varchar(20) NULL DEFAULT NULL COMMENT '验证码', + `content` varchar(500) NULL DEFAULT NULL COMMENT '邮箱内容', + `status` varchar(20) NULL DEFAULT NULL COMMENT '发送状态', + `response` varchar(500) NULL DEFAULT NULL COMMENT '返回结果', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '邮件记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_mail +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_menu`; +CREATE TABLE `sa_system_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID', + `name` varchar(64) NOT NULL COMMENT '菜单名称', + `code` varchar(64) NULL DEFAULT NULL COMMENT '组件名称', + `slug` varchar(100) NULL DEFAULT NULL COMMENT '权限标识,如 user:list, user:add', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型: 1目录, 2菜单, 3按钮/API', + `path` varchar(255) NULL DEFAULT NULL COMMENT '路由地址(前端)或API路径(后端)', + `component` varchar(255) NULL DEFAULT NULL COMMENT '前端组件路径,如 layout/User', + `method` varchar(10) NULL DEFAULT NULL COMMENT '请求方式', + `icon` varchar(64) NULL DEFAULT NULL COMMENT '图标', + `sort` int(11) NULL DEFAULT 100 COMMENT '排序', + `link_url` varchar(255) NULL DEFAULT NULL COMMENT '外部链接', + `is_iframe` tinyint(1) NULL DEFAULT 2 COMMENT '是否iframe', + `is_keep_alive` tinyint(1) NULL DEFAULT 2 COMMENT '是否缓存', + `is_hidden` tinyint(1) NULL DEFAULT 2 COMMENT '是否隐藏', + `is_fixed_tab` tinyint(1) NULL DEFAULT 2 COMMENT '是否固定标签页', + `is_full_page` tinyint(1) NULL DEFAULT 2 COMMENT '是否全屏', + `generate_id` int(11) NULL DEFAULT 0 COMMENT '生成id', + `generate_key` varchar(255) NULL DEFAULT NULL COMMENT '生成key', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(255) NULL DEFAULT NULL, + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_slug`(`slug`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1000 COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_menu +-- ---------------------------- +INSERT INTO `sa_system_menu` VALUES (1, 0, '仪表盘', 'Dashboard', NULL, 1, '/dashboard', NULL, NULL, 'ri:pie-chart-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (2, 1, '工作台', 'Console', NULL, 2, 'console', '/dashboard/console', NULL, 'ri:home-smile-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (3, 0, '系统管理', 'System', NULL, 1, '/system', NULL, NULL, 'ri:user-3-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (4, 3, '用户管理', 'User', NULL, 2, 'user', '/system/user', NULL, 'ri:user-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (5, 3, '部门管理', 'Dept', NULL, 2, 'dept', '/system/dept', NULL, 'ri:node-tree', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (6, 3, '角色管理', 'Role', NULL, 2, 'role', '/system/role', NULL, 'ri:admin-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (7, 3, '岗位管理', 'Post', '', 2, 'post', '/system/post', NULL, 'ri:signpost-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (8, 3, '菜单管理', 'Menu', NULL, 2, 'menu', '/system/menu', NULL, 'ri:menu-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (10, 0, '运维管理', 'Safeguard', NULL, 1, '/safeguard', '', NULL, 'ri:shield-check-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (11, 10, '缓存管理', 'Cache', '', 2, 'cache', '/safeguard/cache', NULL, 'ri:keyboard-box-line', 80, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (12, 10, '数据字典', 'Dict', NULL, 2, 'dict', '/safeguard/dict', NULL, 'ri:database-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (13, 10, '附件管理', 'Attachment', '', 2, 'attachment', '/safeguard/attachment', NULL, 'ri:file-cloud-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (14, 10, '数据表维护', 'Database', '', 2, 'database', '/safeguard/database', NULL, 'ri:database-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (15, 10, '登录日志', 'LoginLog', '', 2, 'login-log', '/safeguard/login-log', NULL, 'ri:login-circle-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (16, 10, '操作日志', 'OperLog', '', 2, 'oper-log', '/safeguard/oper-log', NULL, 'ri:shield-keyhole-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (17, 10, '邮件日志', 'EmailLog', '', 2, 'email-log', '/safeguard/email-log', NULL, 'ri:mail-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (18, 3, '系统设置', 'Config', NULL, 2, 'config', '/system/config', NULL, 'ri:settings-4-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (19, 0, '官方文档', 'Document', '', 4, '', '', NULL, 'ri:file-copy-2-fill', 90, 'https://saithink.top', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (20, 4, '数据列表', '', 'core:user:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (21, 1, '个人中心', 'UserCenter', '', 2, 'user-center', '/dashboard/user-center/index', NULL, 'ri:user-2-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (22, 4, '添加', '', 'core:user:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (23, 4, '修改', '', 'core:user:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (24, 4, '读取', '', 'core:user:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (25, 4, '删除', '', 'core:user:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (26, 4, '重置密码', '', 'core:user:password', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (27, 4, '清理缓存', '', 'core:user:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (28, 4, '设置工作台', '', 'core:user:home', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (29, 5, '数据列表', '', 'core:dept:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (30, 5, '添加', '', 'core:dept:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (31, 5, '修改', '', 'core:dept:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (32, 5, '读取', '', 'core:dept:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (33, 5, '删除', '', 'core:dept:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (34, 6, '添加', '', 'core:role:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (35, 6, '数据列表', '', 'core:role:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (36, 6, '修改', '', 'core:role:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (37, 6, '读取', '', 'core:role:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (38, 6, '删除', '', 'core:role:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (39, 6, '菜单权限', '', 'core:role:menu', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (41, 7, '数据列表', '', 'core:post:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (42, 7, '添加', '', 'core:post:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (43, 7, '修改', '', 'core:post:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (44, 7, '读取', '', 'core:post:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (45, 7, '删除', '', 'core:post:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (46, 7, '导入', '', 'core:post:import', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (47, 7, '导出', '', 'core:post:export', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (48, 8, '数据列表', '', 'core:menu:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (49, 8, '读取', '', 'core:menu:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (50, 8, '添加', '', 'core:menu:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (51, 8, '修改', '', 'core:menu:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (52, 8, '删除', '', 'core:menu:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (53, 18, '数据列表', '', 'core:config:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (54, 18, '管理', '', 'core:config:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (55, 18, '修改', '', 'core:config:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (56, 12, '数据列表', '', 'core:dict:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (57, 12, '管理', '', 'core:dict:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (58, 13, '数据列表', '', 'core:attachment:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (59, 13, '管理', '', 'core:attachment:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (60, 14, '数据表列表', '', 'core:database:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (61, 14, '数据表维护', '', 'core:database:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (62, 14, '回收站数据', '', 'core:recycle:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (63, 14, '回收站管理', '', 'core:recycle:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (64, 15, '数据列表', '', 'core:logs:login', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (65, 15, '删除', '', 'core:logs:deleteLogin', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (66, 16, '数据列表', '', 'core:logs:Oper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (67, 16, '删除', '', 'core:logs:deleteOper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (68, 17, '数据列表', '', 'core:email:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (69, 17, '删除', '', 'core:email:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (70, 10, '服务监控', 'Server', '', 2, 'server', '/safeguard/server', NULL, 'ri:server-line', 90, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (71, 70, '数据列表', '', 'core:server:monitor', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (72, 11, '数据列表', '', 'core:server:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (73, 11, '缓存清理', '', 'core:server:clear', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (74, 2, '登录数据统计', '', 'core:console:list', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (75, 0, '附加权限', 'Permission', '', 1, 'permission', '', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (76, 75, '上传图片', '', 'core:system:uploadImage', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (77, 75, '上传文件', '', 'core:system:uploadFile', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (78, 75, '附件列表', '', 'core:system:resource', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (79, 75, '用户列表', '', 'core:system:user', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (80, 0, '工具', 'Tool', '', 1, '/tool', '', NULL, 'ri:tools-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (81, 80, '代码生成', 'Code', '', 2, 'code', '/tool/code', NULL, 'ri:code-s-slash-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (82, 80, '定时任务', 'Crontab', '', 2, 'crontab', '/tool/crontab', NULL, 'ri:time-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (83, 82, '数据列表', '', 'tool:crontab:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (84, 82, '管理', '', 'tool:crontab:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (85, 82, '运行任务', '', 'tool:crontab:run', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (86, 81, '数据列表', '', 'tool:code:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (87, 81, '管理', '', 'tool:code:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (88, 0, '插件市场', 'Plugin', '', 2, '/plugin', '/plugin/saipackage/install/index', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_oper_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_oper_log`; +CREATE TABLE `sa_system_oper_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` varchar(20) NULL DEFAULT NULL COMMENT '用户名', + `app` varchar(50) NULL DEFAULT NULL COMMENT '应用名称', + `method` varchar(20) NULL DEFAULT NULL COMMENT '请求方式', + `router` varchar(500) NULL DEFAULT NULL COMMENT '请求路由', + `service_name` varchar(30) NULL DEFAULT NULL COMMENT '业务名称', + `ip` varchar(45) NULL DEFAULT NULL COMMENT '请求IP地址', + `ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地', + `request_data` text NULL COMMENT '请求数据', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `username`(`username`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_oper_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_post +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_post`; +CREATE TABLE `sa_system_post` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '岗位名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '岗位代码', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 87 COMMENT = '岗位信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for sa_system_role +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role`; +CREATE TABLE `sa_system_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL COMMENT '角色名称', + `code` varchar(64) NOT NULL COMMENT '角色标识(英文唯一),如: hr_manager', + `level` int(11) NULL DEFAULT 1 COMMENT '角色级别(1-100):用于行政控制,不可操作级别>=自己的角色', + `data_scope` tinyint(4) NULL DEFAULT 1 COMMENT '数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `sort` int(11) NULL DEFAULT 100, + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_slug`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 COMMENT = '角色表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role +-- ---------------------------- +INSERT INTO `sa_system_role` VALUES (1, '超级管理员', 'super_admin', 100, 1, '系统维护者,拥有所有权限', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_role_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role_dept`; +CREATE TABLE `sa_system_role_dept` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) UNSIGNED NOT NULL, + `dept_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_dept_id`(`dept_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色-自定义数据权限关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role_dept +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role_menu`; +CREATE TABLE `sa_system_role_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) UNSIGNED NOT NULL, + `menu_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_menu_id`(`menu_id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色权限关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role_menu +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_user +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user`; +CREATE TABLE `sa_system_user` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(64) NOT NULL COMMENT '登录账号', + `password` varchar(255) NOT NULL COMMENT '加密密码', + `realname` varchar(64) NULL DEFAULT NULL COMMENT '真实姓名', + `gender` varchar(10) NULL DEFAULT NULL COMMENT '性别', + `avatar` varchar(255) NULL DEFAULT NULL COMMENT '头像', + `email` varchar(128) NULL DEFAULT NULL COMMENT '邮箱', + `phone` varchar(20) NULL DEFAULT NULL COMMENT '手机号', + `signed` varchar(255) NULL DEFAULT NULL COMMENT '个性签名', + `dashboard` varchar(255) NULL DEFAULT 'work' COMMENT '工作台', + `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '主归属部门', + `is_super` tinyint(1) NULL DEFAULT 0 COMMENT '是否超级管理员: 1是(跳过权限检查), 0否', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `login_time` timestamp(0) NULL DEFAULT NULL COMMENT '最后登录时间', + `login_ip` varchar(45) NULL DEFAULT NULL COMMENT '最后登录IP', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_username`(`username`) USING BTREE, + INDEX `idx_dept_id`(`dept_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 110 COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user +-- ---------------------------- +INSERT INTO `sa_system_user` VALUES (1, 'admin', '$2y$10$wnixh48uDnaW/6D9EygDd.OHJK0vQY/4nHaTjMKBCVDBP2NiTatqS', '祭道之上', '2', 'https://image.saithink.top/saiadmin/avatar.jpg', 'saiadmin@admin.com', '15888888888', 'SaiAdmin是兼具设计美学与高效开发的后台系统!', 'statistics', 1, 1, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_user_post +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user_post`; +CREATE TABLE `sa_system_user_post` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户主键', + `post_id` bigint(20) UNSIGNED NOT NULL COMMENT '岗位主键', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id`) USING BTREE, + INDEX `idx_post_id`(`post_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '用户与岗位关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user_post +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user_role`; +CREATE TABLE `sa_system_user_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) UNSIGNED NOT NULL, + `role_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_user_id`(`user_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 55 COMMENT = '用户角色关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user_role +-- ---------------------------- +INSERT INTO `sa_system_user_role` VALUES (1, 1, 1); + +-- ---------------------------- +-- Table structure for sa_tool_crontab +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_crontab`; +CREATE TABLE `sa_tool_crontab` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(100) NULL DEFAULT NULL COMMENT '任务名称', + `type` smallint(6) NULL DEFAULT 4 COMMENT '任务类型', + `target` varchar(500) NULL DEFAULT NULL COMMENT '调用任务字符串', + `parameter` varchar(1000) NULL DEFAULT NULL COMMENT '调用任务参数', + `task_style` tinyint(1) NULL DEFAULT NULL COMMENT '执行类型', + `rule` varchar(32) NULL DEFAULT NULL COMMENT '任务执行表达式', + `singleton` smallint(6) NULL DEFAULT 1 COMMENT '是否单次执行 (1 是 2 不是)', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for sa_tool_crontab_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_crontab_log`; +CREATE TABLE `sa_tool_crontab_log` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `crontab_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '任务ID', + `name` varchar(255) NULL DEFAULT NULL COMMENT '任务名称', + `target` varchar(500) NULL DEFAULT NULL COMMENT '任务调用目标字符串', + `parameter` varchar(1000) NULL DEFAULT NULL COMMENT '任务调用参数', + `exception_info` varchar(2000) NULL DEFAULT NULL COMMENT '异常信息', + `status` smallint(6) NULL DEFAULT 1 COMMENT '执行状态 (1成功 2失败)', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务执行日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_crontab_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_tool_generate_columns +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_generate_columns`; +CREATE TABLE `sa_tool_generate_columns` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `table_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '所属表ID', + `column_name` varchar(200) NULL DEFAULT NULL COMMENT '字段名称', + `column_comment` varchar(255) NULL DEFAULT NULL COMMENT '字段注释', + `column_type` varchar(50) NULL DEFAULT NULL COMMENT '字段类型', + `default_value` varchar(50) NULL DEFAULT NULL COMMENT '默认值', + `is_pk` smallint(6) NULL DEFAULT 1 COMMENT '1 非主键 2 主键', + `is_required` smallint(6) NULL DEFAULT 1 COMMENT '1 非必填 2 必填', + `is_insert` smallint(6) NULL DEFAULT 1 COMMENT '1 非插入字段 2 插入字段', + `is_edit` smallint(6) NULL DEFAULT 1 COMMENT '1 非编辑字段 2 编辑字段', + `is_list` smallint(6) NULL DEFAULT 1 COMMENT '1 非列表显示字段 2 列表显示字段', + `is_query` smallint(6) NULL DEFAULT 1 COMMENT '1 非查询字段 2 查询字段', + `is_sort` smallint(6) NULL DEFAULT 1 COMMENT '1 非排序 2 排序', + `query_type` varchar(100) NULL DEFAULT 'eq' COMMENT '查询方式 eq 等于, neq 不等于, gt 大于, lt 小于, like 范围', + `view_type` varchar(100) NULL DEFAULT 'text' COMMENT '页面控件,text, textarea, password, select, checkbox, radio, date, upload, ma-upload(封装的上传控件)', + `dict_type` varchar(200) NULL DEFAULT NULL COMMENT '字典类型', + `allow_roles` varchar(255) NULL DEFAULT NULL COMMENT '允许查看该字段的角色', + `options` varchar(1000) NULL DEFAULT NULL COMMENT '字段其他设置', + `sort` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务字段表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_generate_columns +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_tool_generate_tables +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_generate_tables`; +CREATE TABLE `sa_tool_generate_tables` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `table_name` varchar(200) NULL DEFAULT NULL COMMENT '表名称', + `table_comment` varchar(500) NULL DEFAULT NULL COMMENT '表注释', + `stub` varchar(50) NULL DEFAULT NULL COMMENT 'stub类型', + `template` varchar(50) NULL DEFAULT NULL COMMENT '模板名称', + `namespace` varchar(255) NULL DEFAULT NULL COMMENT '命名空间', + `package_name` varchar(100) NULL DEFAULT NULL COMMENT '控制器包名', + `business_name` varchar(50) NULL DEFAULT NULL COMMENT '业务名称', + `class_name` varchar(50) NULL DEFAULT NULL COMMENT '类名称', + `menu_name` varchar(100) NULL DEFAULT NULL COMMENT '生成菜单名', + `belong_menu_id` int(11) NULL DEFAULT NULL COMMENT '所属菜单', + `tpl_category` varchar(100) NULL DEFAULT NULL COMMENT '生成类型,single 单表CRUD,tree 树表CRUD,parent_sub父子表CRUD', + `generate_type` smallint(6) NULL DEFAULT 1 COMMENT '1 压缩包下载 2 生成到模块', + `generate_path` varchar(100) NULL DEFAULT 'saiadmin-artd' COMMENT '前端根目录', + `generate_model` smallint(6) NULL DEFAULT 1 COMMENT '1 软删除 2 非软删除', + `generate_menus` varchar(255) NULL DEFAULT NULL COMMENT '生成菜单列表', + `build_menu` smallint(6) NULL DEFAULT 1 COMMENT '是否构建菜单', + `component_type` smallint(6) NULL DEFAULT 1 COMMENT '组件显示方式', + `options` varchar(1500) NULL DEFAULT NULL COMMENT '其他业务选项', + `form_width` int(11) NULL DEFAULT 800 COMMENT '表单宽度', + `is_full` tinyint(1) NULL DEFAULT 1 COMMENT '是否全屏', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `source` varchar(255) NULL DEFAULT NULL COMMENT '数据源', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_generate_tables +-- ---------------------------- + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/plugin/saiadmin/exception/ApiException.php b/server/plugin/saiadmin/exception/ApiException.php new file mode 100644 index 0000000..a9040e8 --- /dev/null +++ b/server/plugin/saiadmin/exception/ApiException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\exception; + +use Webman\Http\Request; +use Webman\Http\Response; +use support\exception\BusinessException; + +/** + * 常规操作异常-只返回json数据,不记录异常日志 + */ +class ApiException extends BusinessException +{ + public function render(Request $request): ?Response + { + return json(['code' => $this->getCode() ?: 500, 'message' => $this->getMessage()]); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/exception/SystemException.php b/server/plugin/saiadmin/exception/SystemException.php new file mode 100644 index 0000000..cd9a81f --- /dev/null +++ b/server/plugin/saiadmin/exception/SystemException.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\exception; + +use Throwable; + +/** + * 系统接口错误-返回json数据,并且记录异常日志 + */ +class SystemException extends \RuntimeException +{ + public function __construct($message, $code = 400, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/process/Task.php b/server/plugin/saiadmin/process/Task.php new file mode 100644 index 0000000..dc31a59 --- /dev/null +++ b/server/plugin/saiadmin/process/Task.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\process; + +use plugin\saiadmin\app\logic\tool\CrontabLogic; +use Webman\Channel\Client; +use Workerman\Crontab\Crontab; + +class Task +{ + protected $logic; //login对象 + public $crontabIds = []; //定时任务表主键id => Crontab对象id + + public function __construct() + { + $dbName = env('DB_NAME'); + if (!empty($dbName)) { + $this->logic = new CrontabLogic(); + // 连接webman channel服务 + Client::connect(); + // 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调 + Client::on('crontab', function ($data) { + $this->reload($data); + }); + } + } + public function onWorkerStart() + { + $dbName = env('DB_NAME'); + if (!empty($dbName)) { + $this->initStart(); + } + } + + public function initStart() + { + $logic = new CrontabLogic(); + $taskList = $logic->getAll($logic->search(['status' => 1])); + foreach ($taskList as $item) { + $crontab = new Crontab($item['rule'], function () use ($item) { + $this->logic->run($item['id']); + }); + $this->crontabIds[intval($item['id'])] = $crontab->getId(); //存储定时任务表主键id => Crontab对象id + echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $item['id'] . "][" . $item['name'] . "]:启动成功" . PHP_EOL; + } + } + + public function reload($data) + { + $id = intval($data['args'] ?? 0); //定时任务表主键id + if (isset($this->crontabIds[$id])) { + Crontab::remove($this->crontabIds[$id]); + unset($this->crontabIds[$id]); //删除定时任务表主键id => Crontab对象id + echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $id . "]:移除成功" . PHP_EOL; + } + $item = $this->logic->read($id);// 查询定时任务表数据 + if ($item && $item['status'] == 1) { + $crontab = new Crontab($item['rule'], function () use ($item) { + $this->logic->run($item['id']); + }); + $this->crontabIds[$id] = $crontab->getId(); //存储定时任务表主键id => Crontab对象id + echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $item['id'] . "][" . $item['name'] . "]:启动成功" . PHP_EOL; + } + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/process/Test.php b/server/plugin/saiadmin/process/Test.php new file mode 100644 index 0000000..67eb164 --- /dev/null +++ b/server/plugin/saiadmin/process/Test.php @@ -0,0 +1,15 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\process; + +class Test +{ + public function run($args): void + { + echo '任务[Test]调用:' . date('Y-m-d H:i:s') . "\n"; + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/public/assets/bootstrap.min.css b/server/plugin/saiadmin/public/assets/bootstrap.min.css new file mode 100644 index 0000000..1472dec --- /dev/null +++ b/server/plugin/saiadmin/public/assets/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/server/plugin/saiadmin/public/assets/jquery.min.js b/server/plugin/saiadmin/public/assets/jquery.min.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/server/plugin/saiadmin/public/assets/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\service; + +use PHPMailer\PHPMailer\Exception; +use PHPMailer\PHPMailer\PHPMailer; +use plugin\saiadmin\app\logic\system\SystemConfigLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Arr; + +/** + * 邮件服务类 + */ +class EmailService +{ + /** + * 读取配置 + * @return array + */ + public static function getConfig(): array + { + $logic = new SystemConfigLogic(); + $config = $logic->getGroup('email_config'); + if (!$config) { + throw new ApiException('未设置邮件配置'); + } + return $config; + } + + /** + * Get Mailer + * @return PHPMailer + */ + public static function getMailer(): PHPMailer + { + if (!class_exists(PHPMailer::class)) { + throw new ApiException('请执行 composer require phpmailer/phpmailer 并重启'); + } + $config = static::getConfig(); + $mailer = new PHPMailer(); + $mailer->SMTPDebug = intval(Arr::getConfigValue($config,'SMTPDebug')); + $mailer->isSMTP(); + $mailer->Host = Arr::getConfigValue($config,'Host'); + $mailer->SMTPAuth = true; + $mailer->CharSet = Arr::getConfigValue($config,'CharSet'); + $mailer->Username = Arr::getConfigValue($config,'Username'); + $mailer->Password = Arr::getConfigValue($config,'Password'); + $mailer->SMTPSecure = Arr::getConfigValue($config,'SMTPSecure'); + $mailer->Port = Arr::getConfigValue($config,'Port'); + return $mailer; + } + + /** + * 发送邮件 + * @param $from + * @param $to + * @param $subject + * @param $content + * @return string + * @throws Exception + */ + public static function send($from, $to, $subject, $content): string + { + $mailer = static::getMailer(); + call_user_func_array([$mailer, 'setFrom'], (array)$from); + call_user_func_array([$mailer, 'addAddress'], (array)$to); + $mailer->Subject = $subject; + $mailer->isHTML(true); + $mailer->Body = $content; + $mailer->send(); + return $mailer->ErrorInfo; + } + + /** + * 按照模版发送 + * @param string|array $to + * @param $subject + * @param $content + * @param array $templateData + * @return string + * @throws Exception + */ + public static function sendByTemplate($to, $subject, $content, array $templateData = []): string + { + if ($templateData) { + $search = []; + foreach ($templateData as $key => $value) { + $search[] = '{' . $key . '}'; + } + $content = str_replace($search, array_values($templateData), $content); + } + $config = static::getConfig(); + return static::send([Arr::getConfigValue($config,'From'), Arr::getConfigValue($config,'FromName')], $to, $subject, $content); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/service/OpenSpoutWriter.php b/server/plugin/saiadmin/service/OpenSpoutWriter.php new file mode 100644 index 0000000..882c38e --- /dev/null +++ b/server/plugin/saiadmin/service/OpenSpoutWriter.php @@ -0,0 +1,136 @@ +filepath = $this->getFileName($fileName); + $this->instance = new Writer(); + $this->instance->openToFile($this->filepath); + } + + /** + * 获取完整的文件路径 + * @param string $fileName + * @return string + */ + public function getFileName(string $fileName): string + { + $path = config('plugin.saiadmin.saithink.export_path',base_path() . '/plugin/saiadmin/public/export/'); + @mkdir($path, 0777, true); + return $path . $fileName; + } + + /** + * 设置表格宽度 + * @param array $width 宽度数组 + * @return void + */ + public function setWidth(array $width = []) + { + if (empty($width)) { + return; + } + $sheet = $this->instance->getCurrentSheet(); + foreach ($width as $key => $value) { + $sheet->setColumnWidth($value, $key + 1); + } + } + + /** + * 设置表头 + * @param array $header 表头数组 + * @param $style + * @return void + */ + public function setHeader(array $header = [], $style = null): void + { + if (empty($style)) { + $border = new Border( + new BorderPart("top", "black", "thin"), + new BorderPart("right", "black", "thin"), + new BorderPart("bottom", "black", "thin"), + new BorderPart("left", "black", "thin"), + ); + $style = new Style(); + $style->setFontBold(); + $style->setCellAlignment("center"); + $style->setBorder($border); + } + $rowFromValues = Row::fromValues($header, $style); + $this->instance->addRow($rowFromValues); + } + + /** + * 设置数据 + * @param array $data 数据数组 + * @param $style + * @return void + */ + public function setData(array $data = [], $style = null, array $filter = []): void + { + if (empty($style)) { + $border = new Border( + new BorderPart("top", "black", "thin"), + new BorderPart("right", "black", "thin"), + new BorderPart("bottom", "black", "thin"), + new BorderPart("left", "black", "thin"), + ); + $style = new Style(); + $style->setCellAlignment("center"); + $style->setBorder($border); + } + foreach($data as $row) { + if (!empty($filter)) { + foreach ($filter as $key => $value) { + foreach ($value as $item) { + if ($item['value'] == $row[$key]) { + $row[$key] = $item['label']; + break; + } + } + } + } + $rowFromValues = Row::fromValues($row, $style); + $this->instance->addRow($rowFromValues); + } + } + + /** + * 获取文件 + * @return string + */ + public function returnFile(): string + { + $this->instance->close(); + return $this->filepath; + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/service/Permission.php b/server/plugin/saiadmin/service/Permission.php new file mode 100644 index 0000000..5ae9622 --- /dev/null +++ b/server/plugin/saiadmin/service/Permission.php @@ -0,0 +1,51 @@ +title = $title ?? ''; + $this->slug = $slug; + } + + /** + * 获取权限标题 + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * 获取权限标识 + */ + public function getSlug(): ?string + { + return $this->slug; + } + +} diff --git a/server/plugin/saiadmin/service/storage/ChunkUploadService.php b/server/plugin/saiadmin/service/storage/ChunkUploadService.php new file mode 100644 index 0000000..bef5b93 --- /dev/null +++ b/server/plugin/saiadmin/service/storage/ChunkUploadService.php @@ -0,0 +1,137 @@ +folder = $folder; + $this->config = $logic->getGroup('upload_config'); + $this->path = $this->checkPath(); + } + + /** + * 检查并创建上传路径 + * @return string + */ + public function checkPath(): string + { + $root = Arr::getConfigValue($this->config, 'local_root'); + $path = base_path() . DIRECTORY_SEPARATOR . $root . $this->folder . DIRECTORY_SEPARATOR; + if (!is_dir($path)) { + mkdir($path, 0777, true); + } + return $path; + } + + /** + * 检查切片文件上传状态 + * @param $data + * @return array + */ + public function checkChunk($data): array + { + $allow_file = Arr::getConfigValue($this->config, 'upload_allow_file'); + if (!in_array($data['ext'], explode(',', $allow_file))) { + throw new ApiException('不支持该格式的文件上传'); + } + // 检查已经上传的分片文件 + for ($i = 0; $i < $data['total']; ++$i) { + $chunkFile = $this->path . "{$data['hash']}_{$data['total']}_{$i}.chunk"; + if (!file_exists($chunkFile)) { + if ($i == 0) { + return $this->uploadChunk($data); + } else { + return ['chunk' => $i, 'status' => 'resume']; + } + } + } + // 分片文件已经全部上传 + return ['chunk' => $i, 'status' => 'success']; + } + + /** + * 上传切片 + * @param $data + * @return array + */ + public function uploadChunk($data): array + { + $allow_file = Arr::getConfigValue($this->config, 'upload_allow_file'); + if (!in_array($data['ext'], explode(',', $allow_file))) { + throw new ApiException('不支持该格式的文件上传'); + } + $request = request(); + if (!$request) { + throw new ApiException('切片上传服务必须在 HTTP 请求环境下调用'); + } + $uploadFile = current($request->file()); + $chunkName = $this->path . "{$data['hash']}_{$data['total']}_{$data['index']}.chunk"; + $uploadFile->move($chunkName); + if (($data['index'] + 1) == $data['total']) { + return $this->mergeChunk($data); + } + return ['chunk' => $data['index'], 'status' => 'success']; + } + + /** + * 合并切片文件 + * @param $data + * @return array + */ + public function mergeChunk($data): array + { + $filePath = $this->path . $data['hash'] . '.' . $data['ext']; + $fileHandle = fopen($filePath, 'w'); + for ($i = 0; $i < $data['total']; ++$i) { + $chunkFile = $this->path . "{$data['hash']}_{$data['total']}_{$i}.chunk"; + if (!file_exists($chunkFile)) { + throw new ApiException('切片文件查找失败,请重新上传'); + } + fwrite($fileHandle, file_get_contents($chunkFile)); + unlink($chunkFile); + } + + $domain = Arr::getConfigValue($this->config, 'local_domain'); + $uri = Arr::getConfigValue($this->config, 'local_uri'); + $baseUrl = $domain . $uri . $this->folder . '/'; + + $save_path = Arr::getConfigValue($this->config, 'local_root') . $this->folder . '/'; + $object_name = $data['hash'] . '.' . $data['ext']; + + $info['storage_mode'] = 1; + $info['category_id'] = 1; + $info['origin_name'] = $data['name']; + $info['object_name'] = $object_name; + $info['hash'] = $data['hash']; + $info['mime_type'] = $data['type']; + $info['storage_path'] = $save_path . $object_name; + $info['suffix'] = $data['ext']; + $info['size_byte'] = $data['size']; + $info['size_info'] = formatBytes($data['size']); + $info['url'] = $baseUrl . $object_name; + SystemAttachment::create($info); + return $info; + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/service/storage/UploadService.php b/server/plugin/saiadmin/service/storage/UploadService.php new file mode 100644 index 0000000..de06f21 --- /dev/null +++ b/server/plugin/saiadmin/service/storage/UploadService.php @@ -0,0 +1,136 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\service\storage; + +use plugin\saiadmin\app\logic\system\SystemConfigLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Arr; + +/** + * 文件上传服务 + * @method static array uploadFile(array $config = []) 上传文件 + * @method static array uploadBase64(string $base64, string $extension = 'png') 上传Base64文件 + * @method static array uploadServerFile(string $file_path) 上传服务端文件 + */ +class UploadService +{ + /** + * @desc 存储磁盘 + * @param int $type + * @param string $upload + * @param bool $_is_file_upload + * @return mixed + */ + public static function disk(int $type = 1, string $upload = 'image', bool $_is_file_upload = true) + { + $logic = new SystemConfigLogic(); + $uploadConfig = $logic->getGroup('upload_config'); + + $file = current(request()->file()); + $ext = $file->getUploadExtension() ?: null; + $file_size = $file->getSize(); + if ($file_size > Arr::getConfigValue($uploadConfig, 'upload_size')) { + throw new ApiException('文件大小超过限制'); + } + $allow_file = Arr::getConfigValue($uploadConfig, 'upload_allow_file'); + $allow_image = Arr::getConfigValue($uploadConfig, 'upload_allow_image'); + if ($upload == 'image') { + if (!in_array($ext, explode(',', $allow_image))) { + throw new ApiException('不支持该格式的文件上传'); + } + } else { + if (!in_array($ext, explode(',', $allow_file))) { + throw new ApiException('不支持该格式的文件上传'); + } + } + switch ($type) { + case 1: + // 本地 + $config = [ + 'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class, + 'root' => Arr::getConfigValue($uploadConfig, 'local_root'), + 'dirname' => function () { + return date('Ymd'); + }, + 'domain' => Arr::getConfigValue($uploadConfig, 'local_domain'), + 'uri' => Arr::getConfigValue($uploadConfig, 'local_uri'), + 'algo' => 'sha1', + ]; + break; + case 2: + // 阿里云 + $config = [ + 'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class, + 'accessKeyId' => Arr::getConfigValue($uploadConfig, 'oss_accessKeyId'), + 'accessKeySecret' => Arr::getConfigValue($uploadConfig, 'oss_accessKeySecret'), + 'bucket' => Arr::getConfigValue($uploadConfig, 'oss_bucket'), + 'dirname' => Arr::getConfigValue($uploadConfig, 'oss_dirname'), + 'domain' => Arr::getConfigValue($uploadConfig, 'oss_domain'), + 'endpoint' => Arr::getConfigValue($uploadConfig, 'oss_endpoint'), + 'algo' => 'sha1', + ]; + break; + case 3: + // 七牛 + $config = [ + 'adapter' => \Tinywan\Storage\Adapter\QiniuAdapter::class, + 'accessKey' => Arr::getConfigValue($uploadConfig, 'qiniu_accessKey'), + 'secretKey' => Arr::getConfigValue($uploadConfig, 'qiniu_secretKey'), + 'bucket' => Arr::getConfigValue($uploadConfig, 'qiniu_bucket'), + 'dirname' => Arr::getConfigValue($uploadConfig, 'qiniu_dirname'), + 'domain' => Arr::getConfigValue($uploadConfig, 'qiniu_domain'), + ]; + break; + case 4: + // 腾讯云 + $config = [ + 'adapter' => \Tinywan\Storage\Adapter\CosAdapter::class, + 'secretId' => Arr::getConfigValue($uploadConfig, 'cos_secretId'), + 'secretKey' => Arr::getConfigValue($uploadConfig, 'cos_secretKey'), + 'bucket' => Arr::getConfigValue($uploadConfig, 'cos_bucket'), + 'dirname' => Arr::getConfigValue($uploadConfig, 'cos_dirname'), + 'domain' => Arr::getConfigValue($uploadConfig, 'cos_domain'), + 'region' => Arr::getConfigValue($uploadConfig, 'cos_region'), + ]; + break; + case 5: + // s3 亚马逊 + $config = [ + 'adapter' => \Tinywan\Storage\Adapter\S3Adapter::class, + 'key' => Arr::getConfigValue($uploadConfig, 's3_key'), + 'secret' => Arr::getConfigValue($uploadConfig, 's3_secret'), + 'bucket' => Arr::getConfigValue($uploadConfig, 's3_bucket'), + 'dirname' => Arr::getConfigValue($uploadConfig, 's3_dirname'), + 'domain' => Arr::getConfigValue($uploadConfig, 's3_domain'), + 'region' => Arr::getConfigValue($uploadConfig, 's3_region'), + 'version' => Arr::getConfigValue($uploadConfig, 's3_version'), + // 'use_path_style_endpoint' => Arr::getConfigValue($uploadConfig,'s3_use_path_style_endpoint'), + 'use_path_style_endpoint' => filter_var(Arr::getConfigValue($uploadConfig, 's3_use_path_style_endpoint'), FILTER_VALIDATE_BOOLEAN), + 'endpoint' => Arr::getConfigValue($uploadConfig, 's3_endpoint'), + 'acl' => Arr::getConfigValue($uploadConfig, 's3_acl'), + ]; + break; + default: + throw new ApiException('该上传模式不存在'); + } + return new $config['adapter'](array_merge( + $config, + ['_is_file_upload' => $_is_file_upload] + )); + } + + /** + * @param $name + * @param $arguments + * @return mixed + * @author Tinywan(ShaoBo Wan) + */ + public static function __callStatic($name, $arguments) + { + return static::disk()->{$name}(...$arguments); + } +} diff --git a/server/plugin/saiadmin/utils/Arr.php b/server/plugin/saiadmin/utils/Arr.php new file mode 100644 index 0000000..e4a3ac6 --- /dev/null +++ b/server/plugin/saiadmin/utils/Arr.php @@ -0,0 +1,204 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\utils; + +/** + * Array操作类 + * Class Arr + */ +class Arr +{ + /** + * 获取数组中指定的列 + * @param array $source + * @param string $column + * @return array + */ + public static function getArrayColumn($source, $column): array + { + $columnArr = []; + foreach ($source as $item) { + $columnArr[] = $item[$column]; + } + return $columnArr; + } + + /** + * 批量获取数组中指定的列 + * @param array $source + * @param array $column + * @return array + */ + public static function getArrayColumns($source, $columns): array + { + $columnArr = []; + foreach ($source as $item) { + $tempArr = []; + foreach ($columns as $key) { + $temp = explode('.', $key); + if (count($temp) > 1) { + $tempArr[$key] = $item[$temp[0]][$temp[1]]; + } else { + $tempArr[$key] = $item[$key]; + } + } + $columnArr[] = $tempArr; + } + return $columnArr; + } + + /** + * 把二维数组中某列设置为key返回 + * @param array $array 输入数组 + * @param string $field 要作为键的字段名 + * @param bool $unique 要做键的字段是否唯一(该字段与记录是否一一对应) + * @return array + */ + public static function fieldAsKey($array, $field, $unique = false) { + $result = []; + foreach ($array as $item) { + if (isset($item[$field])) { + if (!$unique && isset($result[$item[$field]])) { + $unique = true; + $result[$item[$field]] = [($result[$item[$field]])]; + $result[$item[$field]][] = $item; + } elseif ($unique) { + $result[$item[$field]][] = $item; + } else { + $result[$item[$field]] = $item; + } + } + } + return $result; + } + + /** + * 数组转字符串去重复 + * @param array $data + * @return false|string[] + */ + public static function unique(array $data) + { + return array_unique(explode(',', implode(',', $data))); + } + + /** + * 获取数组中去重复过后的指定key值 + * @param array $list + * @param string $key + * @return array + */ + public static function getUniqueKey(array $list, string $key) + { + return array_unique(array_column($list, $key)); + } + + /** + * 合并二维数组,并且指定key去重, 第一个覆盖第二个 + * @param array $arr1 + * @param array $arr2 + * @param string $key + * @return array + */ + public static function mergeArray(array $arr1, array $arr2, string $key) + { + $arr = array_merge($arr1,$arr2); + $tmp_arr = []; + foreach($arr as $k => $v) { + if(in_array($v[$key], $tmp_arr)) { + unset($arr[$k]); + } else { + $tmp_arr[] = $v[$key]; + } + } + return $arr; + } + + /** + * 相同键值的合并作为键生成新数组 + * @param array $data + * @param string $field + * @return array + */ + public static function groupSameField(array $data, string $field) + { + $result= []; + foreach ($data as $key => $info) { + $result[$info[$field]][] = $info; + } + return $result; + } + + /** + * 生成无限级树算法 + * @param array $arr 输入数组 + * @param number $pid 根级的pid + * @param string $column_name 列名,id|pid父id的名字|children子数组的键名 + * @return array $ret + */ + public static function makeTree($arr, $pid = 0, $column_name = 'id|pid|children') { + list($idname, $pidname, $cldname) = explode('|', $column_name); + $ret = array(); + foreach ($arr as $k => $v) { + if ($v [$pidname] == $pid) { + $tmp = $arr [$k]; + unset($arr [$k]); + $tmp [$cldname] = self::makeTree($arr, $v [$idname], $column_name); + $ret [] = $tmp; + } + } + return $ret; + } + + /** + * 二位数组按某个键值排序 + * @param array $arr + * @param string $key + * @param int $sort + * @return array + */ + public static function sortArray($arr, $key, $sort = SORT_ASC) + { + array_multisort(array_column($arr,$key),$sort,$arr); + return $arr; + } + + /** + * 数组中根据某一列中某个字段的值来查询这一列数据 + * @param $array + * @param $column + * @param $value + * @return array + */ + public static function getArrayByColumn($array, $column, $value): array + { + $result = []; + foreach ($array as $key => $item) { + if ($item[$column] == $value) { + $result = $item; + } + } + return $result; + } + + /** + * 数组中根据key值获取value + * @param $array + * @param $key + * @return mixed|string + */ + public static function getConfigValue($array, $key) + { + foreach ($array as $item) { + if ($item['key'] === $key) { + return $item['value']; + } + } + return ''; + } + +} diff --git a/server/plugin/saiadmin/utils/Captcha.php b/server/plugin/saiadmin/utils/Captcha.php new file mode 100644 index 0000000..ce09c87 --- /dev/null +++ b/server/plugin/saiadmin/utils/Captcha.php @@ -0,0 +1,121 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\utils; + +use support\think\Cache; +use Ramsey\Uuid\Uuid; +use Webman\Captcha\CaptchaBuilder; +use Webman\Captcha\PhraseBuilder; +use plugin\saiadmin\exception\ApiException; + +/** + * 验证码工具类 + */ +class Captcha +{ + /** + * 图形验证码 + * @return array + */ + public static function imageCaptcha(): array + { + $builder = new PhraseBuilder(4, 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'); + $captcha = new CaptchaBuilder(null, $builder); + $captcha->setBackgroundColor(242, 243, 245); + $captcha->build(120, 36); + + $uuid = Uuid::uuid4(); + $key = $uuid->toString(); + $mode = config('plugin.saiadmin.saithink.captcha.mode', 'session'); + $expire = config('plugin.saiadmin.saithink.captcha.expire', 300); + $code = strtolower($captcha->getPhrase()); + if ($mode === 'cache') { + try { + Cache::set($key, $code, $expire); + } catch (\Exception $e) { + return [ + 'result' => -1, + 'message' => '验证码获取失败,请检查缓存配置' + ]; + } + } else { + $request = request(); + if ($request) { + $request->session()->set($key, $code); + } + } + $img_content = $captcha->get(); + return [ + 'result' => 1, + 'uuid' => $key, + 'image' => 'data:image/png;base64,' . base64_encode($img_content) + ]; + } + + /** + * 数字验证码 + * @param string $key + * @param int $length + * @return array + */ + public static function numberCaptcha(string $key, int $length = 4): array + { + $code = str_pad(rand(0, 999999), $length, '0', STR_PAD_LEFT); + $mode = config('plugin.saiadmin.saithink.captcha.mode', 'session'); + $expire = config('plugin.saiadmin.saithink.captcha.expire', 300); + if ($mode === 'cache') { + try { + Cache::set($key, $code, $expire); + } catch (\Exception $e) { + return [ + 'result' => -1, + 'message' => '验证码获取失败,请检查缓存配置' + ]; + } + } else { + $request = request(); + if ($request) { + $request->session()->set($key, $code); + } + } + return [ + 'result' => 1, + 'uuid' => $key, + 'code' => $code, + ]; + } + + /** + * 验证码验证 + * @param string $uuid + * @param string|int $captcha + * @return bool + */ + public static function checkCaptcha(string $uuid, string|int $captcha): bool + { + $mode = config('plugin.saiadmin.saithink.captcha.mode', 'session'); + if ($mode === 'cache') { + try { + $code = Cache::get($uuid); + Cache::delete($uuid); + } catch (\Exception $e) { + throw new ApiException($e->getMessage()); + } + } else { + try { + $code = session($uuid); + session()->forget($uuid); + } catch (\Exception $e) { + throw new ApiException($e->getMessage()); + } + } + if (strtolower($captcha) !== $code) { + return false; + } + return true; + } +} diff --git a/server/plugin/saiadmin/utils/Helper.php b/server/plugin/saiadmin/utils/Helper.php new file mode 100644 index 0000000..9c15cd7 --- /dev/null +++ b/server/plugin/saiadmin/utils/Helper.php @@ -0,0 +1,254 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\utils; + +/** + * 帮助类 + */ +class Helper +{ + /** + * 数据树形化 + * @param array $data 数据 + * @param string $childrenname 子数据名 + * @param string $keyName 数据key名 + * @param string $pidName 数据上级key名 + * @return array + */ + public static function makeTree(array $data, string $childrenname = 'children', string $keyName = 'id', string $pidName = 'parent_id') + { + $list = []; + foreach ($data as $value) { + $list[$value[$keyName]] = $value; + } + $tree = []; //格式化好的树 + foreach ($list as $item) { + if (isset($list[$item[$pidName]])) { + $list[$item[$pidName]][$childrenname][] = &$list[$item[$keyName]]; + } else { + $tree[] = &$list[$item[$keyName]]; + } + } + return $tree; + } + + /** + * 生成Arco菜单 + * @param array $data 数据 + * @param string $childrenname 子数据名 + * @param string $keyName 数据key名 + * @param string $pidName 数据上级key名 + * @return array + */ + public static function makeArcoMenus(array $data, string $childrenname = 'children', string $keyName = 'id', string $pidName = 'parent_id') + { + $list = []; + foreach ($data as $value) { + if ($value['type'] === 'M'){ + $path = '/'.$value['route']; + $layout = isset($value['is_layout']) ? $value['is_layout'] : 1; + $temp = [ + $keyName => $value[$keyName], + $pidName => $value[$pidName], + 'name' => $value['route'], + 'path' => $path, + 'component' => $value['component'], + 'redirect' => $value['redirect'], + 'meta' => [ + 'title' => $value['name'], + 'type' => $value['type'], + 'hidden' => $value['is_hidden'] === 1, + 'layout' => $layout === 1, + 'hiddenBreadcrumb' => false, + 'icon' => $value['icon'], + ], + ]; + $list[$value[$keyName]] = $temp; + } + if ($value['type'] === 'I' || $value['type'] === 'L'){ + $temp = [ + $keyName => $value[$keyName], + $pidName => $value[$pidName], + 'name' => $value['code'], + 'path' => $value['route'], + 'meta' => [ + 'title' => $value['name'], + 'type' => $value['type'], + 'hidden' => $value['is_hidden'] === 1, + 'hiddenBreadcrumb' => false, + 'icon' => $value['icon'], + ], + ]; + $list[$value[$keyName]] = $temp; + } + } + $tree = []; //格式化好的树 + foreach ($list as $item) { + if (isset($list[$item[$pidName]])) { + $list[$item[$pidName]][$childrenname][] = &$list[$item[$keyName]]; + } else { + $tree[] = &$list[$item[$keyName]]; + } + } + return $tree; + } + + + /** + * 生成Artd菜单 + * @param array $data + * @param string $childrenname + * @param string $keyName + * @param string $pidName + * @return array + */ + public static function makeArtdMenus(array $data, string $childrenname = 'children', string $keyName = 'id', string $pidName = 'parent_id') + { + $list = []; + foreach ($data as $value) { + $component = ''; + if ($value['type'] === 1) { + $component = '/index/index'; + } + if ($value['type'] === 2) { + $component = $value['component']; + } + $temp = [ + $keyName => $value[$keyName], + $pidName => $value[$pidName], + 'name' => $value['code'], + 'path' => $value['path'], + 'component' => $component, + 'meta' => [ + 'title' => $value['name'], + 'icon' => $value['icon'], + 'isIframe' => $value['is_iframe'] === 1, + 'keepAlive' => $value['is_keep_alive'] === 1, + 'isHide' => $value['is_hidden'] === 1, + 'fixedTab' => $value['is_fixed_tab'] === 1, + 'isFullPage' => $value['is_full_page'] === 1, + ], + ]; + if ($value['type'] === 4) { + $temp['path'] = '/outside/Iframe'; + $temp['meta']['link'] = $value['link_url']; + } + $list[$value[$keyName]] = $temp; + } + $tree = []; + foreach ($list as $item) { + if (isset($list[$item[$pidName]])) { + $list[$item[$pidName]][$childrenname][] = &$list[$item[$keyName]]; + } else { + $tree[] = &$list[$item[$keyName]]; + } + } + return $tree; + } + + /** + * 下划线转驼峰 + */ + public static function camelize($uncamelized_words,$separator='_') + { + $uncamelized_words = $separator. str_replace($separator, " ", strtolower($uncamelized_words)); + return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator ); + } + + /** + * 驼峰命名转下划线命名 + */ + public static function uncamelize($camelCaps,$separator='_') + { + return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps)); + } + + /** + * 转换为驼峰 + * @param string $value + * @return string + */ + public static function camel(string $value): string + { + static $cache = []; + $key = $value; + + if (isset($cache[$key])) { + return $cache[$key]; + } + + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + + return $cache[$key] = str_replace(' ', '', $value); + } + + /** + * 获取业务名称 + * @param string $tableName + * @return mixed + */ + public static function get_business(string $tableName) + { + $start = strrpos($tableName,'_'); + if ($start !== false) { + $result = substr($tableName, $start + 1); + } else { + $result = $tableName; + } + return static::camelize($result); + } + + /** + * 获取业务名称 + * @param string $tableName + * @return mixed + */ + public static function get_big_business(string $tableName) + { + $start = strrpos($tableName,'_'); + $result = substr($tableName, $start + 1); + return static::camel($result); + } + + /** + * 只替换一次字符串 + * @param $needle + * @param $replace + * @param $haystack + * @return array|mixed|string|string[] + */ + public static function str_replace_once($needle, $replace, $haystack) + { + $pos = strpos($haystack, $needle); + if ($pos === false) { + return $haystack; + } + return substr_replace($haystack, $replace, $pos, strlen($needle)); + } + + /** + * 遍历目录 + * @param $template_name + * @return array + */ + public static function get_dir($template_name) + { + $dir = base_path($template_name); + $fileDir = []; + if (is_dir($dir)){ + if ($dh = opendir($dir)){ + while (($file = readdir($dh)) !== false){ + if($file != "." && $file != ".."){ + array_push($fileDir, $file); + } + } + closedir($dh); + } + } + return $fileDir; + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/utils/ServerMonitor.php b/server/plugin/saiadmin/utils/ServerMonitor.php new file mode 100644 index 0000000..58cd32d --- /dev/null +++ b/server/plugin/saiadmin/utils/ServerMonitor.php @@ -0,0 +1,253 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\utils; + +/** + * 服务器监控信息 + */ +class ServerMonitor +{ + /** + * 获取内存信息 + * @return array + */ + public function getMemoryInfo(): array + { + $totalMem = 0; // 总内存 (Bytes) + $freeMem = 0; // 可用/剩余内存 (Bytes) + + if (stristr(PHP_OS, 'WIN')) { + // Windows 系统 + // 一次性获取 总可见内存 和 空闲物理内存 (单位都是 KB) + // TotalVisibleMemorySize: 操作系统可识别的内存总数 (比物理内存条总数略少,更准确反映可用上限) + // FreePhysicalMemory: 当前可用物理内存 + $cmd = 'wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /format:csv'; + $output = shell_exec($cmd); + $output = mb_convert_encoding($output ?? '', 'UTF-8', 'GBK, UTF-8, ASCII'); + $lines = explode("\n", trim($output)); + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + + // CSV 格式: Node,FreePhysicalMemory,TotalVisibleMemorySize + $parts = str_getcsv($line); + + // 确保解析正确且排除标题行 (通常索引1是Free, 2是Total) + if (count($parts) >= 3 && is_numeric($parts[1])) { + $freeMem = floatval($parts[1]) * 1024; // KB -> Bytes + $totalMem = floatval($parts[2]) * 1024; // KB -> Bytes + break; + } + } + } else { + // Linux 系统 + // 读取 /proc/meminfo,效率远高于 shell_exec('cat ...') + $memInfo = @file_get_contents('/proc/meminfo'); + if ($memInfo) { + // 使用正则提取 MemTotal 和 MemAvailable (单位 kB) + // MemAvailable 是较新的内核指标,比单纯的 MemFree 更准确(包含可回收的缓存) + if (preg_match('/^MemTotal:\s+(\d+)\s+kB/m', $memInfo, $matches)) { + $totalMem = floatval($matches[1]) * 1024; + } + + if (preg_match('/^MemAvailable:\s+(\d+)\s+kB/m', $memInfo, $matches)) { + $freeMem = floatval($matches[1]) * 1024; + } else { + // 如果内核太老没有 MemAvailable,退化使用 MemFree + if (preg_match('/^MemFree:\s+(\d+)\s+kB/m', $memInfo, $matches)) { + $freeMem = floatval($matches[1]) * 1024; + } + } + } + } + + // 计算已用内存 + $usedMem = $totalMem - $freeMem; + + // 避免除以0 + $rate = ($totalMem > 0) ? ($usedMem / $totalMem) * 100 : 0; + + // PHP 自身占用 + $phpMem = memory_get_usage(true); + + return [ + // 人类可读格式 (String) + 'total' => $this->formatBytes($totalMem), + 'free' => $this->formatBytes($freeMem), + 'used' => $this->formatBytes($usedMem), + 'php' => $this->formatBytes($phpMem), + 'rate' => sprintf('%.2f', $rate) . '%', + + // 原始数值 (Float/Int),方便前端图表使用或逻辑判断,统一单位 Bytes + 'raw' => [ + 'total' => $totalMem, + 'free' => $freeMem, + 'used' => $usedMem, + 'php' => $phpMem, + 'rate' => round($rate, 2) + ] + ]; + } + + /** + * 获取PHP及环境信息 + * @return array + */ + public function getPhpAndEnvInfo(): array + { + return [ + 'php_version' => PHP_VERSION, + 'os' => PHP_OS, + 'project_path' => BASE_PATH, + 'memory_limit' => ini_get('memory_limit'), + 'max_execution_time' => ini_get('max_execution_time'), + 'error_reporting' => ini_get('error_reporting'), + 'display_errors' => ini_get('display_errors'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size'), + 'extension_dir' => ini_get('extension_dir'), + 'loaded_extensions' => implode(', ', get_loaded_extensions()), + ]; + } + + /** + * 获取磁盘信息 + * @return array + */ + public function getDiskInfo(): array + { + $disk = []; + + if (stristr(PHP_OS, 'WIN')) { + // Windows 系统 + // 使用 CSV 格式输出,避免空格解析错误;SkipTop=1 跳过空行 + // LogicalDisk 包含: Caption(盘符), FreeSpace(剩余字节), Size(总字节) + $cmd = 'wmic logicaldisk get Caption,FreeSpace,Size /format:csv'; + $output = shell_exec($cmd); + + // 转换编码,防止中文乱码(视服务器环境而定,通常 Windows CMD 输出为 GBK) + $output = mb_convert_encoding($output ?? '', 'UTF-8', 'GBK, UTF-8, ASCII'); + $lines = explode("\n", trim($output)); + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + + // CSV 格式: Node,Caption,FreeSpace,Size + $parts = str_getcsv($line); + + // 确保数据列足够且是一个盘符 (例如 "C:") + // 索引通常是: 1=>Caption, 2=>FreeSpace, 3=>Size (索引0通常是计算机名) + if (count($parts) >= 4 && preg_match('/^[A-Z]:$/', $parts[1])) { + $caption = $parts[1]; + $freeSpace = floatval($parts[2]); + $totalSize = floatval($parts[3]); + + // 避免除以 0 错误(如光驱未放入光盘时 Size 可能为 0 或 null) + if ($totalSize <= 0) continue; + + $usedSpace = $totalSize - $freeSpace; + + $disk[] = [ + 'filesystem' => $caption, + 'mounted_on' => $caption, + 'size' => $this->formatBytes($totalSize), + 'available' => $this->formatBytes($freeSpace), + 'used' => $this->formatBytes($usedSpace), + 'use_percentage' => sprintf('%.2f', ($usedSpace / $totalSize) * 100) . '%', + 'raw' => [ // 保留原始数据以便前端或其他逻辑使用 + 'size' => $totalSize, + 'available' => $freeSpace, + 'used' => $usedSpace + ] + ]; + } + } + + } else { + // Linux 系统 + // -P: POSIX 输出格式(强制在一行显示,防止长挂载点换行) + // -T: 显示文件系统类型 + // 默认单位是 1K-blocks (1024字节) + $output = shell_exec('df -TP 2>/dev/null'); + $lines = explode("\n", trim($output ?? '')); + + // 过滤表头 + array_shift($lines); + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + + // 限制分割数量,防止挂载点名称中有空格导致解析错位(虽然 -P 很大程度避免了这个问题,但仍需谨慎) + $parts = preg_split('/\s+/', $line); + + // df -TP 输出列: Filesystem(0), Type(1), 1024-blocks(2), Used(3), Available(4), Capacity(5), Mounted on(6) + if (count($parts) >= 7) { + $filesystem = $parts[0]; + $type = $parts[1]; + $totalKB = floatval($parts[2]); // 单位是 KB + $usedKB = floatval($parts[3]); + $availKB = floatval($parts[4]); + $mountedOn = $parts[6]; + + // 过滤逻辑:只显示物理硬盘或特定挂载点 + // 通常过滤掉 tmpfs, devtmpfs, overlay, squashfs(snap) 等 + // 如果你只想看 /dev/ 开头的物理盘,保留原来的正则即可 + if (!preg_match('/^\/dev\//', $filesystem)) { + // continue; // 根据需求决定是否取消注释此行 + } + // 过滤掉 Docker overlay 或 kubelet 等产生的繁杂挂载 + if (strpos($filesystem, 'overlay') !== false) continue; + + // 转换为字节 + $totalSize = $totalKB * 1024; + $usedSize = $usedKB * 1024; + $freeSize = $availKB * 1024; + + if ($totalSize <= 0) continue; + + $disk[] = [ + 'filesystem' => $filesystem, + 'type' => $type, + 'mounted_on' => $mountedOn, + 'size' => $this->formatBytes($totalSize), + 'available' => $this->formatBytes($freeSize), + 'used' => $this->formatBytes($usedSize), + 'use_percentage' => sprintf('%.2f', ($usedSize / $totalSize) * 100) . '%', + 'raw' => [ + 'size' => $totalSize, + 'available' => $freeSize, + 'used' => $usedSize + ] + ]; + } + } + } + return $disk; + } + + /** + * 格式化字节为可读格式 (B, KB, MB, GB, TB...) + * @param int|float $bytes 字节数 + * @param int $precision 小数点后保留位数 + * @return string + */ + private function formatBytes($bytes, int $precision = 2): string + { + if ($bytes <= 0) { + return '0 B'; + } + $base = log($bytes, 1024); + $suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; + // 确保不会数组越界 + $class = min((int)floor($base), count($suffixes) - 1); + return sprintf("%." . $precision . "f", $bytes / pow(1024, $class)) . ' ' . $suffixes[$class]; + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/utils/code/CodeEngine.php b/server/plugin/saiadmin/utils/code/CodeEngine.php new file mode 100644 index 0000000..a048358 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/CodeEngine.php @@ -0,0 +1,289 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\utils\code; + +use Twig\TwigFilter; +use Twig\Environment; +use Twig\Loader\FilesystemLoader; +use plugin\saiadmin\exception\ApiException; + +// 定义目录分隔符常量 +defined('DS') or define('DS', DIRECTORY_SEPARATOR); + +/** + * 代码生成引擎 + */ +class CodeEngine +{ + /** + * @var array 值栈 + */ + private array $value = []; + + /** + * 模板名称 + * @var string + */ + private string $stub = 'saiadmin'; + + /** + * 获取配置文件 + * @return string[] + */ + private static function _getConfig(): array + { + return [ + 'template_path' => base_path() . DS . 'plugin' . DS . 'saiadmin' . DS . 'utils' . DS . 'code' . DS . 'stub', + 'generate_path' => runtime_path() . DS . 'code_engine' . DS . 'saiadmin', + ]; + } + + /** + * 初始化 + * @param array $data 数据 + */ + public function __construct(array $data) + { + // 读取配置文件 + $config = self::_getConfig(); + + // 判断模板是否存在 + if (!is_dir($config['template_path'])) { + throw new ApiException('模板目录不存在!'); + } + // 判断文件生成目录是否存在 + if (!is_dir($config['generate_path'])) { + mkdir($config['generate_path'], 0770, true); + } + // 赋值 + $this->value = $data; + } + + /** + * 设置模板名称 + * @param $stub + * @return void + */ + public function setStub($stub): void + { + $this->stub = $stub; + } + + /** + * 渲染文件内容 + */ + public function renderContent($path, $filename): string + { + $config = self::_getConfig(); + + $path = $config['template_path'] . DS . $this->stub . DS . $path; + + $loader = new FilesystemLoader($path); + $twig = new Environment($loader); + $camelFilter = new TwigFilter('camel', function ($value) { + static $cache = []; + $key = $value; + if (isset($cache[$key])) { + return $cache[$key]; + } + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + return $cache[$key] = str_replace(' ', '', $value); + }); + $boolFilter = new TwigFilter('bool', function ($value) { + if ($value == 1) { + return 'true'; + } else { + return 'false'; + } + }); + $formatFilter = new TwigFilter('formatNumber', function ($value) { + if (ctype_digit((string) $value)) { + return $value; + } else { + return '1'; + } + }); + $defaultFilter = new TwigFilter('parseNumber', function ($value) { + if ($value) { + return $value; + } else { + return 'null'; + } + }); + $containsFilter = new TwigFilter('str_contains', function ($haystack, $needle) { + return str_contains($haystack ?? '', $needle ?? ''); + }); + + $twig->addFilter($camelFilter); + $twig->addFilter($boolFilter); + $twig->addFilter($containsFilter); + $twig->addFilter($formatFilter); + $twig->addFilter($defaultFilter); + + return $twig->render($filename, $this->value); + } + + /** + * 生成后端文件 + */ + public function generateBackend($action, $content): void + { + $outPath = ''; + if ($this->value['template'] == 'app') { + $rootPath = base_path() . DS . 'app' . DS . $this->value['namespace']; + $adminPath = ''; + + } else { + $rootPath = base_path() . DS . 'plugin' . DS . $this->value['namespace'] . DS . 'app'; + $adminPath = DS . 'admin'; + } + $subPath = DS . $this->value['package_name']; + switch ($action) { + case 'controller': + $outPath = $rootPath . $adminPath . DS . 'controller' . $subPath . DS . $this->value['class_name'] . 'Controller.php'; + break; + case 'logic': + $outPath = $rootPath . $adminPath . DS . 'logic' . $subPath . DS . $this->value['class_name'] . 'Logic.php'; + break; + case 'validate': + $outPath = $rootPath . $adminPath . DS . 'validate' . $subPath . DS . $this->value['class_name'] . 'Validate.php'; + break; + case 'model': + $outPath = $rootPath . DS . 'model' . $subPath . DS . $this->value['class_name'] . '.php'; + break; + default: + break; + } + + if (empty($outPath)) { + throw new ApiException('文件类型异常,无法生成指定文件!'); + } + if (!is_dir(dirname($outPath))) { + mkdir(dirname($outPath), 0777, true); + } + + file_put_contents($outPath, $content); + } + + /** + * 生成前端文件 + */ + public function generateFrontend($action, $content): void + { + $rootPath = dirname(base_path()) . DS . $this->value['generate_path']; + if (!is_dir($rootPath)) { + throw new ApiException('前端目录查找失败,必须与后端目录为同级目录!'); + } + + $rootPath = $rootPath . DS . 'src' . DS . 'views' . DS . 'plugin' . DS . $this->value['namespace']; + $subPath = DS . $this->value['package_name']; + switch ($action) { + case 'index': + $outPath = $rootPath . $subPath . DS . $this->value['business_name'] . DS . 'index.vue'; + break; + case 'edit-dialog': + $outPath = $rootPath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'edit-dialog.vue'; + break; + case 'table-search': + $outPath = $rootPath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'table-search.vue'; + break; + case 'api': + $outPath = $rootPath . DS . 'api' . $subPath . DS . $this->value['business_name'] . '.ts'; + break; + default: + break; + } + + if (empty($outPath)) { + throw new ApiException('文件类型异常,无法生成指定文件!'); + } + if (!is_dir(dirname($outPath))) { + mkdir(dirname($outPath), 0777, true); + } + + file_put_contents($outPath, $content); + } + + /** + * 生成临时文件 + */ + public function generateTemp(): void + { + $config = self::_getConfig(); + $rootPath = $config['generate_path']; + + $vuePath = $rootPath . DS . 'vue' . DS . 'src' . DS . 'views' . DS . 'plugin' . DS . $this->value['namespace']; + $phpPath = $rootPath . DS . 'php'; + $sqlPath = $rootPath . DS . 'sql'; + if ($this->value['template'] == 'app') { + $phpPath = $phpPath . DS . 'app' . DS . $this->value['namespace']; + $adminPath = ''; + } else { + $phpPath = $phpPath . DS . 'plugin' . DS . $this->value['namespace'] . DS . 'app'; + $adminPath = DS . 'admin'; + } + $subPath = DS . $this->value['package_name']; + + $indexOutPath = $vuePath . $subPath . DS . $this->value['business_name'] . DS . 'index.vue'; + $this->checkPath($indexOutPath); + $indexContent = $this->renderContent('vue', 'index.stub'); + file_put_contents($indexOutPath, $indexContent); + + $editOutPath = $vuePath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'edit-dialog.vue'; + $this->checkPath($editOutPath); + $editContent = $this->renderContent('vue', 'edit-dialog.stub'); + file_put_contents($editOutPath, $editContent); + + $searchOutPath = $vuePath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'table-search.vue'; + $this->checkPath($searchOutPath); + $searchContent = $this->renderContent('vue', 'table-search.stub'); + file_put_contents($searchOutPath, $searchContent); + + $viewOutPath = $vuePath . DS . 'api' . $subPath . DS . $this->value['business_name'] . '.ts'; + $this->checkPath($viewOutPath); + $viewContent = $this->renderContent('ts', 'api.stub'); + file_put_contents($viewOutPath, $viewContent); + + $controllerOutPath = $phpPath . $adminPath . DS . 'controller' . $subPath . DS . $this->value['class_name'] . 'Controller.php'; + $this->checkPath($controllerOutPath); + $controllerContent = $this->renderContent('php', 'controller.stub'); + file_put_contents($controllerOutPath, $controllerContent); + + $logicOutPath = $phpPath . $adminPath . DS . 'logic' . $subPath . DS . $this->value['class_name'] . 'Logic.php'; + $this->checkPath($logicOutPath); + $logicContent = $this->renderContent('php', 'logic.stub'); + file_put_contents($logicOutPath, $logicContent); + + $validateOutPath = $phpPath . $adminPath . DS . 'validate' . $subPath . DS . $this->value['class_name'] . 'Validate.php'; + $this->checkPath($validateOutPath); + $validateContent = $this->renderContent('php', 'validate.stub'); + file_put_contents($validateOutPath, $validateContent); + + $modelOutPath = $phpPath . DS . 'model' . $subPath . DS . $this->value['class_name'] . '.php'; + $this->checkPath($modelOutPath); + $modelContent = $this->renderContent('php', 'model.stub'); + file_put_contents($modelOutPath, $modelContent); + + $sqlOutPath = $sqlPath . DS . 'sql.sql'; + $this->checkPath($sqlOutPath); + $sqlContent = $this->renderContent('sql', 'sql.stub'); + file_put_contents($sqlOutPath, $sqlContent); + } + + /** + * 检查并生成路径 + * @param $path + * @return void + */ + protected function checkPath($path): void + { + if (!is_dir(dirname($path))) { + mkdir(dirname($path), 0777, true); + } + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/utils/code/CodeZip.php b/server/plugin/saiadmin/utils/code/CodeZip.php new file mode 100644 index 0000000..57a395d --- /dev/null +++ b/server/plugin/saiadmin/utils/code/CodeZip.php @@ -0,0 +1,164 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\utils\code; + +use plugin\saiadmin\exception\ApiException; + +/** + * 代码构建 压缩类 + */ +class CodeZip +{ + + /** + * 获取配置文件 + * @return string[] + */ + private static function _getConfig(): array + { + return [ + 'template_path' => base_path().DIRECTORY_SEPARATOR.'plugin'.DIRECTORY_SEPARATOR.'saiadmin'.DIRECTORY_SEPARATOR.'utils'.DIRECTORY_SEPARATOR.'code'.DIRECTORY_SEPARATOR.'stub', + 'generate_path' => runtime_path().DIRECTORY_SEPARATOR.'code_engine'.DIRECTORY_SEPARATOR.'saiadmin', + ]; + } + + /** + * 构造器 + */ + public function __construct() + { + // 读取配置文件 + $config = self::_getConfig(); + + // 清理源目录 + if (is_dir($config['generate_path'])) { + $this->recursiveDelete($config['generate_path']); + } + + // 清理压缩文件 + $zipName = $config['generate_path'].'.zip'; + if (is_file($zipName)) { + unlink($zipName); + } + } + + /** + * 文件压缩 + */ + public function compress(bool $isDownload = false) + { + // 读取配置文件 + $config = self::_getConfig(); + $zipArc = new \ZipArchive; + $zipName = $config['generate_path'].'.zip'; + $dirPath = $config['generate_path']; + if ($zipArc->open($zipName, \ZipArchive::OVERWRITE | \ZipArchive::CREATE) !== true) { + throw new ApiException('无法打开文件,或者文件创建失败'); + } + $this->addFileToZip($dirPath, $zipArc); + $zipArc->close(); + // 是否下载 + if ($isDownload) { + $this->toBinary($zipName); + } else { + return $zipName; + } + } + + /** + * 文件解压 + */ + public function deCompress(string $file, string $dirName) + { + if (!file_exists($file)) { + return false; + } + // zip实例化对象 + $zipArc = new \ZipArchive(); + // 打开文件 + if (!$zipArc->open($file)) { + return false; + } + // 解压文件 + if (!$zipArc->extractTo($dirName)) { + // 关闭 + $zipArc->close(); + return false; + } + return $zipArc->close(); + } + + /** + * 将文件加入到压缩包 + */ + public function addFileToZip($rootPath, $zip) + { + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($rootPath), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + foreach ($files as $name => $file) + { + // Skip directories (they would be added automatically) + if (!$file->isDir()) + { + // Get real and relative path for current file + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen($rootPath) + 1); + + // Add current file to archive + $zip->addFile($filePath, $relativePath); + } + } + } + + /** + * 递归删除目录下所有文件和文件夹 + */ + public function recursiveDelete($dir) + { + // 打开指定目录 + if ($handle = @opendir($dir)) { + while (($file = readdir($handle)) !== false) { + if (($file == ".") || ($file == "..")) { + continue; + } + if (is_dir($dir . '/' . $file)) { + // 递归 + self::recursiveDelete($dir . '/' . $file); + } else { + unlink($dir . '/' . $file); // 删除文件 + } + } + @closedir($handle); + } + rmdir($dir); + } + + /** + * 下载二进制流文件 + */ + public function toBinary(string $fileName) + { + try { + header("Cache-Control: public"); + header("Content-Description: File Transfer"); + header('Content-disposition: attachment; filename=' . basename($fileName)); //文件名 + header("Content-Type: application/zip"); //zip格式的 + header("Content-Transfer-Encoding: binary"); //告诉浏览器,这是二进制文件 + header('Content-Length: ' . filesize($fileName)); //告诉浏览器,文件大小 + if(ob_get_length() > 0) { + ob_clean(); + } + flush(); + @readfile($fileName); + @unlink($fileName); + } catch (\Throwable $th) { + throw new ApiException('系统生成文件错误'); + } + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/php/controller.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/controller.stub new file mode 100644 index 0000000..8bdd85f --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/controller.stub @@ -0,0 +1,137 @@ +logic = new {{class_name}}Logic(); + $this->validate = new {{class_name}}Validate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('{{menu_name}}列表', '{{namespace}}:{{package_name}}:{{business_name}}:index')] + public function index(Request $request): Response + { + $where = $request->more([ +{% for column in columns %} +{% if column.is_query == '2' %} + ['{{column.column_name}}', ''], +{% endif %} +{% endfor %} + ]); +{% if tpl_category == 'single' %} + $query = $this->logic->search($where); +{% if options.relations != null %} + $query->with([ +{% for item in options.relations %} + '{{item.name}}', +{% endfor %} + ]); +{% endif %} + $data = $this->logic->getList($query); +{% endif %} +{% if tpl_category == 'tree' %} + $data = $this->logic->tree($where); +{% endif %} + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('{{menu_name}}读取', '{{namespace}}:{{package_name}}:{{business_name}}:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('{{menu_name}}添加', '{{namespace}}:{{package_name}}:{{business_name}}:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('{{menu_name}}修改', '{{namespace}}:{{package_name}}:{{business_name}}:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('{{menu_name}}删除', '{{namespace}}:{{package_name}}:{{business_name}}:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/php/logic.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/logic.stub new file mode 100644 index 0000000..f4b4237 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/logic.stub @@ -0,0 +1,89 @@ +model = new {{class_name}}(); + } + +{% if tpl_category == 'tree' %} + /** + * 修改数据 + * @param $id + * @param $data + * @return mixed + */ + public function edit($id, $data): mixed + { + if (!isset($data['{{options.tree_parent_id}}'])) { + $data['{{options.tree_parent_id}}'] = 0; + } + if ($data['{{options.tree_parent_id}}'] == $data['{{options.tree_id}}']) { + throw new ApiException('不能设置父级为自身'); + } + return parent::edit($id, $data); + } + + /** + * 删除数据 + * @param $ids + */ + public function destroy($ids): bool + { + $num = $this->model->whereIn('{{options.tree_parent_id}}', $ids)->count(); + if ($num > 0) { + throw new ApiException('该分类下存在子分类,请先删除子分类'); + } else { + return parent::destroy($ids); + } + } + + /** + * 树形数据 + */ + public function tree($where) + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { +{% if stub == 'eloquent' %} + $query->select('{{options.tree_id}}', '{{options.tree_id}} as value', '{{options.tree_name}} as label', '{{options.tree_parent_id}}'); +{% else %} + $query->field('{{options.tree_id}}, {{options.tree_id}} as value, {{options.tree_name}} as label, {{options.tree_parent_id}}'); +{% endif %} + } +{% if options.relations != null %} + $query->with([ +{% for item in options.relations %} + '{{item.name}}', +{% endfor %} + ]); +{% endif %} + $data = $this->getAll($query); + return Helper::makeTree($data); + } + +{% endif %} +} diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/php/model.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/model.stub new file mode 100644 index 0000000..ad03216 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/model.stub @@ -0,0 +1,304 @@ + 'array', +{% endif %} +{% if column.view_type == 'uploadImage' and column.options.limit > 1 %} + '{{column.column_name}}' => 'array', +{% endif %} +{% if column.view_type == 'imagePicker' and column.options.limit > 1 %} + '{{column.column_name}}' => 'array', +{% endif %} +{% if column.view_type == 'uploadFile' and column.options.limit > 1 %} + '{{column.column_name}}' => 'array', +{% endif %} +{% if column.view_type == 'chunkUpload' and column.options.limit > 1 %} + '{{column.column_name}}' => 'array', +{% endif %} +{% endfor %} + ]); + } +{% else %} +{% for column in columns %} +{% if column.view_type == 'inputTag' or column.view_type == 'checkbox' %} + /** + * {{column.column_comment}} 保存数组转换 + */ + public function set{{column.column_name | camel}}Attr($value) + { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + + /** + * {{column.column_comment}} 读取数组转换 + */ + public function get{{column.column_name | camel}}Attr($value) + { + return json_decode($value ?? '', true); + } + +{% endif %} +{% if column.view_type == 'uploadImage' and column.options.limit > 1 %} + /** + * {{column.column_comment}} 保存数组转换 + */ + public function set{{column.column_name | camel}}Attr($value) + { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + + /** + * {{column.column_comment}} 读取数组转换 + */ + public function get{{column.column_name | camel}}Attr($value) + { + return json_decode($value ?? '', true); + } + +{% endif %} +{% if column.view_type == 'imagePicker' and column.options.limit > 1 %} + /** + * {{column.column_comment}} 保存数组转换 + */ + public function set{{column.column_name | camel}}Attr($value) + { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + + /** + * {{column.column_comment}} 读取数组转换 + */ + public function get{{column.column_name | camel}}Attr($value) + { + return json_decode($value ?? '', true); + } + +{% endif %} +{% if column.view_type == 'uploadFile' and column.options.limit > 1 %} + /** + * {{column.column_comment}} 保存数组转换 + */ + public function set{{column.column_name | camel}}Attr($value) + { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + + /** + * {{column.column_comment}} 读取数组转换 + */ + public function get{{column.column_name | camel}}Attr($value) + { + return json_decode($value ?? '', true); + } + +{% endif %} +{% if column.view_type == 'chunkUpload' and column.options.limit > 1 %} + /** + * {{column.column_comment}} 保存数组转换 + */ + public function set{{column.column_name | camel}}Attr($value) + { + return json_encode($value, JSON_UNESCAPED_UNICODE); + } + + /** + * {{column.column_comment}} 读取数组转换 + */ + public function get{{column.column_name | camel}}Attr($value) + { + return json_decode($value ?? '', true); + } + +{% endif %} +{% endfor %} +{% endif %} +{% for column in columns %} +{% if column.is_query == 2 and column.query_type == 'neq' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->where('{{column.column_name}}', '<>', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'gt' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->where('{{column.column_name}}', '>', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'gte' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->where('{{column.column_name}}', '>=', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'lt' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->where('{{column.column_name}}', '<', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'lte' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->where('{{column.column_name}}', '<=', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'like' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->where('{{column.column_name}}', 'like', '%'.$value.'%'); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'in' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->whereIn('{{column.column_name}}', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'notin' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->whereNotIn('{{column.column_name}}', $value); + } + +{% endif %} +{% if column.is_query == 2 and column.query_type == 'between' %} + /** + * {{column.column_comment}} 搜索 + */ + public function search{{column.column_name | camel}}Attr($query, $value) + { + $query->whereBetween('{{column.column_name}}', $value); + } + +{% endif %} +{% endfor %} +{% for item in options.relations %} +{% if item.type == 'belongsTo' %} + /** + * 关联模型 {{item.name}} + */ + public function {{item.name}}() + { + return $this->{{item.type}}({{item.model}}::class, '{{item.localKey}}', '{{item.foreignKey}}'); + } + +{% endif %} +{% if item.type == 'hasOne' or item.type == 'hasMany' %} + /** + * 关联模型 {{item.name}} + */ + public function {{item.name}}() + { + return $this->{{item.type}}({{item.model}}::class, '{{item.localKey}}', '{{item.foreignKey}}'); + } + +{% endif %} +{% if item.type == 'belongsToMany' and stub == 'think' %} + /** + * 关联模型 {{item.name}} + */ + public function {{item.name}}() + { + return $this->{{item.type}}({{item.model}}::class, {{item.table}}::class, '{{item.localKey}}', '{{item.foreignKey}}'); + } + +{% endif %} +{% if item.type == 'belongsToMany' and stub == 'eloquent' %} + /** + * 关联模型 {{item.name}} + */ + public function {{item.name}}() + { + return $this->{{item.type}}({{item.model}}::class, {{item.table}}::class, '{{item.foreignKey}}', '{{item.localKey}}'); + } + +{% endif %} +{% endfor %} +} diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/php/validate.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/validate.stub new file mode 100644 index 0000000..cc2b205 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/php/validate.stub @@ -0,0 +1,58 @@ + 'require', +{% endif %} +{% endfor %} + ]; + + /** + * 定义错误信息 + */ + protected $message = [ +{% for column in columns %} +{% if column.is_required == 2 and column.is_pk != 2 %} + '{{column.column_name}}' => '{{column.column_comment}}必须填写', +{% endif %} +{% endfor %} + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ +{% for column in columns %} +{% if column.is_required == 2 and column.is_pk != 2 %} + '{{column.column_name}}', +{% endif %} +{% endfor %} + ], + 'update' => [ +{% for column in columns %} +{% if column.is_required == 2 and column.is_pk != 2 %} + '{{column.column_name}}', +{% endif %} +{% endfor %} + ], + ]; + +} diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/sql/sql.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/sql/sql.stub new file mode 100644 index 0000000..d071a74 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/sql/sql.stub @@ -0,0 +1,14 @@ +-- 数据库语句-- + +{% for column in tables %} +-- 菜单[{{column.menu_name}}] SQL +INSERT INTO `sa_system_menu`(`parent_id`, `name`, `code`, `slug`, `type`, `path`, `component`, `icon`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES ({{column.belong_menu_id}}, '{{column.menu_name}}', '{{column.namespace}}/{{column.package_name}}/{{column.business_name}}', '', 2, '{{column.package_name}}/{{column.business_name}}', '/plugin/{{column.namespace}}/{{column.package_name}}/{{column.business_name}}/index', 'ri:home-2-line', 100, 2, 2, 2, 2, 2, now(), now()); + +SET @id := LAST_INSERT_ID(); + +INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '列表', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:index', 3, 100, 2, 2, 2, 2, 2, now(), now()); +INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '保存', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:save', 3, 100, 2, 2, 2, 2, 2, now(), now()); +INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '更新', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:update', 3, 100, 2, 2, 2, 2, 2, now(), now()); +INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '读取', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:read', 3, 100, 2, 2, 2, 2, 2, now(), now()); +INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '删除', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:destroy', 3, 100, 2, 2, 2, 2, 2, now(), now()); +{% endfor %} diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/ts/api.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/ts/api.stub new file mode 100644 index 0000000..a04ab5c --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/ts/api.stub @@ -0,0 +1,69 @@ +import request from '@/utils/http' + +/** + * {{menu_name}} API接口 + */ +export default { + /** + * 获取数据列表 + * @param params 搜索参数 + * @returns 数据列表 + */ + list(params: Record) { +{% if tpl_category == 'tree' %} + return request.get({ +{% else %} + return request.get({ +{% endif %} + url: '/{{url_path}}/index', + params + }) + }, + + /** + * 读取数据 + * @param id 数据ID + * @returns 数据详情 + */ + read(id: number | string) { + return request.get({ + url: '/{{url_path}}/read?id=' + id + }) + }, + + /** + * 创建数据 + * @param params 数据参数 + * @returns 执行结果 + */ + save(params: Record) { + return request.post({ + url: '/{{url_path}}/save', + data: params + }) + }, + + /** + * 更新数据 + * @param params 数据参数 + * @returns 执行结果 + */ + update(params: Record) { + return request.put({ + url: '/{{url_path}}/update', + data: params + }) + }, + + /** + * 删除数据 + * @param id 数据ID + * @returns 执行结果 + */ + delete(params: Record) { + return request.del({ + url: '/{{url_path}}/destroy', + data: params + }) + } +} diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/edit-dialog.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/edit-dialog.stub new file mode 100644 index 0000000..a380fa7 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/edit-dialog.stub @@ -0,0 +1,318 @@ + + + diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/index.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/index.stub new file mode 100644 index 0000000..58818fd --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/index.stub @@ -0,0 +1,189 @@ + + + diff --git a/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/table-search.stub b/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/table-search.stub new file mode 100644 index 0000000..8eaa273 --- /dev/null +++ b/server/plugin/saiadmin/utils/code/stub/saiadmin/vue/table-search.stub @@ -0,0 +1,115 @@ + + + diff --git a/server/plugin/saipackage/app/controller/IndexController.php b/server/plugin/saipackage/app/controller/IndexController.php new file mode 100644 index 0000000..d2429ec --- /dev/null +++ b/server/plugin/saipackage/app/controller/IndexController.php @@ -0,0 +1,51 @@ +connection; + $connection->send(new Response(200, [ + 'Content-Type' => 'text/event-stream', + 'Cache-Control' => 'no-cache', + 'Connection' => 'keep-alive', + 'X-Accel-Buffering' => 'no', + 'Access-Control-Allow-Origin' => '*', + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Expose-Headers' => 'Content-Type', + ], "\r\n")); + + // 消息开始 + $connection->send(new ServerSentEvents([ + 'event' => 'message', 'data' => 'start' + ])); + + // 生成器 + $generator = (new Terminal())->exec(); + foreach ($generator as $chunk) { + $connection->send(new ServerSentEvents([ + 'event' => 'message', 'data' => $chunk + ])); + } + + // 关闭链接 + $connection->close(); + } + +} diff --git a/server/plugin/saipackage/app/controller/InstallController.php b/server/plugin/saipackage/app/controller/InstallController.php new file mode 100644 index 0000000..8a8bb4a --- /dev/null +++ b/server/plugin/saipackage/app/controller/InstallController.php @@ -0,0 +1,389 @@ +adminId > 1) { + throw new ApiException('仅超级管理员能够操作'); + } + } + + /** + * 环境检查状态 + */ + static string $ok = 'ok'; + static string $fail = 'fail'; + static string $warn = 'warn'; + + static array $needDependentVersion = [ + 'php' => '8.1.0', + 'saiadmin' => '6.0.0', + 'saipackage' => '6.0.0', + ]; + + /** + * 应用列表 + * @param Request $request + * @return Response + */ + public function index(Request $request): Response + { + $data = Server::installedList(runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR); + + $phpVersion = phpversion(); + $phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion); + $phpVersionNotes = '正常'; + if (!$phpVersionCompare) { + $phpVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['php']; + } + + $saiadminVersion = config('plugin.saiadmin.app.version'); + $saiadminVersionCompare = Version::compare(self::$needDependentVersion['saiadmin'], $saiadminVersion); + $saiadminVersionNotes = '正常'; + if (!$saiadminVersionCompare) { + $saiadminVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saiadmin']; + } + + $saithinkVersion = config('plugin.saipackage.app.version'); + $saithinkVersionCompare = Version::compare(self::$needDependentVersion['saipackage'], $saithinkVersion); + $saithinkVersionNotes = '正常'; + if (!$saithinkVersionCompare) { + $saithinkVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saipackage']; + } + + + return $this->success([ + 'version' => [ + 'php_version' => [ + 'describe' => $phpVersion, + 'state' => $phpVersionCompare ? self::$ok : self::$fail, + 'notes' => $phpVersionNotes, + ], + 'saiadmin_version' => [ + 'describe' => $saiadminVersion, + 'state' => $saiadminVersionCompare ? self::$ok : self::$fail, + 'notes' => $saiadminVersionNotes, + ], + 'saipackage_version' => [ + 'describe' => $saithinkVersion, + 'state' => $saithinkVersionCompare ? self::$ok : self::$fail, + 'notes' => $saithinkVersionNotes, + ], + ], + 'data' => $data + ]); + } + + /** + * 上传插件 + * @param Request $request + * @return Response + * @throws Throwable + */ + public function upload(Request $request): Response + { + $spl_file = current($request->file()); + if (!$spl_file->isValid()) { + return $this->fail('上传文件校验失败'); + } + $config = config('plugin.saipackage.upload', [ + 'size' => 1024 * 1024 * 5, + 'type' => ['zip'] + ]); + if (!in_array($spl_file->getUploadExtension(), $config['type'])) { + return $this->fail('文件格式上传失败,请选择zip格式文件上传'); + } + if ($spl_file->getSize() > $config['size']) { + return $this->fail('文件大小不能超过5M'); + } + $install = new InstallLogic(); + $info = $install->upload($spl_file); + return $this->success($info); + } + + /** + * 安装插件 + * @param Request $request + * @return Response + * @throws Throwable + */ + public function install(Request $request): Response + { + $appName = $request->post("appName", ''); + if (empty($appName)) { + return $this->fail('参数错误'); + } + $install = new InstallLogic($appName); + $info = $install->install(); + UserMenuCache::clearMenuCache(); + return $this->success($info); + } + + /** + * 卸载插件 + * @param Request $request + * @return Response + * @throws Throwable + */ + public function uninstall(Request $request): Response + { + $appName = $request->post("appName", ''); + if (empty($appName)) { + return $this->fail('参数错误'); + } + $install = new InstallLogic($appName); + $install->uninstall(); + UserMenuCache::clearMenuCache(); + return $this->success('卸载插件成功'); + } + + /** + * 重启 + * @param Request $request + * @return Response + */ + public function reload(Request $request): Response + { + Server::restart(); + + return $this->success('重载成功'); + } + + // ========== 商店代理接口 ========== + + /** + * 代理请求封装 + */ + protected function proxyRequest(string $url, string $method = 'GET', ?string $token = null, ?array $postData = null, int $timeout = 10): array + { + $headers = []; + if ($token) { + $headers[] = "Authorization: Bearer {$token}"; + } + if ($postData !== null) { + $headers[] = "Content-Type: application/json"; + } + + $context = stream_context_create([ + 'http' => [ + 'method' => $method, + 'header' => implode("\r\n", $headers), + 'content' => $postData ? json_encode($postData) : null, + 'timeout' => $timeout, + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ]); + + $response = file_get_contents($url, false, $context); + + if ($response === false) { + return ['success' => false, 'message' => '请求失败']; + } + + // 尝试解析 JSON + $data = json_decode($response, true); + if ($data && isset($data['code'])) { + if ($data['code'] === 200) { + return ['success' => true, 'data' => $data['data'] ?? null]; + } + return ['success' => false, 'message' => $data['message'] ?? '请求失败']; + } + + // 非 JSON 响应(可能是文件) + return ['success' => true, 'raw' => $response, 'headers' => $http_response_header ?? []]; + } + + /** + * 获取应用商店列表 + */ + public function appList(Request $request): Response + { + $params = http_build_query([ + 'page' => $request->input('page', 1), + 'limit' => $request->input('limit', 16), + 'price' => $request->input('price', 'all'), + 'type' => $request->input('type', ''), + 'keywords' => $request->input('keywords', ''), + ]); + + $result = $this->proxyRequest("https://saas.saithink.top/api/app/appstore/store/appList?{$params}"); + + return $result['success'] + ? $this->success($result['data']) + : $this->fail($result['message']); + } + + /** + * 获取商店验证码 + */ + public function storeCaptcha(): Response + { + $result = $this->proxyRequest("https://saas.saithink.top/api/app/appstore/index/captcha"); + + return $result['success'] + ? $this->success($result['data']) + : $this->fail($result['message']); + } + + /** + * 商店登录 + */ + public function storeLogin(Request $request): Response + { + $result = $this->proxyRequest( + "https://saas.saithink.top/api/app/appstore/index/login", + 'POST', + null, + [ + 'username' => $request->input('username'), + 'password' => $request->input('password'), + 'code' => $request->input('code'), + 'uuid' => $request->input('uuid'), + ] + ); + + return $result['success'] + ? $this->success($result['data']) + : $this->fail($result['message']); + } + + /** + * 获取商店用户信息 + */ + public function storeUserInfo(Request $request): Response + { + $token = $request->input('token'); + if (empty($token)) { + return $this->fail('未登录'); + } + + $result = $this->proxyRequest( + "https://saas.saithink.top/api/app/appstore/user/info", + 'GET', + $token + ); + + return $result['success'] + ? $this->success($result['data']) + : $this->fail($result['message']); + } + + /** + * 获取已购应用列表 + */ + public function storePurchasedApps(Request $request): Response + { + $token = $request->input('token'); + if (empty($token)) { + return $this->fail('未登录'); + } + + $result = $this->proxyRequest( + "https://saas.saithink.top/api/app/appstore/user/appList", + 'GET', + $token + ); + + return $result['success'] + ? $this->success($result['data']) + : $this->fail($result['message']); + } + + /** + * 获取应用版本列表 + */ + public function storeAppVersions(Request $request): Response + { + $token = $request->input('token'); + $appId = $request->input('app_id'); + + if (empty($token)) { + return $this->fail('未登录'); + } + + $result = $this->proxyRequest( + "https://saas.saithink.top/api/app/appstore/user/versionList?app_id={$appId}", + 'GET', + $token + ); + + return $result['success'] + ? $this->success($result['data']) + : $this->fail($result['message']); + } + + /** + * 下载应用 - 下载并调用 InstallLogic 处理 + */ + public function storeDownloadApp(Request $request): Response + { + $token = $request->input('token'); + $versionId = $request->input('id'); + + if (empty($token)) { + return $this->fail('未登录'); + } + + if (empty($versionId)) { + return $this->fail('版本ID不能为空'); + } + + $result = $this->proxyRequest( + "https://saas.saithink.top/api/app/appstore/user/downloadApp", + 'POST', + $token, + ['id' => (int) $versionId], + 60 + ); + + if (!$result['success']) { + return $this->fail($result['message'] ?? '下载失败'); + } + + if (!isset($result['raw'])) { + return $this->fail('下载失败'); + } + + // 保存临时 zip 文件 + $tempZip = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR . 'downloadTemp' . date('YmdHis') . '.zip'; + if (!is_dir(dirname($tempZip))) { + mkdir(dirname($tempZip), 0755, true); + } + file_put_contents($tempZip, $result['raw']); + + try { + // 调用 InstallLogic 处理 + $install = new InstallLogic(); + $info = $install->uploadFromPath($tempZip); + + return $this->success($info, '下载成功,请在插件列表中安装'); + } catch (Throwable $e) { + @unlink($tempZip); + return $this->fail($e->getMessage()); + } + } +} diff --git a/server/plugin/saipackage/app/functions.php b/server/plugin/saipackage/app/functions.php new file mode 100644 index 0000000..ac9ec9f --- /dev/null +++ b/server/plugin/saipackage/app/functions.php @@ -0,0 +1,6 @@ +installDir = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR; + $this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR; + if (!is_dir($this->installDir)) { + mkdir($this->installDir, 0755, true); + } + if (!is_dir($this->backupsDir)) { + mkdir($this->backupsDir, 0755, true); + } + + if ($appName) { + $this->appName = $appName; + $this->appDir = $this->installDir . $appName . DIRECTORY_SEPARATOR; + } + } + + public function getInstallState() + { + if (!is_dir($this->appDir)) { + return self::UNINSTALLED; + } + $info = $this->getInfo(); + if ($info && isset($info['state'])) { + return $info['state']; + } + + // 目录已存在,但非正常的模块 + return Filesystem::dirIsEmpty($this->appDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED; + } + + /** + * 获取允许覆盖的目录 + * @return string[] + */ + public function getAllowedPath(): array + { + $backend = 'plugin' . DIRECTORY_SEPARATOR . $this->appName; + $frontend = env('FRONTEND_DIR', 'saiadmin-artd') . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'plugin' . DIRECTORY_SEPARATOR . $this->appName; + return [ + $this->appDir . $backend => base_path() . DIRECTORY_SEPARATOR . $backend, + $this->appDir . $frontend => dirname(base_path()) . DIRECTORY_SEPARATOR . $frontend + ]; + } + + /** + * 上传安装 + * @param mixed $file + * @return array 模块的基本信息 + * @throws Throwable + */ + public function upload(mixed $file): array + { + $copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip'; + $file->move($copyTo); + + // 解压 + $copyToDir = Filesystem::unzip($copyTo); + $copyToDir .= DIRECTORY_SEPARATOR; + + // 删除zip + @unlink($file); + @unlink($copyTo); + + // 读取ini + $info = Server::getIni($copyToDir); + if (empty($info['app'])) { + Filesystem::delDir($copyToDir); + // 基本配置不完整 + throw new ApiException('插件的基础配置信息错误'); + } + + + $this->appName = $info['app']; + $this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR; + + $upgrade = false; + if (is_dir($this->appDir)) { + $oldInfo = $this->getInfo(); + if ($oldInfo && !empty($oldInfo['app'])) { + $versions = explode('.', $oldInfo['version']); + if (isset($versions[2])) { + $versions[2]++; + } + $nextVersion = implode('.', $versions); + $upgrade = Version::compare($nextVersion, $info['version']); + if (!$upgrade) { + Filesystem::delDir($copyToDir); + throw new ApiException('插件已经存在'); + } + } + + if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) { + Filesystem::delDir($copyToDir); + // 模块目录被占 + throw new ApiException('该插件的安装目录已经被占用'); + } + } + + $newInfo = ['state' => self::WAIT_INSTALL]; + if ($upgrade) { + $newInfo['update'] = 1; + + // 清理旧版本代码 + Filesystem::delDir($this->appDir); + } + + // 放置新模块 + rename($copyToDir, $this->appDir); + + // 检查新包是否完整 + $this->checkPackage(); + + // 设置为待安装状态 + $this->setInfo($newInfo); + + return $info; + } + + /** + * 从本地 zip 文件路径安装(用于在线下载后安装) + * @param string $zipPath zip 文件完整路径 + * @return array 模块的基本信息 + * @throws Throwable + */ + public function uploadFromPath(string $zipPath): array + { + if (!is_file($zipPath)) { + throw new ApiException('文件不存在'); + } + + // 解压 + $copyToDir = Filesystem::unzip($zipPath); + $copyToDir .= DIRECTORY_SEPARATOR; + + // 删除 zip + @unlink($zipPath); + + // 读取 ini + $info = Server::getIni($copyToDir); + if (empty($info['app'])) { + Filesystem::delDir($copyToDir); + throw new ApiException('插件的基础配置信息错误'); + } + + $this->appName = $info['app']; + $this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR; + + $upgrade = false; + if (is_dir($this->appDir)) { + $oldInfo = $this->getInfo(); + if ($oldInfo && !empty($oldInfo['app'])) { + $versions = explode('.', $oldInfo['version']); + if (isset($versions[2])) { + $versions[2]++; + } + $nextVersion = implode('.', $versions); + $upgrade = Version::compare($nextVersion, $info['version']); + if (!$upgrade) { + Filesystem::delDir($copyToDir); + throw new ApiException('插件已经存在'); + } + } + + if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) { + Filesystem::delDir($copyToDir); + throw new ApiException('该插件的安装目录已经被占用'); + } + } + + $newInfo = ['state' => self::WAIT_INSTALL]; + if ($upgrade) { + $newInfo['update'] = 1; + Filesystem::delDir($this->appDir); + } + + // 放置新模块 + rename($copyToDir, $this->appDir); + + // 检查新包是否完整 + $this->checkPackage(); + + // 设置为待安装状态 + $this->setInfo($newInfo); + + return $info; + } + + + /** + * 安装或更新 + * @return array + * @throws Throwable + */ + public function install(): array + { + $state = $this->getInstallState(); + if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED) { + throw new ApiException('插件已经存在'); + } + + if ($state == self::DEPENDENT_WAIT_INSTALL) { + throw new ApiException('等待依赖安装'); + } + + echo '开始安装[' . $this->appName . ']' . PHP_EOL; + + $info = $this->getInfo(); + + if ($state == self::WAIT_INSTALL) { + echo '安装数据库' . PHP_EOL; + $sql = $this->appDir . 'install.sql'; + Server::importSql($sql); + } + + if (isset($info['update']) && $info['update'] == 1) { + echo '更新数据库' . PHP_EOL; + $sql = $this->appDir . 'update.sql'; + Server::importSql($sql); + + unset($info['update']); + $this->setInfo([], $info); + } + + // 依赖检查 + $this->dependConflictHandle(); + + // 执行安装脚本 + echo '安装文件' . PHP_EOL; + $pathRelation = $this->getAllowedPath(); + Server::installByRelation($pathRelation); + + // 依赖更新 + echo '依赖更新' . PHP_EOL; + $this->dependUpdateHandle(); + + // 清理菜单缓存 + UserMenuCache::clearMenuCache(); + + // 重启后端 + Server::restart(); + + return $info; + } + + /** + * @return void + * @throws Throwable + */ + public function uninstall(): void + { + $state = $this->getInstallState(); + if ($state != self::INSTALLED) { + echo PHP_EOL . '删除插件[' . $this->appName . ']' . PHP_EOL; + $pathRelation = $this->getAllowedPath(); + foreach ($pathRelation as $key => $value) { + if (is_dir($value)) { + Filesystem::delDir($value); + } + } + + // 删除临时目录 + Filesystem::delDir($this->appDir); + + return; + } + + echo '开始卸载[' . $this->appName . ']' . PHP_EOL; + + echo '卸载数据库' . PHP_EOL; + $sql = $this->appDir . 'uninstall.sql'; + Server::importSql($sql); + + echo '备份文件' . PHP_EOL; + $backFiles = []; + $pathRelation = $this->getAllowedPath(); + $index = 1; + foreach ($pathRelation as $key => $value) { + if (is_dir($value)) { + $backFiles[$this->appName . '-' . $index] = $value; + $index++; + } + } + $backupsZip = $this->backupsDir . $this->appName . '-uninstall-' . date('YmdHis') . '.zip'; + Filesystem::zipDir($backFiles, $backupsZip); + + echo '卸载文件' . PHP_EOL; + $pathRelation = $this->getAllowedPath(); + foreach ($pathRelation as $key => $value) { + if (is_dir($value)) { + Filesystem::delDir($value); + } + } + + // 删除临时目录 + Filesystem::delDir($this->appDir); + + // 清理菜单缓存 + UserMenuCache::clearMenuCache(); + + // 重启后端 + Server::restart(); + + } + + /** + * 检查包是否完整 + * @throws Throwable + */ + public function checkPackage(): bool + { + if (!is_dir($this->appDir)) { + throw new ApiException('插件目录不存在'); + } + $info = $this->getInfo(); + $infoKeys = ['app', 'title', 'about', 'author', 'version', 'state']; + foreach ($infoKeys as $value) { + if (!array_key_exists($value, $info)) { + Filesystem::delDir($this->appDir); + throw new ApiException('该插件的基础配置信息不完善'); + } + } + return true; + } + + /** + * 依赖安装完成标记 + * @throws Throwable + */ + public function dependentInstallComplete(string $type): void + { + $info = $this->getInfo(); + if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) { + if ($type == 'npm') { + unset($info['npm_dependent_wait_install']); + } + if ($type == 'composer') { + unset($info['composer_dependent_wait_install']); + } + if ($type == 'all') { + unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install']); + } + if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install'])) { + $info['state'] = self::INSTALLED; + } + $this->setInfo([], $info); + } + } + + /** + * 依赖冲突检查 + * @return bool + * @throws Throwable + */ + public function dependConflictHandle(): bool + { + $info = $this->getInfo(); + if ($info['state'] != self::WAIT_INSTALL && $info['state'] != self::CONFLICT_PENDING) { + return false; + } + + $coverFiles = [];// 要覆盖的文件-备份 + $depends = Server::getDepend($this->appDir); + + $serverDep = new Depends(base_path() . DIRECTORY_SEPARATOR . 'composer.json', 'composer'); + $webDep = new Depends(dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json'); + + // 如果有依赖更新,增加要备份的文件 + if ($depends) { + foreach ($depends as $key => $item) { + if (!$item) { + continue; + } + if ($key == 'require' || $key == 'require-dev') { + $coverFiles[] = base_path() . DIRECTORY_SEPARATOR . 'composer.json'; + continue; + } + if ($key == 'dependencies' || $key == 'devDependencies') { + $coverFiles[] = dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json'; + } + } + } + + // 备份将被覆盖的文件 + if ($coverFiles) { + $backupsZip = $this->backupsDir . $this->appName . '-cover-' . date('YmdHis') . '.zip'; + Filesystem::zip($coverFiles, $backupsZip); + } + + if ($depends) { + $npm = false; + $composer = false; + + // composer config 更新 + $composerConfig = Server::getConfig($this->appDir, 'composerConfig'); + if ($composerConfig) { + $serverDep->setComposerConfig($composerConfig); + } + + foreach ($depends as $key => $item) { + if (!$item) { + continue; + } + if ($key == 'require') { + $composer = true; + $serverDep->addDepends($item, false, true); + } elseif ($key == 'require-dev') { + $composer = true; + $serverDep->addDepends($item, true, true); + } elseif ($key == 'dependencies') { + $npm = true; + $webDep->addDepends($item, false, true); + } elseif ($key == 'devDependencies') { + $npm = true; + $webDep->addDepends($item, true, true); + } + } + if ($npm) { + $info['npm_dependent_wait_install'] = 1; + $info['state'] = self::DEPENDENT_WAIT_INSTALL; + } + if ($composer) { + $info['composer_dependent_wait_install'] = 1; + $info['state'] = self::DEPENDENT_WAIT_INSTALL; + } + if ($info['state'] != self::DEPENDENT_WAIT_INSTALL) { + // 无冲突 + $this->setInfo([ + 'state' => self::INSTALLED, + ]); + } else { + $this->setInfo([], $info); + } + } else { + // 无冲突 + $this->setInfo([ + 'state' => self::INSTALLED, + ]); + } + return true; + } + + /** + * 依赖升级处理 + * @throws Throwable + */ + public function dependUpdateHandle(): void + { + $info = $this->getInfo(); + if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) { + $waitInstall = []; + if (isset($info['composer_dependent_wait_install'])) { + $waitInstall[] = 'composer_dependent_wait_install'; + } + if (isset($info['npm_dependent_wait_install'])) { + $waitInstall[] = 'npm_dependent_wait_install'; + } + if (empty($waitInstall)) { + $this->setInfo([ + 'state' => self::INSTALLED, + ]); + } + } + } + + /** + * 获取模块基本信息 + */ + public function getInfo(): array + { + return Server::getIni($this->appDir); + } + + /** + * 设置模块基本信息 + * @throws Throwable + */ + public function setInfo(array $kv = [], array $arr = []): bool + { + if ($kv) { + $info = $this->getInfo(); + foreach ($kv as $k => $v) { + $info[$k] = $v; + } + return Server::setIni($this->appDir, $info); + } elseif ($arr) { + return Server::setIni($this->appDir, $arr); + } + throw new ApiException('参数错误'); + } +} diff --git a/server/plugin/saipackage/config/app.php b/server/plugin/saipackage/config/app.php new file mode 100644 index 0000000..3b0e896 --- /dev/null +++ b/server/plugin/saipackage/config/app.php @@ -0,0 +1,10 @@ + true, + 'controller_suffix' => 'Controller', + 'controller_reuse' => false, + 'version' => '6.0.1' +]; diff --git a/server/plugin/saipackage/config/autoload.php b/server/plugin/saipackage/config/autoload.php new file mode 100644 index 0000000..64b9337 --- /dev/null +++ b/server/plugin/saipackage/config/autoload.php @@ -0,0 +1,6 @@ + [ + base_path() . '/plugin/saipackage/app/functions.php', + ] +]; \ No newline at end of file diff --git a/server/plugin/saipackage/config/container.php b/server/plugin/saipackage/config/container.php new file mode 100644 index 0000000..34d7f5c --- /dev/null +++ b/server/plugin/saipackage/config/container.php @@ -0,0 +1,2 @@ + \plugin\saiadmin\app\exception\Handler::class, +]; \ No newline at end of file diff --git a/server/plugin/saipackage/config/log.php b/server/plugin/saipackage/config/log.php new file mode 100644 index 0000000..30cd7e0 --- /dev/null +++ b/server/plugin/saipackage/config/log.php @@ -0,0 +1,20 @@ + [ + 'handlers' => [ + [ + 'class' => Monolog\Handler\RotatingFileHandler::class, + 'constructor' => [ + runtime_path() . '/logs/saipackage.log', + 7, + Monolog\Logger::DEBUG, + ], + 'formatter' => [ + 'class' => Monolog\Formatter\LineFormatter::class, + 'constructor' => [null, 'Y-m-d H:i:s', true], + ], + ] + ], + ], +]; diff --git a/server/plugin/saipackage/config/middleware.php b/server/plugin/saipackage/config/middleware.php new file mode 100644 index 0000000..bcaf734 --- /dev/null +++ b/server/plugin/saipackage/config/middleware.php @@ -0,0 +1,6 @@ + [ + ] +]; diff --git a/server/plugin/saipackage/config/process.php b/server/plugin/saipackage/config/process.php new file mode 100644 index 0000000..881ab67 --- /dev/null +++ b/server/plugin/saipackage/config/process.php @@ -0,0 +1,2 @@ + true, + 'middleware' => [], // Static file Middleware +]; diff --git a/server/plugin/saipackage/config/terminal.php b/server/plugin/saipackage/config/terminal.php new file mode 100644 index 0000000..55c6d76 --- /dev/null +++ b/server/plugin/saipackage/config/terminal.php @@ -0,0 +1,79 @@ + [ + // 查看版本的命令 + 'version' => [ + 'npm' => 'npm -v', + 'yarn' => 'yarn -v', + 'pnpm' => 'pnpm -v', + 'node' => 'node -v', + ], + // 测试命令 + 'test' => [ + 'npm' => [ + 'cwd' => public_path() . DIRECTORY_SEPARATOR . 'npm-install-test', + 'command' => 'npm install', + ], + 'yarn' => [ + 'cwd' => public_path() . DIRECTORY_SEPARATOR . 'npm-install-test', + 'command' => 'yarn install', + ], + 'pnpm' => [ + 'cwd' => public_path() . DIRECTORY_SEPARATOR . 'npm-install-test', + 'command' => 'pnpm install', + ], + ], + // 安装 WEB 依赖包 + 'web-install' => [ + 'npm' => [ + 'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'), + 'command' => 'npm install', + ], + 'yarn' => [ + 'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'), + 'command' => 'yarn install', + ], + 'pnpm' => [ + 'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'), + 'command' => 'pnpm install', + ], + ], + // 构建 WEB 端 + 'web-build' => [ + 'npm' => [ + 'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'), + 'command' => 'npm run build', + ], + 'yarn' => [ + 'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'), + 'command' => 'yarn run build', + ], + 'pnpm' => [ + 'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'), + 'command' => 'pnpm run build', + ], + ], + // 设置 NPM 源 + 'set-npm-registry' => [ + 'npm' => 'npm config set registry https://registry.npmjs.org/ && npm config get registry', + 'taobao' => 'npm config set registry https://registry.npmmirror.com/ && npm config get registry', + 'tencent' => 'npm config set registry https://mirrors.cloud.tencent.com/npm/ && npm config get registry' + ], + // 设置 composer 源 + 'set-composer-registry' => [ + 'composer' => 'composer config --unset repos.packagist', + 'tencent' => 'composer config -g repos.packagist composer https://mirrors.cloud.tencent.com/composer/', + 'huawei' => 'composer config -g repos.packagist composer https://mirrors.huaweicloud.com/repository/php/', + 'kkame' => 'composer config -g repos.packagist composer https://packagist.kr', + ], + // 安装 composer 包 + 'composer' => [ + 'update' => [ + 'cwd' => base_path(), + 'command' => 'composer update --no-interaction', + ], + ] + ], +]; \ No newline at end of file diff --git a/server/plugin/saipackage/config/translation.php b/server/plugin/saipackage/config/translation.php new file mode 100644 index 0000000..3bef794 --- /dev/null +++ b/server/plugin/saipackage/config/translation.php @@ -0,0 +1,10 @@ + 'zh_CN', + // Fallback language + 'fallback_locale' => ['zh_CN', 'en'], + // Folder where language files are stored + 'path' => base_path() . "/plugin/saipackage/resource/translations", +]; diff --git a/server/plugin/saipackage/config/upload.php b/server/plugin/saipackage/config/upload.php new file mode 100644 index 0000000..639c309 --- /dev/null +++ b/server/plugin/saipackage/config/upload.php @@ -0,0 +1,5 @@ + ['zip'], + 'size' => 1024 * 1024 * 5, // 5MB +]; \ No newline at end of file diff --git a/server/plugin/saipackage/config/view.php b/server/plugin/saipackage/config/view.php new file mode 100644 index 0000000..8acb91e --- /dev/null +++ b/server/plugin/saipackage/config/view.php @@ -0,0 +1,10 @@ + Raw::class +]; diff --git a/server/webman b/server/webman new file mode 100644 index 0000000..3f55062 --- /dev/null +++ b/server/webman @@ -0,0 +1,71 @@ +#!/usr/bin/env php +load(); + } else { + Dotenv::createMutable(run_path())->load(); + } +} + +$appConfig = require $appConfigFile; +if ($timezone = $appConfig['default_timezone'] ?? '') { + date_default_timezone_set($timezone); +} + +if ($errorReporting = $appConfig['error_reporting'] ?? '') { + error_reporting($errorReporting); +} + +if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) { + require_once __DIR__ . '/support/bootstrap.php'; +} else { + if (class_exists('Support\App')) { + Support\App::loadAllConfig(['route']); + } else { + Config::reload(config_path(), ['route', 'container']); + } +} + +$cli = new Command(); +$cli->setName('webman cli'); +$cli->installInternalCommands(); +if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) { + $cli->installCommands($command_path); +} + +foreach (config('plugin', []) as $firm => $projects) { + if (isset($projects['app'])) { + foreach (['', '/app'] as $app) { + if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) { + $command_path = base_path() . "/plugin/$firm{$app}/$command_str"; + $cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str"); + } + } + } + foreach ($projects as $name => $project) { + if (!is_array($project)) { + continue; + } + $project['command'] ??= []; + array_walk($project['command'], [$cli, 'createCommandInstance']); + } +} + +$cli->run();