Compare commits

...
52 Commits
Author SHA1 Message Date
Wu Clan 1f958fc42a Bump fastapi to remove warning for python 3.14 (#872) 2025-10-21 15:45:05 +08:00
Wu Clan e6e53296a8 Optimize sqlalchemy types to simplify compatibility (#870)
* Optimize sqlalchemy types to simplify compatibility

* Update TimeZone type comment
2025-10-20 17:41:00 +08:00
Wu Clan f533de8abd Bump rtoml and uvicorn to support python 3.14 (#871) 2025-10-20 12:42:28 +08:00
Wu Clan adc6427aab Update the changelog for v1.10.1 (#869) 2025-10-18 18:29:30 +08:00
Wu Clan d034b78b48 Bump dependencies to the latest version (#868) 2025-10-18 18:14:43 +08:00
Wu Clan 6deda906de Update the release changelog workflow (#867) 2025-10-18 18:08:29 +08:00
Wu Clan a063105de8 Fix the volume in the docker compose script (#865)
* Fix the volume in the docker compose script

* Fix the db comment
2025-10-18 15:57:29 +08:00
Wu Clan 02b4215742 Fix tasks related to deleting db logs (#863) 2025-10-18 15:49:55 +08:00
Wu Clan 56b6e2113f Update the changelog for v1.10.0 (#864) 2025-10-18 15:44:17 +08:00
Wu Clan 4fcc78eb94 Update the changelog for v1.9.0 (#862) 2025-10-18 15:41:28 +08:00
Wu Clan b28a4d7673 Update the primary database to postgresql (#829)
* Update the primary database to postgresql

* Delete the prints

* Fix lint

* Update models in code generation

* Delete print

* Fix the salt column type
2025-10-17 23:03:17 +08:00
Wu Clan 27383e4ff5 Fix ctx in validation exception handler (#861)
* Fix ctx in validation exception handler

* Fix lint
2025-10-17 16:21:28 +08:00
Wu Clan bf16f8916d Update the changelog for v1.9.0 (#856) 2025-10-17 13:05:31 +08:00
Wu Clan 75a62ac6aa Fix dict plugin pgsql init script missing (#857)
* Fix dict plugin pgsql init script missing

* Fix lint
2025-10-17 13:05:10 +08:00
Wu Clan bbc4aaa020 Fix the api ninja in code generation (#858) 2025-10-17 13:04:54 +08:00
Wu Clan 89e5d02fe2 Update unique judgment logic for dict data (#859) 2025-10-17 13:04:35 +08:00
Wu Clan a93973af06 Fix the import table cli command (#860) 2025-10-17 13:02:28 +08:00
Wu Clan 15a9527866 Refactor the service layer db session call method (#850)
* Refactor the service layer db session call method

* Fix lint

* Update paging interfaces

* Fix some errors
2025-10-16 12:49:56 +08:00
Wu Clan 9147d35c8c Fix safely of access ctx in exception handlers (#855) 2025-10-16 12:45:01 +08:00
Wu Clan 6a065797f5 Update request state usage to context variable (#853)
* Update request state usage to context variable

* Update context to custom ctx

* Fix the exception interception in opera log

* restore elapsed
2025-10-16 10:23:39 +08:00
Wu Clan dd08775c85 Fix the superuser verify missing auth (#854) 2025-10-14 17:55:54 +08:00
Wu Clan 0cbe635ed8 Update superuser verify to dependency injection (#852)
* Update superuser verify to dependency injection

* Update sql scripts
2025-10-14 17:38:47 +08:00
Wu Clan e923f13e83 Bump fastapi and pydantic to the latest (#851) 2025-10-14 15:18:35 +08:00
Wu Clan 889da744ef Update the changelog for v1.8.3 (#849) 2025-10-13 17:31:23 +08:00
Wu Clan 74df1d1ce2 Bump sqlalchemy crud plus from 0.12.0 to 0.12.1 (#848) 2025-10-13 10:45:49 +08:00
Wu Clan 54e09b68c7 Bump fastapi and sqlalchemy to support python 3.14 (#847) 2025-10-11 13:19:10 +08:00
Wu Clan 5762834744 Update the ruff rules and format the code (#846)
* Update the ruff rules and format the code

* Update the per-file-ignores

* Update the ci

* Update rules

* Fix codes

* Fix pagination

* Update rules
2025-10-10 19:02:49 +08:00
Wu Clan 51354593d0 Bump dependencies and pre-commits (#845) 2025-10-09 13:26:56 +08:00
Wu Clan 8751425e2b Fix SQL script syntax error in config plugin (#843) 2025-09-27 12:33:58 +08:00
Wu Clan 921f05fa19 Fix the pgsql script in dict plugin (#842) 2025-09-27 12:17:45 +08:00
Wu Clan 5754c5449d Update the changelog for v1.8.2 (#841) 2025-09-26 13:32:05 +08:00
Wu Clan 2f535e3510 Update the version number to 1.8.2 (#840) 2025-09-26 13:28:41 +08:00
Wu Clan c25e447530 Update docker and deployment script comments (#839)
* Update docker and deployment script comments

* Update celery stdout logfile conf
2025-09-26 13:18:34 +08:00
Wu Clan 13359b736c Fix the phone filter in the user list (#838) 2025-09-26 12:08:53 +08:00
Wu Clan 500f20d91c Add extension parameters to paging data (#835) 2025-09-26 12:03:41 +08:00
Wu Clan d4de42fb46 Add the refresh token to demo mode exclusion (#834) 2025-09-26 12:03:27 +08:00
Wu Clan 3f0537a727 Simplify the code generation CLI parameters (#837) 2025-09-26 12:03:08 +08:00
Wu Clan d1cdb9e462 Fix the get model object method return logic (#832)
* Fix the get model object method return

* Update the error trigger method

* Delete the error log
2025-09-22 20:51:32 +08:00
Wu Clan 6c029b9be7 Fix code generation template missing Decimal (#830)
* Fix code generation template missing Decimal

* Update template rendering

* Update version number

* Fix lint
2025-09-22 10:46:13 +08:00
Wu Clan df12a0c701 Update the user agent in opera log model (#831) 2025-09-22 10:42:26 +08:00
Wu Clan 0be5685d9f Add version number output to the startup CLI (#820) 2025-09-17 11:11:50 +08:00
Wu Clan 3efca8af37 Update the dict plugin table structure (#817)
* Update the dict plugin table structure

* Update dict sql

* FIx the dict plugin sql
2025-09-15 09:04:30 +08:00
Wu Clan 82b2f808ac Fix the summary in opera log middleware (#816) 2025-09-14 13:22:24 +08:00
Wu Clan f417374f3e Update the login captcha verify logic (#815) 2025-09-12 21:02:21 +08:00
Wu Clan 237534c63e Update the startup progress information display (#812) 2025-09-12 15:44:17 +08:00
morty zhao b525124fdf Fix special character password support in alembic (#811) 2025-09-12 11:07:55 +08:00
Wu Clan 5217c95451 Fix summary default in opera log middleware (#809) 2025-09-11 18:21:40 +08:00
B ea973cac90 Fix pgsql syntax error in code generation (#808)
* Update title field in OperaLogSchemaBase to allow None values

* Update response type in get_all_tables to allow None values in dictionary

* Fix code generation error in PostgreSQL

* Update opera_log.py
2025-09-11 17:53:15 +08:00
Wu Clan 1bedda0e66 Update the offline location information parse (#807) 2025-09-11 14:49:09 +08:00
B c1087fa51d Fix the subprocess ensurepip got stuck in Linux (#806)
* Fix ensurepip_install

* fix code format
2025-09-10 19:42:13 +08:00
Wu Clan cfddf44e7e Update the swagger docs version to dynamic (#805) 2025-09-09 19:36:33 +08:00
Wu Clan 085b32e921 Update the changelog for v1.8.1 (#804) 2025-09-09 19:19:30 +08:00
296 changed files with 5428 additions and 4793 deletions
+2
View File
@@ -18,4 +18,6 @@ jobs:
with:
file: CHANGELOG.md
pull_request: true
commit_summary_template: 'Update the changelog for %s'
args: -c
github_token: ${{ secrets.GH_TOKEN }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
name: lint ${{ matrix.python-version }}
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
fail-fast: false
steps:
- uses: actions/checkout@v4
+4 -7
View File
@@ -2,25 +2,22 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
# - id: check-added-large-files
# - id: end-of-file-fixer
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.12.11
rev: v0.14.0
hooks:
- id: ruff
- id: ruff-check
args:
# https://github.com/astral-sh/ruff-pre-commit/issues/64
- '--config'
- 'backend/.ruff.toml'
- '--fix'
- '--unsafe-fixes'
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.8.14
rev: 0.9.0
hooks:
- id: uv-lock
- id: uv-export
+174
View File
@@ -0,0 +1,174 @@
line-length = 120
preview = true
fix = true
unsafe-fixes = true
show-fixes = true
required-version = ">=0.13.0"
[lint]
select = [
"FAST",
"ANN001",
"ANN201",
"ANN202",
"ANN204",
"ANN205",
"ANN206",
"ASYNC110",
"ASYNC116",
"ASYNC210",
"ASYNC212",
"ASYNC230",
"ASYNC240",
"ASYNC250",
"ASYNC251",
"S310",
"FBT001",
"FBT002",
"B002",
"B005",
"B006",
"B007",
"B008",
"B009",
"B010",
"B013",
"B014",
"B019",
"B020",
"B021",
"B024",
"B025",
"B026",
"B027",
"B039",
"COM",
"C402",
"C403",
"C404",
"C408",
"C410",
"C411",
"C414",
"C416",
"C417",
"C418",
"C419",
"C420",
"DTZ",
"EXE",
"ISC001",
"ISC002",
"ISC003",
"PIE",
"PYI009",
"PYI010",
"PYI011",
"PYI012",
"PYI013",
"PYI016",
"PYI017",
"PYI019",
"PYI020",
"PYI021",
"PYI024",
"PYI026",
"PYI030",
"PYI033",
"PYI034",
"PYI036",
"PYI041",
"PYI042",
"PYI055",
"PYI061",
"PYI062",
"PYI063",
"Q001",
"Q002",
"RSE102",
"RET501",
"RET505",
"RET506",
"RET507",
"RET508",
"SIM101",
"SIM102",
"SIM103",
"SIM107",
"SIM108",
"SIM109",
"SIM110",
"SIM114",
"SIM115",
"SIM201",
"SIM202",
"SIM210",
"SIM211",
"SIM212",
"SIM300",
"SIM401",
"SIM910",
"TID252",
"TC",
"FLY",
"I",
"C901",
"N",
"PERF",
"E",
"W",
"D404",
"D417",
"D419",
"F",
"PGH",
"PLC1901",
"UP",
"FURB",
"RUF",
"TRY",
]
ignore = [
"COM812",
"PGH003",
"RUF001",
"RUF002",
"RUF003",
"RUF006",
"RUF012",
"TRY400",
"TRY003",
"TRY301"
]
[lint.per-file-ignores]
"**/model/*.py" = ["TC003"]
"backend/common/socketio/server.py" = ["ANN001"]
"backend/common/exception/exception_handler.py" = ["ANN202","RUF029"]
[lint.flake8-pytest-style]
parametrize-names-type = "list"
parametrize-values-row-type = "list"
parametrize-values-type = "list"
[lint.flake8-quotes]
inline-quotes = "single"
[lint.flake8-type-checking]
runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"]
[lint.flake8-unused-arguments]
ignore-variadic-names = true
[lint.isort]
case-sensitive = true
lines-between-types = 1
order-by-type = true
[lint.pylint]
allow-dunder-method-names = ["__tablename__", "__table_args__"]
[format]
docstring-code-format = true
preview = true
quote-style = "single"
+287
View File
@@ -1,3 +1,183 @@
<a id="v1.10.1"></a>
# [v1.10.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.1) - 2025-10-18
## What's Changed
* Update the changelog for v1.10.0 by [@wu-clan](https://github.com/wu-clan) in [#864](https://github.com/fastapi-practices/fastapi_best_architecture/pull/864)
* Fix tasks related to deleting db logs by [@wu-clan](https://github.com/wu-clan) in [#863](https://github.com/fastapi-practices/fastapi_best_architecture/pull/863)
* Fix the volume in the docker compose script by [@wu-clan](https://github.com/wu-clan) in [#865](https://github.com/fastapi-practices/fastapi_best_architecture/pull/865)
* Update the release changelog workflow by [@wu-clan](https://github.com/wu-clan) in [#867](https://github.com/fastapi-practices/fastapi_best_architecture/pull/867)
* Bump dependencies to the latest version by [@wu-clan](https://github.com/wu-clan) in [#868](https://github.com/fastapi-practices/fastapi_best_architecture/pull/868)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.0...v1.10.1
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.10.1]
<a id="v1.10.0"></a>
# [v1.10.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.0) - 2025-10-17
## What's Changed
* Fix the import table cli command by [@wu-clan](https://github.com/wu-clan) in [#860](https://github.com/fastapi-practices/fastapi_best_architecture/pull/860)
* Update unique judgment logic for dict data by [@wu-clan](https://github.com/wu-clan) in [#859](https://github.com/fastapi-practices/fastapi_best_architecture/pull/859)
* Fix the api ninja in code generation by [@wu-clan](https://github.com/wu-clan) in [#858](https://github.com/fastapi-practices/fastapi_best_architecture/pull/858)
* Fix dict plugin pgsql init script missing by [@wu-clan](https://github.com/wu-clan) in [#857](https://github.com/fastapi-practices/fastapi_best_architecture/pull/857)
* Update the changelog for v1.9.0 by [@wu-clan](https://github.com/wu-clan) in [#856](https://github.com/fastapi-practices/fastapi_best_architecture/pull/856)
* Fix ctx in validation exception handler by [@wu-clan](https://github.com/wu-clan) in [#861](https://github.com/fastapi-practices/fastapi_best_architecture/pull/861)
* Update the primary database to postgresql by [@wu-clan](https://github.com/wu-clan) in [#829](https://github.com/fastapi-practices/fastapi_best_architecture/pull/829)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.9.0...v1.10.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.10.0]
<a id="v1.9.0"></a>
# [v1.9.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.9.0) - 2025-10-16
## What's Changed
* Update changelog for v1.8.3 by [@wu-clan](https://github.com/wu-clan) in [#849](https://github.com/fastapi-practices/fastapi_best_architecture/pull/849)
* Bump fastapi and pydantic to the latest by [@wu-clan](https://github.com/wu-clan) in [#851](https://github.com/fastapi-practices/fastapi_best_architecture/pull/851)
* Update superuser verify to dependency injection by [@wu-clan](https://github.com/wu-clan) in [#852](https://github.com/fastapi-practices/fastapi_best_architecture/pull/852)
* Fix the superuser verify missing auth by [@wu-clan](https://github.com/wu-clan) in [#854](https://github.com/fastapi-practices/fastapi_best_architecture/pull/854)
* Update request state usage to context variable by [@wu-clan](https://github.com/wu-clan) in [#853](https://github.com/fastapi-practices/fastapi_best_architecture/pull/853)
* Fix safely of access ctx in exception handlers by [@wu-clan](https://github.com/wu-clan) in [#855](https://github.com/fastapi-practices/fastapi_best_architecture/pull/855)
* Refactor the service layer db session call method by [@wu-clan](https://github.com/wu-clan) in [#850](https://github.com/fastapi-practices/fastapi_best_architecture/pull/850)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.3...v1.9.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.9.0]
<a id="v1.8.3"></a>
# [v1.8.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.3) - 2025-10-13
## What's Changed
* Update changelog for v1.8.2 by [@wu-clan](https://github.com/wu-clan) in [#841](https://github.com/fastapi-practices/fastapi_best_architecture/pull/841)
* Fix the pgsql script in dict plugin by [@wu-clan](https://github.com/wu-clan) in [#842](https://github.com/fastapi-practices/fastapi_best_architecture/pull/842)
* Fix SQL script syntax error in config plugin by [@wu-clan](https://github.com/wu-clan) in [#843](https://github.com/fastapi-practices/fastapi_best_architecture/pull/843)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#845](https://github.com/fastapi-practices/fastapi_best_architecture/pull/845)
* Update the ruff rules and format the code by [@wu-clan](https://github.com/wu-clan) in [#846](https://github.com/fastapi-practices/fastapi_best_architecture/pull/846)
* Bump fastapi and sqlalchemy to support python 3.14 by [@wu-clan](https://github.com/wu-clan) in [#847](https://github.com/fastapi-practices/fastapi_best_architecture/pull/847)
* Bump sqlalchemy-crud-plus from 0.12.0 to 0.12.1 by [@wu-clan](https://github.com/wu-clan) in [#848](https://github.com/fastapi-practices/fastapi_best_architecture/pull/848)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.2...v1.8.3
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.8.3]
<a id="v1.8.2"></a>
# [v1.8.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.2) - 2025-09-26
## What's Changed
* Update changelog for v1.8.1 by [@wu-clan](https://github.com/wu-clan) in [#804](https://github.com/fastapi-practices/fastapi_best_architecture/pull/804)
* Update the swagger docs version to dynamic by [@wu-clan](https://github.com/wu-clan) in [#805](https://github.com/fastapi-practices/fastapi_best_architecture/pull/805)
* Fix the subprocess ensurepip got stuck in Linux by [@byte-voyager](https://github.com/byte-voyager) in [#806](https://github.com/fastapi-practices/fastapi_best_architecture/pull/806)
* Update the offline location information parse by [@wu-clan](https://github.com/wu-clan) in [#807](https://github.com/fastapi-practices/fastapi_best_architecture/pull/807)
* Fix pgsql syntax error in code generation by [@byte-voyager](https://github.com/byte-voyager) in [#808](https://github.com/fastapi-practices/fastapi_best_architecture/pull/808)
* Fix summary default in opera log middleware by [@wu-clan](https://github.com/wu-clan) in [#809](https://github.com/fastapi-practices/fastapi_best_architecture/pull/809)
* Fix special character password support in alembic by [@MortyZhaoy](https://github.com/MortyZhaoy) in [#811](https://github.com/fastapi-practices/fastapi_best_architecture/pull/811)
* Update the startup progress information display by [@wu-clan](https://github.com/wu-clan) in [#812](https://github.com/fastapi-practices/fastapi_best_architecture/pull/812)
* Update the login captcha verify logic by [@wu-clan](https://github.com/wu-clan) in [#815](https://github.com/fastapi-practices/fastapi_best_architecture/pull/815)
* Fix the summary in opera log middleware by [@wu-clan](https://github.com/wu-clan) in [#816](https://github.com/fastapi-practices/fastapi_best_architecture/pull/816)
* Update the dict plugin table structure by [@wu-clan](https://github.com/wu-clan) in [#817](https://github.com/fastapi-practices/fastapi_best_architecture/pull/817)
* Add version number output to the startup CLI by [@wu-clan](https://github.com/wu-clan) in [#820](https://github.com/fastapi-practices/fastapi_best_architecture/pull/820)
* Update the user agent in opera log model by [@wu-clan](https://github.com/wu-clan) in [#831](https://github.com/fastapi-practices/fastapi_best_architecture/pull/831)
* Fix code generation template missing Decimal by [@wu-clan](https://github.com/wu-clan) in [#830](https://github.com/fastapi-practices/fastapi_best_architecture/pull/830)
* Fix the get model object method return logic by [@wu-clan](https://github.com/wu-clan) in [#832](https://github.com/fastapi-practices/fastapi_best_architecture/pull/832)
* Simplify the code generation CLI parameters by [@wu-clan](https://github.com/wu-clan) in [#837](https://github.com/fastapi-practices/fastapi_best_architecture/pull/837)
* Add the refresh token to demo mode exclusion by [@wu-clan](https://github.com/wu-clan) in [#834](https://github.com/fastapi-practices/fastapi_best_architecture/pull/834)
* Add extension parameters to paging data by [@wu-clan](https://github.com/wu-clan) in [#835](https://github.com/fastapi-practices/fastapi_best_architecture/pull/835)
* Fix the phone filter in the user list by [@wu-clan](https://github.com/wu-clan) in [#838](https://github.com/fastapi-practices/fastapi_best_architecture/pull/838)
* Update docker and deployment script comments by [@wu-clan](https://github.com/wu-clan) in [#839](https://github.com/fastapi-practices/fastapi_best_architecture/pull/839)
* Update the version number to 1.8.2 by [@wu-clan](https://github.com/wu-clan) in [#840](https://github.com/fastapi-practices/fastapi_best_architecture/pull/840)
## New Contributors
* [@byte-voyager](https://github.com/byte-voyager) made their first contribution in [#806](https://github.com/fastapi-practices/fastapi_best_architecture/pull/806)
* [@MortyZhaoy](https://github.com/MortyZhaoy) made their first contribution in [#811](https://github.com/fastapi-practices/fastapi_best_architecture/pull/811)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.1...v1.8.2
## Contributors
<a href="https://github.com/MortyZhaoy"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FMortyZhaoy.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@MortyZhaoy"></a>
<a href="https://github.com/byte-voyager"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fbyte-voyager.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@byte-voyager"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.8.2]
<a id="v1.8.1"></a>
# [v1.8.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.1) - 2025-09-09
## What's Changed
* Update changelog for v1.8.0 by [@wu-clan](https://github.com/wu-clan) in [#772](https://github.com/fastapi-practices/fastapi_best_architecture/pull/772)
* Optimize the celery crontab expression validation by [@yanlingsishao](https://github.com/yanlingsishao) in [#773](https://github.com/fastapi-practices/fastapi_best_architecture/pull/773)
* Add bulk update interface for config plugin by [@wu-clan](https://github.com/wu-clan) in [#774](https://github.com/fastapi-practices/fastapi_best_architecture/pull/774)
* Optimize email sending config update logic by [@wu-clan](https://github.com/wu-clan) in [#775](https://github.com/fastapi-practices/fastapi_best_architecture/pull/775)
* Add test data SQL script for config plugin by [@wu-clan](https://github.com/wu-clan) in [#776](https://github.com/fastapi-practices/fastapi_best_architecture/pull/776)
* Update the env parameter value in env file by [@wu-clan](https://github.com/wu-clan) in [#777](https://github.com/fastapi-practices/fastapi_best_architecture/pull/777)
* Fix the docker compose env configuration file by [@wu-clan](https://github.com/wu-clan) in [#778](https://github.com/fastapi-practices/fastapi_best_architecture/pull/778)
* Fix redis client not close after plugin parse by [@wu-clan](https://github.com/wu-clan) in [#780](https://github.com/fastapi-practices/fastapi_best_architecture/pull/780)
* Fix the celery beat distributed lock timeout by [@wu-clan](https://github.com/wu-clan) in [#779](https://github.com/fastapi-practices/fastapi_best_architecture/pull/779)
* Fix the plugin model object detection logic by [@wuyao4](https://github.com/wuyao4) in [#782](https://github.com/fastapi-practices/fastapi_best_architecture/pull/782)
* Update the celery task result table creation logic by [@wu-clan](https://github.com/wu-clan) in [#783](https://github.com/fastapi-practices/fastapi_best_architecture/pull/783)
* Fix code generation template params and filename by [@wu-clan](https://github.com/wu-clan) in [#784](https://github.com/fastapi-practices/fastapi_best_architecture/pull/784)
* Fix CLI openapi url in production environment by [@siyue-wang](https://github.com/siyue-wang) in [#785](https://github.com/fastapi-practices/fastapi_best_architecture/pull/785)
* Update the model datetime column type to custom by [@wu-clan](https://github.com/wu-clan) in [#786](https://github.com/fastapi-practices/fastapi_best_architecture/pull/786)
* Update the opera log exception message record by [@wu-clan](https://github.com/wu-clan) in [#788](https://github.com/fastapi-practices/fastapi_best_architecture/pull/788)
* Update the handling of CORS 500 status code by [@wu-clan](https://github.com/wu-clan) in [#789](https://github.com/fastapi-practices/fastapi_best_architecture/pull/789)
* Add the Google OAuth2 login by [@wu-clan](https://github.com/wu-clan) in [#790](https://github.com/fastapi-practices/fastapi_best_architecture/pull/790)
* Fix the tzinfo comparison in the timezone type by [@IAseven](https://github.com/IAseven) in [#787](https://github.com/fastapi-practices/fastapi_best_architecture/pull/787)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#791](https://github.com/fastapi-practices/fastapi_best_architecture/pull/791)
* Update the notice model and pagination by [@wu-clan](https://github.com/wu-clan) in [#792](https://github.com/fastapi-practices/fastapi_best_architecture/pull/792)
* Add the dict type query all interface by [@wu-clan](https://github.com/wu-clan) in [#794](https://github.com/fastapi-practices/fastapi_best_architecture/pull/794)
* Fix sql script syntax error in config plugin by [@wu-clan](https://github.com/wu-clan) in [#793](https://github.com/fastapi-practices/fastapi_best_architecture/pull/793)
* Fix the psycopg dependency naming by [@wu-clan](https://github.com/wu-clan) in [#795](https://github.com/fastapi-practices/fastapi_best_architecture/pull/795)
* Add schema for the tree data by [@wu-clan](https://github.com/wu-clan) in [#796](https://github.com/fastapi-practices/fastapi_best_architecture/pull/796)
* Add the code generator to the CLI by [@wu-clan](https://github.com/wu-clan) in [#798](https://github.com/fastapi-practices/fastapi_best_architecture/pull/798)
* Optimize the code generation interaction logic by [@wu-clan](https://github.com/wu-clan) in [#799](https://github.com/fastapi-practices/fastapi_best_architecture/pull/799)
* Fix console import in main module by [@wu-clan](https://github.com/wu-clan) in [#800](https://github.com/fastapi-practices/fastapi_best_architecture/pull/800)
* Fix the subprocess check call failed in windows by [@yzbf-lin](https://github.com/yzbf-lin) in [#802](https://github.com/fastapi-practices/fastapi_best_architecture/pull/802)
* Fix alembic migration file missing import by [@wu-clan](https://github.com/wu-clan) in [#803](https://github.com/fastapi-practices/fastapi_best_architecture/pull/803)
## New Contributors
* [@yanlingsishao](https://github.com/yanlingsishao) made their first contribution in [#773](https://github.com/fastapi-practices/fastapi_best_architecture/pull/773)
* [@wuyao4](https://github.com/wuyao4) made their first contribution in [#782](https://github.com/fastapi-practices/fastapi_best_architecture/pull/782)
* [@siyue-wang](https://github.com/siyue-wang) made their first contribution in [#785](https://github.com/fastapi-practices/fastapi_best_architecture/pull/785)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.0...v1.8.1
## Contributors
<a href="https://github.com/IAseven"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FIAseven.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@IAseven"></a>
<a href="https://github.com/siyue-wang"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fsiyue-wang.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@siyue-wang"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
<a href="https://github.com/wuyao4"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwuyao4.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wuyao4"></a>
<a href="https://github.com/yanlingsishao"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fyanlingsishao.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@yanlingsishao"></a>
<a href="https://github.com/yzbf-lin"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fyzbf-lin.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@yzbf-lin"></a>
[Changes][v1.8.1]
<a id="v1.8.0"></a>
# [v1.8.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.0) - 2025-08-15
@@ -39,6 +219,12 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.7.0...v1.8.0
## Contributors
<a href="https://github.com/IAseven"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FIAseven.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@IAseven"></a>
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.8.0]
@@ -74,6 +260,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.7.0]
@@ -97,6 +287,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.6.0]
@@ -123,6 +317,11 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
## Contributors
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.5.2]
@@ -139,6 +338,11 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
## Contributors
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.5.1]
@@ -162,6 +366,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.5.0]
@@ -183,6 +391,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.4.3]
@@ -204,6 +416,11 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
## Contributors
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.4.2]
@@ -221,6 +438,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.4.1]
@@ -242,6 +463,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.4.0]
@@ -259,6 +484,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.3.0]
@@ -276,6 +505,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.2.0]
@@ -293,6 +526,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.1.2]
@@ -307,6 +544,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.1.1]
@@ -326,6 +567,11 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
## Contributors
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.1.0]
@@ -350,6 +596,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.0.5]
@@ -378,6 +628,12 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
## Contributors
<a href="https://github.com/PoetryL"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FPoetryL.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@PoetryL"></a>
<a href="https://github.com/ThankCat"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FThankCat.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@ThankCat"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.0.4]
@@ -396,6 +652,11 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
## Contributors
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.0.3]
@@ -411,6 +672,10 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
## Contributors
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.0.2]
@@ -430,6 +695,11 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
## Contributors
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.0.1]
@@ -755,9 +1025,26 @@
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/commits/v1.0.0
## Contributors
<a href="https://github.com/Meepoljdx"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FMeepoljdx.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@Meepoljdx"></a>
<a href="https://github.com/dependabot"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdependabot.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@dependabot"></a>
<a href="https://github.com/dividduang"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdividduang.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@dividduang"></a>
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
<a href="https://github.com/obrua"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fobrua.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@obrua"></a>
<a href="https://github.com/qhp13654398483"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fqhp13654398483.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@qhp13654398483"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
<a href="https://github.com/yshan2028"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fyshan2028.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@yshan2028"></a>
[Changes][v1.0.0]
[v1.10.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.0...v1.10.1
[v1.10.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.9.0...v1.10.0
[v1.9.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.3...v1.9.0
[v1.8.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.2...v1.8.3
[v1.8.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.1...v1.8.2
[v1.8.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.0...v1.8.1
[v1.8.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.7.0...v1.8.0
[v1.7.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
[v1.6.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
+2 -2
View File
@@ -43,7 +43,7 @@ WORKDIR /fba/backend
# === FastAPI server image ===
FROM base_server AS fastapi_server
COPY deploy/backend/fastapi_server.conf /etc/supervisor/conf.d/
COPY deploy/backend/fba_server.conf /etc/supervisor/conf.d/
RUN mkdir -p /var/log/fastapi_server
@@ -54,7 +54,7 @@ CMD ["/usr/local/bin/granian", "main:app", "--interface", "asgi", "--host", "0.0
# === Celery server image ===
FROM base_server AS celery
COPY deploy/backend/celery.conf /etc/supervisor/conf.d/
COPY deploy/backend/fba_celery.conf /etc/supervisor/conf.d/
RUN mkdir -p /var/log/celery
+3 -3
View File
@@ -1,10 +1,10 @@
# Env
ENVIRONMENT='dev'
# Database
DATABASE_TYPE='mysql'
DATABASE_TYPE='postgresql'
DATABASE_HOST='127.0.0.1'
DATABASE_PORT=3306
DATABASE_USER='root'
DATABASE_PORT=5432
DATABASE_USER='postgres'
DATABASE_PASSWORD='123456'
# Redis
REDIS_HOST='127.0.0.1'
-55
View File
@@ -1,55 +0,0 @@
line-length = 120
cache-dir = ".ruff_cache"
target-version = "py310"
unsafe-fixes = true
show-fixes = true
[lint]
select = [
"E",
"F",
"I",
"TC",
# W
"W505",
# PT
"PT018",
# SIM
"SIM101",
"SIM114",
# PGH
"PGH004",
# PL
"PLE1142",
# RUF
"RUF100",
# UP
"UP007"
]
preview = true
ignore = ["FURB101"]
[lint.flake8-pytest-style]
mark-parentheses = false
parametrize-names-type = "list"
parametrize-values-row-type = "list"
parametrize-values-type = "tuple"
[lint.flake8-unused-arguments]
ignore-variadic-names = true
[lint.isort]
lines-between-types = 1
order-by-type = true
[lint.per-file-ignores]
"**/api/v1/*.py" = ["TC"]
"**/model/*.py" = ["TC003"]
"**/model/__init__.py" = ["F401"]
"**/tests/*.py" = ["E402"]
[format]
preview = true
quote-style = "single"
docstring-code-format = true
skip-magic-trailing-comma = false
+1 -3
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.i18n import i18n
__version__ = '1.8.0'
__version__ = '1.10.2'
# 初始化 i18n
+7 -6
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ruff: noqa: F403, F401, I001, RUF100
import asyncio
import os
from logging.config import fileConfig
from alembic import context
@@ -39,10 +37,13 @@ if alembic_config.config_file_name is not None:
target_metadata = MappedBase.metadata
# other values from the config, defined by the needs of env.py,
alembic_config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL.render_as_string(hide_password=False))
alembic_config.set_main_option(
'sqlalchemy.url',
SQLALCHEMY_DATABASE_URL.render_as_string(hide_password=False).replace('%', '%%'),
)
def run_migrations_offline():
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
@@ -71,7 +72,7 @@ def run_migrations_offline():
def do_run_migrations(connection: Connection) -> None:
# 当迁移无变化时,不生成迁移记录
def process_revision_directives(context, revision, directives):
def process_revision_directives(context, revision, directives) -> None: # noqa: ANN001
if alembic_config.cmd_opts.autogenerate:
script = directives[0]
if script.upgrade_ops.is_empty():
+5 -12
View File
@@ -1,29 +1,22 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os.path
from backend.core.path_conf import BASE_PATH
from backend.utils.import_parse import get_model_object
from backend.utils.import_parse import get_model_objects
def get_app_models() -> list[type]:
"""获取 app 所有模型类"""
app_path = os.path.join(BASE_PATH, 'app')
app_path = BASE_PATH / 'app'
list_dirs = os.listdir(app_path)
apps = []
for d in list_dirs:
if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__':
apps.append(d)
apps = [d for d in list_dirs if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__']
objs = []
for app in apps:
module_path = f'backend.app.{app}.model'
obj = get_model_object(module_path)
obj = get_model_objects(module_path)
if obj:
objs.append(obj)
objs.extend(obj)
return objs
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.auth import router as auth_router
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.auth.auth import router as auth_router
+15 -11
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Request, Response
@@ -12,13 +10,16 @@ from backend.app.admin.schema.user import AuthLoginParam
from backend.app.admin.service.auth_service import auth_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证')
async def login_swagger(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken:
token, user = await auth_service.swagger_login(obj=obj)
async def login_swagger(
db: CurrentSessionTransaction, obj: Annotated[HTTPBasicCredentials, Depends()]
) -> GetSwaggerToken:
token, user = await auth_service.swagger_login(db=db, obj=obj)
return GetSwaggerToken(access_token=token, user=user)
@@ -29,21 +30,24 @@ async def login_swagger(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetS
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
)
async def login(
request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
db: CurrentSessionTransaction,
response: Response,
obj: AuthLoginParam,
background_tasks: BackgroundTasks,
) -> ResponseSchemaModel[GetLoginToken]:
data = await auth_service.login(request=request, response=response, obj=obj, background_tasks=background_tasks)
data = await auth_service.login(db=db, response=response, obj=obj, background_tasks=background_tasks)
return response_base.success(data=data)
@router.get('/codes', summary='获取所有授权码', description='适配 vben admin v5', dependencies=[DependsJwtAuth])
async def get_codes(request: Request) -> ResponseSchemaModel[list[str]]:
codes = await auth_service.get_codes(request=request)
async def get_codes(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[str]]:
codes = await auth_service.get_codes(db=db, request=request)
return response_base.success(data=codes)
@router.post('/tokens', summary='刷新 token')
async def refresh_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
data = await auth_service.refresh_token(request=request)
@router.post('/refresh', summary='刷新 token')
async def refresh_token(db: CurrentSession, request: Request) -> ResponseSchemaModel[GetNewToken]:
data = await auth_service.refresh_token(db=db, request=request)
return response_base.success(data=data)
+7 -7
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from uuid import uuid4
from fast_captcha import img_captcha
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends
from fastapi_limiter.depends import RateLimiter
from starlette.concurrency import run_in_threadpool
@@ -18,17 +18,17 @@ router = APIRouter()
summary='获取登录验证码',
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
)
async def get_captcha(request: Request) -> ResponseSchemaModel[GetCaptchaDetail]:
async def get_captcha() -> ResponseSchemaModel[GetCaptchaDetail]:
"""
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是IO密集型任务,使用线程池尽量减少性能损耗
"""
img_type: str = 'base64'
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
ip = request.state.ip
uuid = str(uuid4())
await redis_client.set(
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}',
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{uuid}',
code,
ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
)
data = GetCaptchaDetail(image_type=img_type, image=img)
data = GetCaptchaDetail(uuid=uuid, img_type=img_type, image=img)
return response_base.success(data=data)
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.log.login_log import router as login_log
+9 -11
View File
@@ -1,17 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from backend.app.admin.schema.login_log import DeleteLoginLogParam, GetLoginLogDetail
from backend.app.admin.service.login_log_service import login_log_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@@ -24,14 +22,14 @@ router = APIRouter()
DependsPagination,
],
)
async def get_login_logs_paged(
async def get_login_logs_paginated(
db: CurrentSession,
username: Annotated[str | None, Query(description='用户名')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
ip: Annotated[str | None, Query(description='IP 地址')] = None,
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
log_select = await login_log_service.get_select(username=username, status=status, ip=ip)
page_data = await paging_data(db, log_select)
page_data = await login_log_service.get_list(db=db, username=username, status=status, ip=ip)
return response_base.success(data=page_data)
@@ -43,8 +41,8 @@ async def get_login_logs_paged(
DependsRBAC,
],
)
async def delete_login_logs(obj: DeleteLoginLogParam) -> ResponseModel:
count = await login_log_service.delete(obj=obj)
async def delete_login_logs(db: CurrentSessionTransaction, obj: DeleteLoginLogParam) -> ResponseModel:
count = await login_log_service.delete(db=db, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -58,6 +56,6 @@ async def delete_login_logs(obj: DeleteLoginLogParam) -> ResponseModel:
DependsRBAC,
],
)
async def delete_all_login_logs() -> ResponseModel:
await login_log_service.delete_all()
async def delete_all_login_logs(db: CurrentSessionTransaction) -> ResponseModel:
await login_log_service.delete_all(db=db)
return response_base.success()
+9 -11
View File
@@ -1,17 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from backend.app.admin.schema.opera_log import DeleteOperaLogParam, GetOperaLogDetail
from backend.app.admin.service.opera_log_service import opera_log_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@@ -24,14 +22,14 @@ router = APIRouter()
DependsPagination,
],
)
async def get_opera_logs_paged(
async def get_opera_logs_paginated(
db: CurrentSession,
username: Annotated[str | None, Query(description='用户名')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
ip: Annotated[str | None, Query(description='IP 地址')] = None,
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
log_select = await opera_log_service.get_select(username=username, status=status, ip=ip)
page_data = await paging_data(db, log_select)
page_data = await opera_log_service.get_list(db=db, username=username, status=status, ip=ip)
return response_base.success(data=page_data)
@@ -43,8 +41,8 @@ async def get_opera_logs_paged(
DependsRBAC,
],
)
async def delete_opera_logs(obj: DeleteOperaLogParam) -> ResponseModel:
count = await opera_log_service.delete(obj=obj)
async def delete_opera_logs(db: CurrentSessionTransaction, obj: DeleteOperaLogParam) -> ResponseModel:
count = await opera_log_service.delete(db=db, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -58,6 +56,6 @@ async def delete_opera_logs(obj: DeleteOperaLogParam) -> ResponseModel:
DependsRBAC,
],
)
async def delete_all_opera_logs() -> ResponseModel:
await opera_log_service.delete_all()
async def delete_all_opera_logs(db: CurrentSessionTransaction) -> ResponseModel:
await opera_log_service.delete_all(db=db)
return response_base.success()
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.monitor.online import router as token_router
+5 -14
View File
@@ -1,17 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi import APIRouter, Path, Query
from backend.app.admin.schema.token import GetTokenDetail
from backend.common.enums import StatusType
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.common.security.jwt import DependsJwtAuth, DependsSuperUser, jwt_decode, revoke_token
from backend.core.conf import settings
from backend.database.redis import redis_client
@@ -37,8 +33,8 @@ async def get_sessions(
'browser': extra_info.get('browser', '未知'),
'device': extra_info.get('device', '未知'),
'last_login_time': extra_info.get('last_login_time', '未知'),
}
)
},
),
)
for key in token_keys:
@@ -77,16 +73,11 @@ async def get_sessions(
@router.delete(
'/{pk}',
summary='强制下线',
dependencies=[
Depends(RequestPermission('sys:session:delete')),
DependsRBAC,
],
dependencies=[DependsSuperUser],
)
async def delete_session(
request: Request,
pk: Annotated[int, Path(description='用户 ID')],
session_uuid: Annotated[str, Query(description='会话 UUID')],
) -> ResponseModel:
superuser_verify(request)
await revoke_token(pk, session_uuid)
return response_base.success()
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.common.response.response_schema import ResponseModel, response_base
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from starlette.concurrency import run_in_threadpool
-2
View File
@@ -1,5 +1,3 @@
# !/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
+18 -17
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
@@ -12,12 +10,12 @@ from backend.app.admin.schema.data_rule import (
UpdateDataRuleParam,
)
from backend.app.admin.service.data_rule_service import data_rule_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@@ -37,16 +35,17 @@ async def get_data_rule_model_columns(
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
async def get_all_data_rules() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
data = await data_rule_service.get_all()
async def get_all_data_rules(db: CurrentSession) -> ResponseSchemaModel[list[GetDataRuleDetail]]:
data = await data_rule_service.get_all(db=db)
return response_base.success(data=data)
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
async def get_data_rule(
db: CurrentSession,
pk: Annotated[int, Path(description='数据规则 ID')],
) -> ResponseSchemaModel[GetDataRuleDetail]:
data = await data_rule_service.get(pk=pk)
data = await data_rule_service.get(db=db, pk=pk)
return response_base.success(data=data)
@@ -58,11 +57,11 @@ async def get_data_rule(
DependsPagination,
],
)
async def get_data_rules_paged(
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
async def get_data_rules_paginated(
db: CurrentSession,
name: Annotated[str | None, Query(description='规则名称')] = None,
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
data_rule_select = await data_rule_service.get_select(name=name)
page_data = await paging_data(db, data_rule_select)
page_data = await data_rule_service.get_list(db=db, name=name)
return response_base.success(data=page_data)
@@ -74,8 +73,8 @@ async def get_data_rules_paged(
DependsRBAC,
],
)
async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
await data_rule_service.create(obj=obj)
async def create_data_rule(db: CurrentSessionTransaction, obj: CreateDataRuleParam) -> ResponseModel:
await data_rule_service.create(db=db, obj=obj)
return response_base.success()
@@ -88,9 +87,11 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
],
)
async def update_data_rule(
pk: Annotated[int, Path(description='数据规则 ID')], obj: UpdateDataRuleParam
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='数据规则 ID')],
obj: UpdateDataRuleParam,
) -> ResponseModel:
count = await data_rule_service.update(pk=pk, obj=obj)
count = await data_rule_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -104,8 +105,8 @@ async def update_data_rule(
DependsRBAC,
],
)
async def delete_data_rules(obj: DeleteDataRuleParam) -> ResponseModel:
count = await data_rule_service.delete(obj=obj)
async def delete_data_rules(db: CurrentSessionTransaction, obj: DeleteDataRuleParam) -> ResponseModel:
count = await data_rule_service.delete(db=db, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
+23 -20
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
@@ -13,35 +11,37 @@ from backend.app.admin.schema.data_scope import (
UpdateDataScopeRuleParam,
)
from backend.app.admin.service.data_scope_service import data_scope_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.get('/all', summary='获取所有数据范围', dependencies=[DependsJwtAuth])
async def get_all_data_scope() -> ResponseSchemaModel[list[GetDataScopeDetail]]:
data = await data_scope_service.get_all()
async def get_all_data_scope(db: CurrentSession) -> ResponseSchemaModel[list[GetDataScopeDetail]]:
data = await data_scope_service.get_all(db=db)
return response_base.success(data=data)
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
async def get_data_scope(
db: CurrentSession,
pk: Annotated[int, Path(description='数据范围 ID')],
) -> ResponseSchemaModel[GetDataScopeDetail]:
data = await data_scope_service.get(pk=pk)
data = await data_scope_service.get(db=db, pk=pk)
return response_base.success(data=data)
@router.get('/{pk}/rules', summary='获取数据范围所有规则', dependencies=[DependsJwtAuth])
async def get_data_scope_rules(
db: CurrentSession,
pk: Annotated[int, Path(description='数据范围 ID')],
) -> ResponseSchemaModel[GetDataScopeWithRelationDetail]:
data = await data_scope_service.get_rules(pk=pk)
data = await data_scope_service.get_rules(db=db, pk=pk)
return response_base.success(data=data)
@@ -53,13 +53,12 @@ async def get_data_scope_rules(
DependsPagination,
],
)
async def get_data_scopes_paged(
async def get_data_scopes_paginated(
db: CurrentSession,
name: Annotated[str | None, Query(description='范围名称')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetDataScopeDetail]]:
data_scope_select = await data_scope_service.get_select(name=name, status=status)
page_data = await paging_data(db, data_scope_select)
page_data = await data_scope_service.get_list(db=db, name=name, status=status)
return response_base.success(data=page_data)
@@ -71,8 +70,8 @@ async def get_data_scopes_paged(
DependsRBAC,
],
)
async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
await data_scope_service.create(obj=obj)
async def create_data_scope(db: CurrentSessionTransaction, obj: CreateDataScopeParam) -> ResponseModel:
await data_scope_service.create(db=db, obj=obj)
return response_base.success()
@@ -85,9 +84,11 @@ async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
],
)
async def update_data_scope(
pk: Annotated[int, Path(description='数据范围 ID')], obj: UpdateDataScopeParam
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='数据范围 ID')],
obj: UpdateDataScopeParam,
) -> ResponseModel:
count = await data_scope_service.update(pk=pk, obj=obj)
count = await data_scope_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -102,9 +103,11 @@ async def update_data_scope(
],
)
async def update_data_scope_rules(
pk: Annotated[int, Path(description='数据范围 ID')], rule_ids: UpdateDataScopeRuleParam
):
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='数据范围 ID')],
rule_ids: UpdateDataScopeRuleParam,
) -> ResponseModel:
count = await data_scope_service.update_data_scope_rule(db=db, pk=pk, rule_ids=rule_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -118,8 +121,8 @@ async def update_data_scope_rules(
DependsRBAC,
],
)
async def delete_data_scopes(obj: DeleteDataScopeParam) -> ResponseModel:
count = await data_scope_service.delete(obj=obj)
async def delete_data_scopes(db: CurrentSessionTransaction, obj: DeleteDataScopeParam) -> ResponseModel:
count = await data_scope_service.delete(db=db, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
+15 -11
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
@@ -10,25 +8,29 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseSchemaModel[GetDeptDetail]:
data = await dept_service.get(pk=pk)
async def get_dept(
db: CurrentSession, pk: Annotated[int, Path(description='部门 ID')]
) -> ResponseSchemaModel[GetDeptDetail]:
data = await dept_service.get(db=db, pk=pk)
return response_base.success(data=data)
@router.get('', summary='获取部门树', dependencies=[DependsJwtAuth])
async def get_dept_tree(
db: CurrentSession,
request: Request,
name: Annotated[str | None, Query(description='部门名称')] = None,
leader: Annotated[str | None, Query(description='部门负责人')] = None,
phone: Annotated[str | None, Query(description='联系电话')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[list[GetDeptTree]]:
dept = await dept_service.get_tree(request=request, name=name, leader=leader, phone=phone, status=status)
dept = await dept_service.get_tree(db=db, request=request, name=name, leader=leader, phone=phone, status=status)
return response_base.success(data=dept)
@@ -40,8 +42,8 @@ async def get_dept_tree(
DependsRBAC,
],
)
async def create_dept(obj: CreateDeptParam) -> ResponseModel:
await dept_service.create(obj=obj)
async def create_dept(db: CurrentSessionTransaction, obj: CreateDeptParam) -> ResponseModel:
await dept_service.create(db=db, obj=obj)
return response_base.success()
@@ -53,8 +55,10 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam) -> ResponseModel:
count = await dept_service.update(pk=pk, obj=obj)
async def update_dept(
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam
) -> ResponseModel:
count = await dept_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -68,8 +72,8 @@ async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: Up
DependsRBAC,
],
)
async def delete_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(pk=pk)
async def delete_dept(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(db=db, pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, File, UploadFile
+17 -13
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Path, Query, Request
@@ -10,28 +8,32 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='已适配 vben admin v5', dependencies=[DependsJwtAuth])
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
menu = await menu_service.get_sidebar(request=request)
async def get_user_sidebar(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
menu = await menu_service.get_sidebar(db=db, request=request)
return response_base.success(data=menu)
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseSchemaModel[GetMenuDetail]:
data = await menu_service.get(pk=pk)
async def get_menu(
db: CurrentSession, pk: Annotated[int, Path(description='菜单 ID')]
) -> ResponseSchemaModel[GetMenuDetail]:
data = await menu_service.get(db=db, pk=pk)
return response_base.success(data=data)
@router.get('', summary='获取菜单树', dependencies=[DependsJwtAuth])
async def get_menu_tree(
db: CurrentSession,
title: Annotated[str | None, Query(description='菜单标题')] = None,
status: Annotated[int | None, Query(description='状体')] = None,
) -> ResponseSchemaModel[list[GetMenuTree]]:
menu = await menu_service.get_tree(title=title, status=status)
menu = await menu_service.get_tree(db=db, title=title, status=status)
return response_base.success(data=menu)
@@ -43,8 +45,8 @@ async def get_menu_tree(
DependsRBAC,
],
)
async def create_menu(obj: CreateMenuParam) -> ResponseModel:
await menu_service.create(obj=obj)
async def create_menu(db: CurrentSessionTransaction, obj: CreateMenuParam) -> ResponseModel:
await menu_service.create(db=db, obj=obj)
return response_base.success()
@@ -56,8 +58,10 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam) -> ResponseModel:
count = await menu_service.update(pk=pk, obj=obj)
async def update_menu(
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam
) -> ResponseModel:
count = await menu_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -71,8 +75,8 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
DependsRBAC,
],
)
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
count = await menu_service.delete(pk=pk)
async def delete_menu(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
count = await menu_service.delete(db=db, pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+4 -5
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated, Any
from fastapi import APIRouter, Depends, File, Path, UploadFile
@@ -46,8 +44,9 @@ async def install_plugin(
plugin_name = await plugin_service.install(type=type, file=file, repo_url=repo_url)
return response_base.success(
res=CustomResponse(
code=200, msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务'
)
code=200,
msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务',
),
)
@@ -63,7 +62,7 @@ async def install_plugin(
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
await plugin_service.uninstall(plugin=plugin)
return response_base.success(
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务')
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务'),
)
+32 -24
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
@@ -15,39 +13,44 @@ from backend.app.admin.schema.role import (
UpdateRoleScopeParam,
)
from backend.app.admin.service.role_service import role_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await role_service.get_all()
async def get_all_roles(db: CurrentSession) -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await role_service.get_all(db=db)
return response_base.success(data=data)
@router.get('/{pk}/menus', summary='获取角色菜单树', dependencies=[DependsJwtAuth])
async def get_role_menu_tree(
db: CurrentSession,
pk: Annotated[int, Path(description='角色 ID')],
) -> ResponseSchemaModel[list[GetMenuTree] | None]:
menu = await role_service.get_menu_tree(pk=pk)
menu = await role_service.get_menu_tree(db=db, pk=pk)
return response_base.success(data=menu)
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
async def get_role_scopes(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
rule = await role_service.get_scopes(pk=pk)
async def get_role_scopes(
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
) -> ResponseSchemaModel[list[int]]:
rule = await role_service.get_scopes(db=db, pk=pk)
return response_base.success(data=rule)
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
async def get_role(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
data = await role_service.get(pk=pk)
async def get_role(
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
data = await role_service.get(db=db, pk=pk)
return response_base.success(data=data)
@@ -59,13 +62,12 @@ async def get_role(pk: Annotated[int, Path(description='角色 ID')]) -> Respons
DependsPagination,
],
)
async def get_roles_paged(
async def get_roles_paginated(
db: CurrentSession,
name: Annotated[str | None, Query(description='角色名称')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
role_select = await role_service.get_select(name=name, status=status)
page_data = await paging_data(db, role_select)
page_data = await role_service.get_list(db=db, name=name, status=status)
return response_base.success(data=page_data)
@@ -77,8 +79,8 @@ async def get_roles_paged(
DependsRBAC,
],
)
async def create_role(obj: CreateRoleParam) -> ResponseModel:
await role_service.create(obj=obj)
async def create_role(db: CurrentSessionTransaction, obj: CreateRoleParam) -> ResponseModel:
await role_service.create(db=db, obj=obj)
return response_base.success()
@@ -90,8 +92,10 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam) -> ResponseModel:
count = await role_service.update(pk=pk, obj=obj)
async def update_role(
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam
) -> ResponseModel:
count = await role_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -106,9 +110,11 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
],
)
async def update_role_menus(
pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='角色 ID')],
menu_ids: UpdateRoleMenuParam,
) -> ResponseModel:
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
count = await role_service.update_role_menu(db=db, pk=pk, menu_ids=menu_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -123,9 +129,11 @@ async def update_role_menus(
],
)
async def update_role_scopes(
pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='角色 ID')],
scope_ids: UpdateRoleScopeParam,
) -> ResponseModel:
count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
count = await role_service.update_role_scope(db=db, pk=pk, scope_ids=scope_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -139,8 +147,8 @@ async def update_role_scopes(
DependsRBAC,
],
)
async def delete_roles(obj: DeleteRoleParam) -> ResponseModel:
count = await role_service.delete(obj=obj)
async def delete_roles(db: CurrentSessionTransaction, obj: DeleteRoleParam) -> ResponseModel:
count = await role_service.delete(db=db, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
+44 -32
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query, Request
@@ -14,12 +12,12 @@ from backend.app.admin.schema.user import (
)
from backend.app.admin.service.user_service import user_service
from backend.common.enums import UserPermissionType
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.jwt import DependsJwtAuth, DependsSuperUser
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@@ -32,15 +30,18 @@ async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUs
@router.get('/{pk}', summary='获取用户信息', dependencies=[DependsJwtAuth])
async def get_userinfo(
db: CurrentSession,
pk: Annotated[int, Path(description='用户 ID')],
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
data = await user_service.get_userinfo(pk=pk)
data = await user_service.get_userinfo(db=db, pk=pk)
return response_base.success(data=data)
@router.get('/{pk}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
async def get_user_roles(pk: Annotated[int, Path(description='用户 ID')]) -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await user_service.get_roles(pk=pk)
async def get_user_roles(
db: CurrentSession, pk: Annotated[int, Path(description='用户 ID')]
) -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await user_service.get_roles(db=db, pk=pk)
return response_base.success(data=data)
@@ -52,62 +53,68 @@ async def get_user_roles(pk: Annotated[int, Path(description='用户 ID')]) -> R
DependsPagination,
],
)
async def get_users_paged(
async def get_users_paginated(
db: CurrentSession,
dept: Annotated[int | None, Query(description='部门 ID')] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
phone: Annotated[str | None, Query(description='手机号')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status)
page_data = await paging_data(db, user_select)
page_data = await user_service.get_list(db=db, dept=dept, username=username, phone=phone, status=status)
return response_base.success(data=page_data)
@router.post('', summary='创建用户', dependencies=[DependsRBAC])
async def create_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
await user_service.create(request=request, obj=obj)
data = await user_service.get_userinfo(username=obj.username)
@router.post('', summary='创建用户', dependencies=[DependsSuperUser])
async def create_user(
db: CurrentSessionTransaction, obj: AddUserParam
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
await user_service.create(db=db, obj=obj)
data = await user_service.get_userinfo(db=db, username=obj.username)
return response_base.success(data=data)
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsRBAC])
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsSuperUser])
async def update_user(
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: UpdateUserParam
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='用户 ID')],
obj: UpdateUserParam,
) -> ResponseModel:
count = await user_service.update(request=request, pk=pk, obj=obj)
count = await user_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/{pk}/permissions', summary='更新用户权限', dependencies=[DependsRBAC])
@router.put('/{pk}/permissions', summary='更新用户权限', dependencies=[DependsSuperUser])
async def update_user_permission(
db: CurrentSessionTransaction,
request: Request,
pk: Annotated[int, Path(description='用户 ID')],
type: Annotated[UserPermissionType, Query(description='权限类型')],
) -> ResponseModel:
count = await user_service.update_permission(request=request, pk=pk, type=type)
count = await user_service.update_permission(db=db, request=request, pk=pk, type=type)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/me/password', summary='更新当前用户密码', dependencies=[DependsJwtAuth])
async def update_user_password(request: Request, obj: ResetPasswordParam) -> ResponseModel:
count = await user_service.update_password(request=request, obj=obj)
async def update_user_password(
db: CurrentSessionTransaction, request: Request, obj: ResetPasswordParam
) -> ResponseModel:
count = await user_service.update_password(db=db, request=request, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsRBAC])
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsSuperUser])
async def reset_user_password(
request: Request,
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='用户 ID')],
password: Annotated[str, Body(embed=True, description='新密码')],
) -> ResponseModel:
count = await user_service.reset_password(request=request, pk=pk, password=password)
count = await user_service.reset_password(db=db, pk=pk, password=password)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -115,9 +122,11 @@ async def reset_user_password(
@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth])
async def update_user_nickname(
request: Request, nickname: Annotated[str, Body(embed=True, description='用户昵称')]
db: CurrentSessionTransaction,
request: Request,
nickname: Annotated[str, Body(embed=True, description='用户昵称')],
) -> ResponseModel:
count = await user_service.update_nickname(request=request, nickname=nickname)
count = await user_service.update_nickname(db=db, request=request, nickname=nickname)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -125,9 +134,11 @@ async def update_user_nickname(
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
async def update_user_avatar(
request: Request, avatar: Annotated[str, Body(embed=True, description='用户头像地址')]
db: CurrentSessionTransaction,
request: Request,
avatar: Annotated[str, Body(embed=True, description='用户头像地址')],
) -> ResponseModel:
count = await user_service.update_avatar(request=request, avatar=avatar)
count = await user_service.update_avatar(db=db, request=request, avatar=avatar)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -135,11 +146,12 @@ async def update_user_avatar(
@router.put('/me/email', summary='更新当前用户邮箱', dependencies=[DependsJwtAuth])
async def update_user_email(
db: CurrentSessionTransaction,
request: Request,
captcha: Annotated[str, Body(embed=True, description='邮箱验证码')],
email: Annotated[str, Body(embed=True, description='用户邮箱')],
) -> ResponseModel:
count = await user_service.update_email(request=request, captcha=captcha, email=email)
count = await user_service.update_email(db=db, request=request, captcha=captcha, email=email)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -153,8 +165,8 @@ async def update_user_email(
DependsRBAC,
],
)
async def delete_user(pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.delete(pk=pk)
async def delete_user(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.delete(db=db, pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+3 -5
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,9 +21,9 @@ class CRUDDataRule(CRUDPlus[DataRule]):
"""
return await self.select_model(db, pk)
async def get_list(self, name: str | None) -> Select:
async def get_select(self, name: str | None) -> Select:
"""
获取规则列表
获取规则列表查询表达式
:param name: 规则名称
:return:
+3 -5
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -52,9 +50,9 @@ class CRUDDataScope(CRUDPlus[DataScope]):
"""
return await self.select_models(db)
async def get_list(self, name: str | None, status: int | None) -> Select:
async def get_select(self, name: str | None, status: int | None) -> Select:
"""
获取数据范围列表
获取数据范围列表查询表达式
:param name: 范围名称
:param status: 范围状态
+4 -6
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,7 +20,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param dept_id: 部门 ID
:return:
"""
return await self.select_model_by_column(db, id=dept_id, del_flag=0)
return await self.select_model_by_column(db, id=dept_id, del_flag=False)
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
"""
@@ -32,7 +30,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param name: 部门名称
:return:
"""
return await self.select_model_by_column(db, name=name, del_flag=0)
return await self.select_model_by_column(db, name=name, del_flag=False)
async def get_all(
self,
@@ -54,7 +52,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param status: 部门状态
:return:
"""
filters = {'del_flag': 0}
filters = {'del_flag': False}
if name is not None:
filters['name__like'] = f'%{name}%'
+2 -4
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy import delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,9 +10,9 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
class CRUDLoginLog(CRUDPlus[LoginLog]):
"""登录日志数据库操作类"""
async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取登录日志列表
获取登录日志列表查询表达式
:param username: 用户名
:param status: 登录状态
+1 -3
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
+2 -4
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy import delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,9 +10,9 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
"""操作日志数据库操作类"""
async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取操作日志列表
获取操作日志列表查询表达式
:param username: 用户名
:param status: 操作状态
+3 -5
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -47,9 +45,9 @@ class CRUDRole(CRUDPlus[Role]):
"""
return await self.select_models(db)
async def get_list(self, name: str | None, status: int | None) -> Select:
async def get_select(self, name: str | None, status: int | None) -> Select:
"""
获取角色列表
获取角色列表查询表达式
:param name: 角色名称
:param status: 角色状态
+11 -9
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import bcrypt
from sqlalchemy import select
@@ -183,9 +181,9 @@ class CRUDUser(CRUDPlus[User]):
new_pwd = get_hash_password(password, salt)
return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt})
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
"""
获取用户列表
获取用户列表查询表达式
:param dept: 部门 ID
:param username: 用户名
@@ -200,7 +198,7 @@ class CRUDUser(CRUDPlus[User]):
if username:
filters['username__like'] = f'%{username}%'
if phone:
filters['phone_like'] = f'%{phone}%'
filters['phone__like'] = f'%{phone}%'
if status is not None:
filters['status'] = status
@@ -214,7 +212,7 @@ class CRUDUser(CRUDPlus[User]):
**filters,
)
async def set_super(self, db: AsyncSession, user_id: int, is_super: bool) -> int:
async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int:
"""
设置用户超级管理员状态
@@ -225,7 +223,7 @@ class CRUDUser(CRUDPlus[User]):
"""
return await self.update_model(db, user_id, {'is_superuser': is_super})
async def set_staff(self, db: AsyncSession, user_id: int, is_staff: bool) -> int:
async def set_staff(self, db: AsyncSession, user_id: int, *, is_staff: bool) -> int:
"""
设置用户后台登录状态
@@ -247,7 +245,7 @@ class CRUDUser(CRUDPlus[User]):
"""
return await self.update_model(db, user_id, {'status': status})
async def set_multi_login(self, db: AsyncSession, user_id: int, multi_login: bool) -> int:
async def set_multi_login(self, db: AsyncSession, user_id: int, *, multi_login: bool) -> int:
"""
设置用户多端登录状态
@@ -259,7 +257,11 @@ class CRUDUser(CRUDPlus[User]):
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
async def get_with_relation(
self, db: AsyncSession, *, user_id: int | None = None, username: str | None = None
self,
db: AsyncSession,
*,
user_id: int | None = None,
username: str | None = None,
) -> User | None:
"""
获取用户关联信息
+8 -10
View File
@@ -1,10 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.admin.model.data_rule import DataRule
from backend.app.admin.model.data_scope import DataScope
from backend.app.admin.model.dept import Dept
from backend.app.admin.model.login_log import LoginLog
from backend.app.admin.model.menu import Menu
from backend.app.admin.model.opera_log import OperaLog
from backend.app.admin.model.role import Role
from backend.app.admin.model.user import User
from backend.app.admin.model.data_rule import DataRule as DataRule
from backend.app.admin.model.data_scope import DataScope as DataScope
from backend.app.admin.model.dept import Dept as Dept
from backend.app.admin.model.login_log import LoginLog as LoginLog
from backend.app.admin.model.menu import Menu as Menu
from backend.app.admin.model.opera_log import OperaLog as OperaLog
from backend.app.admin.model.role import Role as Role
from backend.app.admin.model.user import User as User
+7 -8
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_data_scope_rule
@@ -20,14 +19,14 @@ class DataRule(Base):
__tablename__ = 'sys_data_rule'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
column: Mapped[str] = mapped_column(String(20), comment='模型字段名')
name: Mapped[str] = mapped_column(sa.String(500), unique=True, comment='名称')
model: Mapped[str] = mapped_column(sa.String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
column: Mapped[str] = mapped_column(sa.String(20), comment='模型字段名')
operator: Mapped[int] = mapped_column(comment='运算符(0and、1or')
expression: Mapped[int] = mapped_column(
comment='表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in'
comment='表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in',
)
value: Mapped[str] = mapped_column(String(255), comment='规则值')
value: Mapped[str] = mapped_column(sa.String(255), comment='规则值')
# 数据范围规则多对多
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules')
+3 -4
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_data_scope_rule, sys_role_data_scope
@@ -20,7 +19,7 @@ class DataScope(Base):
__tablename__ = 'sys_data_scope'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
name: Mapped[str] = mapped_column(sa.String(50), unique=True, comment='名称')
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
# 数据范围规则多对多
+11 -15
View File
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
import sqlalchemy as sa
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
from sqlalchemy.dialects.postgresql import INTEGER
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
@@ -20,22 +18,20 @@ class Dept(Base):
__tablename__ = 'sys_dept'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), comment='部门名称')
name: Mapped[str] = mapped_column(sa.String(50), comment='部门名称')
sort: Mapped[int] = mapped_column(default=0, comment='排序')
leader: Mapped[str | None] = mapped_column(String(20), default=None, comment='负责人')
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机')
email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱')
leader: Mapped[str | None] = mapped_column(sa.String(20), default=None, comment='负责人')
phone: Mapped[str | None] = mapped_column(sa.String(11), default=None, comment='手机')
email: Mapped[str | None] = mapped_column(sa.String(50), default=None, comment='邮箱')
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
del_flag: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='删除标志(0删除 1存在)'
)
del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在)')
# 父级部门一对多
parent_id: Mapped[int | None] = mapped_column(
BigInteger, ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
sa.BigInteger, sa.ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
)
parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
parent: Mapped[Dept | None] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list[Dept] | None] = relationship(init=False, back_populates='parent')
# 部门用户一对多
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
+18 -18
View File
@@ -1,13 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import DataClassBase, TimeZone, id_key
from backend.common.model import DataClassBase, TimeZone, UniversalText, id_key
from backend.utils.timezone import timezone
@@ -17,19 +14,22 @@ class LoginLog(DataClassBase):
__tablename__ = 'sys_login_log'
id: Mapped[id_key] = mapped_column(init=False)
user_uuid: Mapped[str] = mapped_column(String(50), comment='用户UUID')
username: Mapped[str] = mapped_column(String(20), comment='用户名')
user_uuid: Mapped[str] = mapped_column(sa.String(50), comment='用户UUID')
username: Mapped[str] = mapped_column(sa.String(20), comment='用户名')
status: Mapped[int] = mapped_column(insert_default=0, comment='登录状态(0失败 1成功)')
ip: Mapped[str] = mapped_column(String(50), comment='登录IP地址')
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
ip: Mapped[str] = mapped_column(sa.String(50), comment='登录IP地址')
country: Mapped[str | None] = mapped_column(sa.String(50), comment='国家')
region: Mapped[str | None] = mapped_column(sa.String(50), comment='地区')
city: Mapped[str | None] = mapped_column(sa.String(50), comment='城市')
user_agent: Mapped[str] = mapped_column(sa.String(255), comment='请求头')
os: Mapped[str | None] = mapped_column(sa.String(50), comment='操作系统')
browser: Mapped[str | None] = mapped_column(sa.String(50), comment='浏览器')
device: Mapped[str | None] = mapped_column(sa.String(50), comment='设备')
msg: Mapped[str] = mapped_column(UniversalText, comment='提示消息')
login_time: Mapped[datetime] = mapped_column(TimeZone, comment='登录时间')
created_time: Mapped[datetime] = mapped_column(
TimeZone, init=False, default_factory=timezone.now, comment='创建时间'
TimeZone,
init=False,
default_factory=timezone.now,
comment='创建时间',
)
+33 -25
View File
@@ -1,54 +1,62 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import BigInteger, Column, ForeignKey, Table
import sqlalchemy as sa
from backend.common.model import MappedBase
sys_user_role = Table(
sys_user_role = sa.Table(
'sys_user_role',
MappedBase.metadata,
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('user_id', BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
sa.Column(
'user_id', sa.BigInteger, sa.ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'
),
sa.Column(
'role_id', sa.BigInteger, sa.ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'
),
)
sys_role_menu = Table(
sys_role_menu = sa.Table(
'sys_role_menu',
MappedBase.metadata,
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('menu_id', BigInteger, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
sa.Column(
'role_id', sa.BigInteger, sa.ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'
),
sa.Column(
'menu_id', sa.BigInteger, sa.ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'
),
)
sys_role_data_scope = Table(
sys_role_data_scope = sa.Table(
'sys_role_data_scope',
MappedBase.metadata,
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
Column(
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
sa.Column(
'role_id', sa.BigInteger, sa.ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'
),
sa.Column(
'data_scope_id',
BigInteger,
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
sa.BigInteger,
sa.ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True,
comment='数据范围 ID',
),
)
sys_data_scope_rule = Table(
sys_data_scope_rule = sa.Table(
'sys_data_scope_rule',
MappedBase.metadata,
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column(
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
sa.Column(
'data_scope_id',
BigInteger,
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
sa.BigInteger,
sa.ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True,
comment='数据范围 ID',
),
Column(
sa.Column(
'data_rule_id',
BigInteger,
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
sa.BigInteger,
sa.ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
primary_key=True,
comment='数据规则 ID',
),
+15 -22
View File
@@ -1,16 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
import sqlalchemy as sa
from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_menu
from backend.common.model import Base, id_key
from backend.common.model import Base, UniversalText, id_key
if TYPE_CHECKING:
from backend.app.admin.model import Role
@@ -22,30 +19,26 @@ class Menu(Base):
__tablename__ = 'sys_menu'
id: Mapped[id_key] = mapped_column(init=False)
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
path: Mapped[str | None] = mapped_column(String(200), comment='路由地址')
title: Mapped[str] = mapped_column(sa.String(50), comment='菜单标题')
name: Mapped[str] = mapped_column(sa.String(50), comment='菜单名称')
path: Mapped[str | None] = mapped_column(sa.String(200), comment='路由地址')
sort: Mapped[int] = mapped_column(default=0, comment='排序')
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
icon: Mapped[str | None] = mapped_column(sa.String(100), default=None, comment='菜单图标')
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
component: Mapped[str | None] = mapped_column(sa.String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(sa.String(100), default=None, comment='权限标识')
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
link: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='外链地址'
)
remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
link: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='外链地址')
remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注')
# 父级菜单一对多
parent_id: Mapped[int | None] = mapped_column(
BigInteger, ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
sa.BigInteger, sa.ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
)
parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
parent: Mapped[Menu | None] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list[Menu] | None] = relationship(init=False, back_populates='parent')
# 菜单角色多对多
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
+19 -22
View File
@@ -1,13 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from sqlalchemy import String
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import DataClassBase, TimeZone, id_key
from backend.common.model import DataClassBase, TimeZone, UniversalText, id_key
from backend.utils.timezone import timezone
@@ -17,23 +14,23 @@ class OperaLog(DataClassBase):
__tablename__ = 'sys_opera_log'
id: Mapped[id_key] = mapped_column(init=False)
trace_id: Mapped[str] = mapped_column(String(32), comment='请求跟踪 ID')
username: Mapped[str | None] = mapped_column(String(20), comment='用户名')
method: Mapped[str] = mapped_column(String(20), comment='请求类型')
title: Mapped[str] = mapped_column(String(255), comment='操作模块')
path: Mapped[str] = mapped_column(String(500), comment='请求路径')
ip: Mapped[str] = mapped_column(String(50), comment='IP地址')
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
args: Mapped[str | None] = mapped_column(JSON(), comment='请求参数')
trace_id: Mapped[str] = mapped_column(sa.String(32), comment='请求跟踪 ID')
username: Mapped[str | None] = mapped_column(sa.String(20), comment='用户名')
method: Mapped[str] = mapped_column(sa.String(20), comment='请求类型')
title: Mapped[str] = mapped_column(sa.String(255), comment='操作模块')
path: Mapped[str] = mapped_column(sa.String(500), comment='请求路径')
ip: Mapped[str] = mapped_column(sa.String(50), comment='IP地址')
country: Mapped[str | None] = mapped_column(sa.String(50), comment='国家')
region: Mapped[str | None] = mapped_column(sa.String(50), comment='地区')
city: Mapped[str | None] = mapped_column(sa.String(50), comment='城市')
user_agent: Mapped[str] = mapped_column(sa.String(255), comment='请求头')
os: Mapped[str | None] = mapped_column(sa.String(50), comment='操作系统')
browser: Mapped[str | None] = mapped_column(sa.String(50), comment='浏览器')
device: Mapped[str | None] = mapped_column(sa.String(50), comment='设备')
args: Mapped[str | None] = mapped_column(sa.JSON(), comment='请求参数')
status: Mapped[int] = mapped_column(comment='操作状态(0异常 1正常)')
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
msg: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
code: Mapped[str] = mapped_column(sa.String(20), insert_default='200', comment='操作状态码')
msg: Mapped[str | None] = mapped_column(UniversalText, comment='提示消息')
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms')
opera_time: Mapped[datetime] = mapped_column(TimeZone, comment='操作时间')
created_time: Mapped[datetime] = mapped_column(
+6 -13
View File
@@ -1,16 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import INTEGER, TEXT
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
from backend.common.model import Base, id_key
from backend.common.model import Base, UniversalText, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DataScope, Menu, User
@@ -22,14 +19,10 @@ class Role(Base):
__tablename__ = 'sys_role'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
name: Mapped[str] = mapped_column(sa.String(20), unique=True, comment='角色名称')
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
is_filter_scopes: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='过滤数据权限(0否 1是)'
)
remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
is_filter_scopes: Mapped[bool] = mapped_column(default=True, comment='过滤数据权限(0否 1是)')
remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注')
# 角色用户多对多
users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles')
+14 -22
View File
@@ -1,12 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import VARBINARY, Boolean, ForeignKey, String
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_user_role
@@ -24,24 +22,18 @@ class User(Base):
__tablename__ = 'sys_user'
id: Mapped[id_key] = mapped_column(init=False)
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
uuid: Mapped[str] = mapped_column(sa.String(50), init=False, default_factory=uuid4_str, unique=True)
username: Mapped[str] = mapped_column(sa.String(20), unique=True, index=True, comment='用户名')
nickname: Mapped[str] = mapped_column(sa.String(20), comment='昵称')
password: Mapped[str | None] = mapped_column(sa.String(255), comment='密码')
salt: Mapped[bytes | None] = mapped_column(sa.LargeBinary(255), comment='加密盐')
email: Mapped[str | None] = mapped_column(sa.String(50), default=None, unique=True, index=True, comment='邮箱')
phone: Mapped[str | None] = mapped_column(sa.String(11), default=None, comment='手机号')
avatar: Mapped[str | None] = mapped_column(sa.String(255), default=None, comment='头像')
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
is_superuser: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
)
is_staff: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
)
is_multi_login: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
)
is_superuser: Mapped[bool] = mapped_column(default=False, comment='超级权限(0否 1是)')
is_staff: Mapped[bool] = mapped_column(default=False, comment='后台管理登陆(0否 1是)')
is_multi_login: Mapped[bool] = mapped_column(default=False, comment='是否重复登陆(0否 1是)')
join_time: Mapped[datetime] = mapped_column(TimeZone, init=False, default_factory=timezone.now, comment='注册时间')
last_login_time: Mapped[datetime | None] = mapped_column(
TimeZone, init=False, onupdate=timezone.now, comment='上次登录'
@@ -49,7 +41,7 @@ class User(Base):
# 部门用户一对多
dept_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
sa.BigInteger, sa.ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
)
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+2 -3
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pydantic import Field
from backend.common.schema import SchemaBase
@@ -8,5 +6,6 @@ from backend.common.schema import SchemaBase
class GetCaptchaDetail(SchemaBase):
"""验证码详情"""
image_type: str = Field(description='图片类型')
uuid: str = Field(description='图片唯一标识')
img_type: str = Field(description='图片类型')
image: str = Field(description='图片内容')
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Any
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import Field
+1 -2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Any
@@ -22,6 +20,7 @@ class AuthSchemaBase(SchemaBase):
class AuthLoginParam(AuthSchemaBase):
"""用户登录参数"""
uuid: str = Field(description='验证码 UUID')
captcha: str = Field(description='验证码')
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+37 -36
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import Request, Response
from fastapi.security import HTTPBasicCredentials
from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,6 +9,7 @@ from backend.app.admin.model import User
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
from backend.app.admin.schema.user import AuthLoginParam
from backend.app.admin.service.login_log_service import login_log_service
from backend.common.context import ctx
from backend.common.enums import LoginLogStatusType
from backend.common.exception import errors
from backend.common.i18n import t
@@ -25,7 +24,7 @@ from backend.common.security.jwt import (
password_verify,
)
from backend.core.conf import settings
from backend.database.db import async_db_session, uuid4_str
from backend.database.db import uuid4_str
from backend.database.redis import redis_client
from backend.utils.timezone import timezone
@@ -49,7 +48,6 @@ class AuthService:
if user.password is None:
raise errors.AuthorizationError(msg='用户名或密码有误')
else:
if not password_verify(password, user.password):
raise errors.AuthorizationError(msg='用户名或密码有误')
@@ -58,61 +56,70 @@ class AuthService:
return user
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
async def swagger_login(self, *, db: AsyncSession, obj: HTTPBasicCredentials) -> tuple[str, User]:
"""
Swagger 文档登录
:param db: 数据库会话
:param obj: 登录凭证
:return:
"""
async with async_db_session.begin() as db:
user = await self.user_verify(db, obj.username, obj.password)
await user_dao.update_login_time(db, obj.username)
access_token = await create_access_token(
user.id,
user.is_multi_login,
multi_login=user.is_multi_login,
# extra info
swagger=True,
)
return access_token.access_token, user
async def login(
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
self,
*,
db: AsyncSession,
response: Response,
obj: AuthLoginParam,
background_tasks: BackgroundTasks,
) -> GetLoginToken:
"""
用户登录
:param db: 数据库会话
:param request: 请求对象
:param response: 响应对象
:param obj: 登录参数
:param background_tasks: 后台任务
:return:
"""
async with async_db_session.begin() as db:
user = None
try:
user = await self.user_verify(db, obj.username, obj.password)
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{obj.uuid}')
if not captcha_code:
raise errors.RequestError(msg=t('error.captcha.expired'))
if captcha_code.lower() != obj.captcha.lower():
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{obj.uuid}')
await user_dao.update_login_time(db, obj.username)
await db.refresh(user)
access_token = await create_access_token(
user.id,
user.is_multi_login,
multi_login=user.is_multi_login,
# extra info
username=user.username,
nickname=user.nickname,
last_login_time=timezone.to_str(user.last_login_time),
ip=request.state.ip,
os=request.state.os,
browser=request.state.browser,
device=request.state.device,
ip=ctx.ip,
os=ctx.os,
browser=ctx.browser,
device=ctx.device,
)
refresh_token = await create_refresh_token(
access_token.session_uuid,
user.id,
multi_login=user.is_multi_login,
)
refresh_token = await create_refresh_token(access_token.session_uuid, user.id, user.is_multi_login)
response.set_cookie(
key=settings.COOKIE_REFRESH_TOKEN_KEY,
value=refresh_token.refresh_token,
@@ -128,32 +135,26 @@ class AuthService:
log.error('登陆错误: 用户密码有误')
task = BackgroundTask(
login_log_service.create,
**dict(
db=db,
request=request,
user_uuid=user.uuid if user else uuid4_str(),
username=obj.username,
login_time=timezone.now(),
status=LoginLogStatusType.fail.value,
msg=e.msg,
),
)
raise errors.RequestError(code=e.code, msg=e.msg, background=task)
except Exception as e:
log.error(f'登陆错误: {e}')
raise e
raise
else:
background_tasks.add_task(
login_log_service.create,
**dict(
db=db,
request=request,
user_uuid=user.uuid,
username=obj.username,
login_time=timezone.now(),
status=LoginLogStatusType.success.value,
msg=t('success.login.success'),
),
)
data = GetLoginToken(
access_token=access_token.access_token,
@@ -164,16 +165,16 @@ class AuthService:
return data
@staticmethod
async def get_codes(*, request: Request) -> list[str]:
async def get_codes(*, db: AsyncSession, request: Request) -> list[str]:
"""
获取用户权限码
:param db: 数据库会话
:param request: FastAPI 请求对象
:return:
"""
codes = set()
if request.user.is_superuser:
async with async_db_session.begin() as db:
menus = await menu_dao.get_all(db, None, None)
for menu in menus:
if menu.perms:
@@ -189,10 +190,11 @@ class AuthService:
return list(codes)
@staticmethod
async def refresh_token(*, request: Request) -> GetNewToken:
async def refresh_token(*, db: AsyncSession, request: Request) -> GetNewToken:
"""
刷新令牌
:param db: 数据库会话
:param request: FastAPI 请求对象
:return:
"""
@@ -200,28 +202,27 @@ class AuthService:
if not refresh_token:
raise errors.RequestError(msg='Refresh Token 已过期,请重新登录')
token_payload = jwt_decode(refresh_token)
async with async_db_session() as db:
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
elif not user.status:
if not user.status:
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
if not user.is_multi_login:
if await redis_client.keys(match=f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
if not user.is_multi_login and await redis_client.keys(match=f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
new_token = await create_new_token(
refresh_token,
token_payload.session_uuid,
user.id,
user.is_multi_login,
multi_login=user.is_multi_login,
# extra info
username=user.username,
nickname=user.nickname,
last_login_time=timezone.to_str(user.last_login_time),
ip=request.state.ip,
os=request.state.os,
browser=request.state.browser,
device_type=request.state.device,
ip=ctx.ip,
os=ctx.os,
browser=ctx.browser,
device_type=ctx.device,
)
data = GetNewToken(
access_token=new_token.new_access_token,
+27 -21
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from typing import Any
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_data_rule import data_rule_dao
from backend.app.admin.model import DataRule
@@ -13,8 +12,8 @@ from backend.app.admin.schema.data_rule import (
UpdateDataRuleParam,
)
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.utils.import_parse import dynamic_import_data_model
@@ -22,14 +21,15 @@ class DataRuleService:
"""数据规则服务类"""
@staticmethod
async def get(*, pk: int) -> DataRule:
async def get(*, db: AsyncSession, pk: int) -> DataRule:
"""
获取数据规则详情
:param db: 数据库会话
:param pk: 规则 ID
:return:
"""
async with async_db_session() as db:
data_rule = await data_rule_dao.get(db, pk)
if not data_rule:
raise errors.NotFoundError(msg='数据规则不存在')
@@ -60,64 +60,70 @@ class DataRuleService:
return model_columns
@staticmethod
async def get_select(*, name: str | None) -> Select:
async def get_list(*, db: AsyncSession, name: str | None) -> dict[str, Any]:
"""
获取数据规则列表查询条件
获取数据规则列表
:param db: 数据库会话
:param name: 规则名称
:return:
"""
return await data_rule_dao.get_list(name=name)
data_rule_select = await data_rule_dao.get_select(name=name)
return await paging_data(db, data_rule_select)
@staticmethod
async def get_all() -> Sequence[DataRule]:
"""获取所有数据规则"""
async with async_db_session() as db:
async def get_all(*, db: AsyncSession) -> Sequence[DataRule]:
"""
获取所有数据规则
:param db: 数据库会话
:return:
"""
data_rules = await data_rule_dao.get_all(db)
return data_rules
@staticmethod
async def create(*, obj: CreateDataRuleParam) -> None:
async def create(*, db: AsyncSession, obj: CreateDataRuleParam) -> None:
"""
创建数据规则
:param db: 数据库会话
:param obj: 规则创建参数
:return:
"""
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get_by_name(db, obj.name)
if data_rule:
raise errors.ConflictError(msg='数据规则已存在')
await data_rule_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
"""
更新数据规则
:param db: 数据库会话
:param pk: 规则 ID
:param obj: 规则更新参数
:return:
"""
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get(db, pk)
if not data_rule:
raise errors.NotFoundError(msg='数据规则不存在')
if data_rule.name != obj.name:
if await data_rule_dao.get_by_name(db, obj.name):
if data_rule.name != obj.name and await data_rule_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='数据规则已存在')
count = await data_rule_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, obj: DeleteDataRuleParam) -> int:
async def delete(*, db: AsyncSession, obj: DeleteDataRuleParam) -> int:
"""
批量删除数据规则
:param db: 数据库会话
:param obj: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_rule_dao.delete(db, obj.pks)
return count
+31 -25
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from typing import Any
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.app.admin.model import DataScope
@@ -13,8 +12,8 @@ from backend.app.admin.schema.data_scope import (
UpdateDataScopeRuleParam,
)
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
@@ -22,80 +21,88 @@ class DataScopeService:
"""数据范围服务类"""
@staticmethod
async def get(*, pk: int) -> DataScope:
async def get(*, db: AsyncSession, pk: int) -> DataScope:
"""
获取数据范围详情
:param db: 数据库会话
:param pk: 范围 ID
:return:
"""
async with async_db_session() as db:
data_scope = await data_scope_dao.get(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
return data_scope
@staticmethod
async def get_all() -> Sequence[DataScope]:
"""获取所有数据范围"""
async with async_db_session() as db:
async def get_all(*, db: AsyncSession) -> Sequence[DataScope]:
"""
获取所有数据范围
:param db: 数据库会话
:return:
"""
data_scopes = await data_scope_dao.get_all(db)
return data_scopes
@staticmethod
async def get_rules(*, pk: int) -> DataScope:
async def get_rules(*, db: AsyncSession, pk: int) -> DataScope:
"""
获取数据范围规则
:param db: 数据库会话
:param pk: 范围 ID
:return:
"""
async with async_db_session() as db:
data_scope = await data_scope_dao.get_with_relation(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
return data_scope
@staticmethod
async def get_select(*, name: str | None, status: int | None) -> Select:
async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]:
"""
获取数据范围列表查询条件
获取数据范围列表
:param db: 数据库会话
:param name: 范围名称
:param status: 范围状态
:return:
"""
return await data_scope_dao.get_list(name, status)
data_scope_select = await data_scope_dao.get_select(name, status)
return await paging_data(db, data_scope_select)
@staticmethod
async def create(*, obj: CreateDataScopeParam) -> None:
async def create(*, db: AsyncSession, obj: CreateDataScopeParam) -> None:
"""
创建数据范围
:param db: 数据库会话
:param obj: 数据范围参数
:return:
"""
async with async_db_session.begin() as db:
data_scope = await data_scope_dao.get_by_name(db, obj.name)
if data_scope:
raise errors.ConflictError(msg='数据范围已存在')
await data_scope_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDataScopeParam) -> int:
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int:
"""
更新数据范围
:param db: 数据库会话
:param pk: 范围 ID
:param obj: 数据范围更新参数
:return:
"""
async with async_db_session.begin() as db:
data_scope = await data_scope_dao.get(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
if data_scope.name != obj.name:
if await data_scope_dao.get_by_name(db, obj.name):
if data_scope.name != obj.name and await data_scope_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='数据范围已存在')
count = await data_scope_dao.update(db, pk, obj)
for role in await data_scope.awaitable_attrs.roles:
@@ -104,7 +111,7 @@ class DataScopeService:
return count
@staticmethod
async def update_data_scope_rule(*, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
async def update_data_scope_rule(*, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
"""
更新数据范围规则
@@ -112,19 +119,18 @@ class DataScopeService:
:param rule_ids: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_scope_dao.update_rules(db, pk, rule_ids)
return count
@staticmethod
async def delete(*, obj: DeleteDataScopeParam) -> int:
async def delete(*, db: AsyncSession, obj: DeleteDataScopeParam) -> int:
"""
批量删除数据范围
:param db: 数据库会话
:param obj: 范围 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_scope_dao.delete(db, obj.pks)
for pk in obj.pks:
data_rule = await data_scope_dao.get(db, pk)
+20 -15
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data
@@ -18,14 +16,15 @@ class DeptService:
"""部门服务类"""
@staticmethod
async def get(*, pk: int) -> Dept:
async def get(*, db: AsyncSession, pk: int) -> Dept:
"""
获取部门详情
:param db: 数据库会话
:param pk: 部门 ID
:return:
"""
async with async_db_session() as db:
dept = await dept_dao.get(db, pk)
if not dept:
raise errors.NotFoundError(msg='部门不存在')
@@ -33,11 +32,18 @@ class DeptService:
@staticmethod
async def get_tree(
*, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
*,
db: AsyncSession,
request: Request,
name: str | None,
leader: str | None,
phone: str | None,
status: int | None,
) -> list[dict[str, Any]]:
"""
获取部门树形结构
:param db: 数据库会话
:param request: FastAPI 请求对象
:param name: 部门名称
:param leader: 部门负责人
@@ -45,20 +51,20 @@ class DeptService:
:param status: 状态
:return:
"""
async with async_db_session() as db:
dept_select = await dept_dao.get_all(request, db, name, leader, phone, status)
tree_data = get_tree_data(dept_select)
return tree_data
@staticmethod
async def create(*, obj: CreateDeptParam) -> None:
async def create(*, db: AsyncSession, obj: CreateDeptParam) -> None:
"""
创建部门
:param db: 数据库会话
:param obj: 部门创建参数
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get_by_name(db, obj.name)
if dept:
raise errors.ConflictError(msg='部门名称已存在')
@@ -69,20 +75,19 @@ class DeptService:
await dept_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDeptParam) -> int:
async def update(*, db: AsyncSession, pk: int, obj: UpdateDeptParam) -> int:
"""
更新部门
:param db: 数据库会话
:param pk: 部门 ID
:param obj: 部门更新参数
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get(db, pk)
if not dept:
raise errors.NotFoundError(msg='部门不存在')
if dept.name != obj.name:
if await dept_dao.get_by_name(db, obj.name):
if dept.name != obj.name and await dept_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='部门名称已存在')
if obj.parent_id:
parent_dept = await dept_dao.get(db, obj.parent_id)
@@ -94,14 +99,14 @@ class DeptService:
return count
@staticmethod
async def delete(*, pk: int) -> int:
async def delete(*, db: AsyncSession, pk: int) -> int:
"""
删除部门
:param db: 数据库会话
:param pk: 部门 ID
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get_with_relation(db, pk)
if dept.users:
raise errors.ConflictError(msg='部门下存在用户,无法删除')
+19 -22
View File
@@ -1,37 +1,36 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Any
from fastapi import Request
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_login_log import login_log_dao
from backend.app.admin.schema.login_log import CreateLoginLogParam, DeleteLoginLogParam
from backend.common.context import ctx
from backend.common.log import log
from backend.database.db import async_db_session
from backend.common.pagination import paging_data
class LoginLogService:
"""登录日志服务类"""
@staticmethod
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
"""
获取登录日志列表查询条件
获取登录日志列表
:param db: 数据库会话
:param username: 用户名
:param status: 状态
:param ip: IP 地址
:return:
"""
return await login_log_dao.get_list(username=username, status=status, ip=ip)
log_select = await login_log_dao.get_select(username=username, status=status, ip=ip)
return await paging_data(db, log_select)
@staticmethod
async def create(
*,
db: AsyncSession,
request: Request,
user_uuid: str,
username: str,
login_time: datetime,
@@ -42,7 +41,6 @@ class LoginLogService:
创建登录日志
:param db: 数据库会话
:param request: FastAPI 请求对象
:param user_uuid: 用户 UUID
:param username: 用户名
:param login_time: 登录时间
@@ -55,14 +53,14 @@ class LoginLogService:
user_uuid=user_uuid,
username=username,
status=status,
ip=request.state.ip,
country=request.state.country,
region=request.state.region,
city=request.state.city,
user_agent=request.state.user_agent,
browser=request.state.browser,
os=request.state.os,
device=request.state.device,
ip=ctx.ip,
country=ctx.country,
region=ctx.region,
city=ctx.city,
user_agent=ctx.user_agent,
browser=ctx.browser,
os=ctx.os,
device=ctx.device,
msg=msg,
login_time=login_time,
)
@@ -71,21 +69,20 @@ class LoginLogService:
log.error(f'登录日志创建失败: {e}')
@staticmethod
async def delete(*, obj: DeleteLoginLogParam) -> int:
async def delete(*, db: AsyncSession, obj: DeleteLoginLogParam) -> int:
"""
批量删除登录日志
:param db: 数据库会话
:param obj: 日志 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await login_log_dao.delete(db, obj.pks)
return count
@staticmethod
async def delete_all() -> None:
async def delete_all(*, db: AsyncSession) -> None:
"""清空所有登录日志"""
async with async_db_session.begin() as db:
await login_log_dao.delete_all(db)
+21 -19
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.model import Menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
@@ -18,42 +16,45 @@ class MenuService:
"""菜单服务类"""
@staticmethod
async def get(*, pk: int) -> Menu:
async def get(*, db: AsyncSession, pk: int) -> Menu:
"""
获取菜单详情
:param db: 数据库会话
:param pk: 菜单 ID
:return:
"""
async with async_db_session() as db:
menu = await menu_dao.get(db, menu_id=pk)
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
return menu
@staticmethod
async def get_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
async def get_tree(*, db: AsyncSession, title: str | None, status: int | None) -> list[dict[str, Any]]:
"""
获取菜单树形结构
:param db: 数据库会话
:param title: 菜单标题
:param status: 状态
:return:
"""
async with async_db_session() as db:
menu_data = await menu_dao.get_all(db, title=title, status=status)
menu_tree = get_tree_data(menu_data)
return menu_tree
@staticmethod
async def get_sidebar(*, request: Request) -> list[dict[str, Any] | None]:
async def get_sidebar(*, db: AsyncSession, request: Request) -> list[dict[str, Any] | None]:
"""
获取用户的菜单侧边栏
:param db: 数据库会话
:param request: FastAPI 请求对象
:return:
"""
async with async_db_session() as db:
if request.user.is_superuser:
menu_data = await menu_dao.get_sidebar(db, None)
else:
@@ -61,21 +62,21 @@ class MenuService:
menu_ids = set()
if roles:
for role in roles:
for menu in role.menus:
menu_ids.add(menu.id)
menu_ids.update(menu.id for menu in role.menus)
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
menu_tree = get_vben5_tree_data(menu_data)
return menu_tree
@staticmethod
async def create(*, obj: CreateMenuParam) -> None:
async def create(*, db: AsyncSession, obj: CreateMenuParam) -> None:
"""
创建菜单
:param db: 数据库会话
:param obj: 菜单创建参数
:return:
"""
async with async_db_session.begin() as db:
title = await menu_dao.get_by_title(db, obj.title)
if title:
raise errors.ConflictError(msg='菜单标题已存在')
@@ -86,20 +87,20 @@ class MenuService:
await menu_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateMenuParam) -> int:
async def update(*, db: AsyncSession, pk: int, obj: UpdateMenuParam) -> int:
"""
更新菜单
:param db: 数据库会话
:param pk: 菜单 ID
:param obj: 菜单更新参数
:return:
"""
async with async_db_session.begin() as db:
menu = await menu_dao.get(db, pk)
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
if menu.title != obj.title:
if await menu_dao.get_by_title(db, obj.title):
if menu.title != obj.title and await menu_dao.get_by_title(db, obj.title):
raise errors.ConflictError(msg='菜单标题已存在')
if obj.parent_id:
parent_menu = await menu_dao.get(db, obj.parent_id)
@@ -114,14 +115,15 @@ class MenuService:
return count
@staticmethod
async def delete(*, pk: int) -> int:
async def delete(*, db: AsyncSession, pk: int) -> int:
"""
删除菜单
:param db: 数据库会话
:param pk: 菜单 ID
:return:
"""
async with async_db_session.begin() as db:
children = await menu_dao.get_children(db, pk)
if children:
raise errors.ConflictError(msg='菜单下存在子菜单,无法删除')
+22 -16
View File
@@ -1,65 +1,71 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_opera_log import opera_log_dao
from backend.app.admin.schema.opera_log import CreateOperaLogParam, DeleteOperaLogParam
from backend.database.db import async_db_session
from backend.common.pagination import paging_data
class OperaLogService:
"""操作日志服务类"""
@staticmethod
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
"""
获取操作日志列表查询条件
获取操作日志列表
:param db: 数据库会话
:param username: 用户名
:param status: 状态
:param ip: IP 地址
:return:
"""
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
log_select = await opera_log_dao.get_select(username=username, status=status, ip=ip)
return await paging_data(db, log_select)
@staticmethod
async def create(*, obj: CreateOperaLogParam) -> None:
async def create(*, db: AsyncSession, obj: CreateOperaLogParam) -> None:
"""
创建操作日志
:param db: 数据库会话
:param obj: 操作日志创建参数
:return:
"""
async with async_db_session.begin() as db:
await opera_log_dao.create(db, obj)
@staticmethod
async def bulk_create(*, objs: list[CreateOperaLogParam]) -> None:
async def bulk_create(*, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
"""
批量创建操作日志
:param db: 数据库会话
:param objs: 操作日志创建参数列表
:return:
"""
async with async_db_session.begin() as db:
await opera_log_dao.bulk_create(db, objs)
@staticmethod
async def delete(*, obj: DeleteOperaLogParam) -> int:
async def delete(*, db: AsyncSession, obj: DeleteOperaLogParam) -> int:
"""
批量删除操作日志
:param db: 数据库会话
:param obj: 日志 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await opera_log_dao.delete(db, obj.pks)
return count
@staticmethod
async def delete_all() -> None:
"""清空所有操作日志"""
async with async_db_session.begin() as db:
async def delete_all(*, db: AsyncSession) -> None:
"""
清空所有操作日志
:param db: 数据库会话
:return:
"""
await opera_log_dao.delete_all(db)
+12 -16
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import json
import os
@@ -8,6 +6,8 @@ import zipfile
from typing import Any
import anyio
from fastapi import UploadFile
from backend.common.enums import PluginType, StatusType
@@ -26,14 +26,10 @@ class PluginService:
@staticmethod
async def get_all() -> list[dict[str, Any]]:
"""获取所有插件"""
keys = []
result = []
async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*'):
keys.append(key)
keys = [key async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*')]
for info in await redis_client.mget(*keys):
result.append(json.loads(info))
result = [json.loads(info) for info in await redis_client.mget(*keys)]
return result
@@ -61,24 +57,24 @@ class PluginService:
return await install_git_plugin(repo_url)
@staticmethod
async def uninstall(*, plugin: str):
async def uninstall(*, plugin: str) -> None:
"""
卸载插件
:param plugin: 插件名称
:return:
"""
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
if not os.path.exists(plugin_dir):
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
if not await plugin_dir.exists():
raise errors.NotFoundError(msg='插件不存在')
await uninstall_requirements_async(plugin)
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
bacup_dir = PLUGIN_DIR / f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup'
shutil.move(plugin_dir, bacup_dir)
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
@staticmethod
async def update_status(*, plugin: str):
async def update_status(*, plugin: str) -> None:
"""
更新插件状态
@@ -107,8 +103,8 @@ class PluginService:
:param plugin: 插件名称
:return:
"""
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
if not os.path.exists(plugin_dir):
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
if not await plugin_dir.exists():
raise errors.NotFoundError(msg='插件不存在')
bio = io.BytesIO()
@@ -117,7 +113,7 @@ class PluginService:
dirs[:] = [d for d in dirs if d != '__pycache__']
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, start=plugin_dir)
arcname = os.path.relpath(file_path, start=plugin_dir) # noqa: ASYNC240
zf.write(file_path, os.path.join(plugin, arcname))
bio.seek(0)
+42 -29
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any, Sequence
from collections.abc import Sequence
from typing import Any
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.app.admin.crud.crud_menu import menu_dao
@@ -16,8 +15,8 @@ from backend.app.admin.schema.role import (
UpdateRoleScopeParam,
)
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data
@@ -26,46 +25,55 @@ class RoleService:
"""角色服务类"""
@staticmethod
async def get(*, pk: int) -> Role:
async def get(*, db: AsyncSession, pk: int) -> Role:
"""
获取角色详情
:param db: 数据库会话
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
return role
@staticmethod
async def get_all() -> Sequence[Role]:
"""获取所有角色"""
async with async_db_session() as db:
async def get_all(*, db: AsyncSession) -> Sequence[Role]:
"""
获取所有角色
:param db: 数据库会话
:return:
"""
roles = await role_dao.get_all(db)
return roles
@staticmethod
async def get_select(*, name: str | None, status: int | None) -> Select:
async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]:
"""
获取角色列表查询条件
获取角色列表
:param db: 数据库会话
:param name: 角色名称
:param status: 状态
:return:
"""
return await role_dao.get_list(name=name, status=status)
role_select = await role_dao.get_select(name=name, status=status)
return await paging_data(db, role_select)
@staticmethod
async def get_menu_tree(*, pk: int) -> list[dict[str, Any] | None]:
async def get_menu_tree(*, db: AsyncSession, pk: int) -> list[dict[str, Any] | None]:
"""
获取角色的菜单树形结构
:param db: 数据库会话
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
@@ -73,14 +81,15 @@ class RoleService:
return menu_tree
@staticmethod
async def get_scopes(*, pk: int) -> list[int]:
async def get_scopes(*, db: AsyncSession, pk: int) -> list[int]:
"""
获取角色数据范围列表
:param db: 数据库会话
:param pk:
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
@@ -88,34 +97,35 @@ class RoleService:
return scope_ids
@staticmethod
async def create(*, obj: CreateRoleParam) -> None:
async def create(*, db: AsyncSession, obj: CreateRoleParam) -> None:
"""
创建角色
:param db: 数据库会话
:param obj: 角色创建参数
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get_by_name(db, obj.name)
if role:
raise errors.ConflictError(msg='角色已存在')
await role_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateRoleParam) -> int:
async def update(*, db: AsyncSession, pk: int, obj: UpdateRoleParam) -> int:
"""
更新角色
:param db: 数据库会话
:param pk: 角色 ID
:param obj: 角色更新参数
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
if role.name != obj.name:
if await role_dao.get_by_name(db, obj.name):
if role.name != obj.name and await role_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='角色已存在')
count = await role_dao.update(db, pk, obj)
for user in await role.awaitable_attrs.users:
@@ -123,15 +133,16 @@ class RoleService:
return count
@staticmethod
async def update_role_menu(*, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
async def update_role_menu(*, db: AsyncSession, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
"""
更新角色菜单
:param db: 数据库会话
:param pk: 角色 ID
:param menu_ids: 菜单 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
@@ -145,15 +156,16 @@ class RoleService:
return count
@staticmethod
async def update_role_scope(*, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
async def update_role_scope(*, db: AsyncSession, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
"""
更新角色数据范围
:param db: 数据库会话
:param pk: 角色 ID
:param scope_ids: 权限规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
@@ -167,14 +179,15 @@ class RoleService:
return count
@staticmethod
async def delete(*, obj: DeleteRoleParam) -> int:
async def delete(*, db: AsyncSession, obj: DeleteRoleParam) -> int:
"""
批量删除角色
:param db: 数据库会话
:param obj: 角色 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await role_dao.delete(db, obj.pks)
for pk in obj.pks:
role = await role_dao.get(db, pk)
+42 -47
View File
@@ -1,11 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random
from typing import Sequence
from collections.abc import Sequence
from typing import Any
from fastapi import Request
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.crud.crud_role import role_dao
@@ -16,12 +15,13 @@ from backend.app.admin.schema.user import (
ResetPasswordParam,
UpdateUserParam,
)
from backend.common.context import ctx
from backend.common.enums import UserPermissionType
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.common.response.response_code import CustomErrorCode
from backend.common.security.jwt import get_token, jwt_decode, password_verify, superuser_verify
from backend.common.security.jwt import get_token, jwt_decode, password_verify
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
@@ -29,61 +29,61 @@ class UserService:
"""用户服务类"""
@staticmethod
async def get_userinfo(*, pk: int | None = None, username: str | None = None) -> User:
async def get_userinfo(*, db: AsyncSession, pk: int | None = None, username: str | None = None) -> User:
"""
获取用户信息
:param db: 数据库会话
:param pk: 用户 ID
:param username: 用户名
:return:
"""
async with async_db_session() as db:
user = await user_dao.get_with_relation(db, user_id=pk, username=username)
if not user:
raise errors.NotFoundError(msg='用户不存在')
return user
@staticmethod
async def get_roles(*, pk: int) -> Sequence[Role]:
async def get_roles(*, db: AsyncSession, pk: int) -> Sequence[Role]:
"""
获取用户所有角色
:param db: 数据库会话
:param pk: 用户 ID
:return:
"""
async with async_db_session() as db:
user = await user_dao.get_with_relation(db, user_id=pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
return user.roles
@staticmethod
async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
async def get_list(*, db: AsyncSession, dept: int, username: str, phone: str, status: int) -> dict[str, Any]:
"""
获取用户列表查询条件
获取用户列表
:param db: 数据库会话
:param dept: 部门 ID
:param username: 用户名
:param phone: 手机号
:param status: 状态
:return:
"""
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
user_select = await user_dao.get_select(dept=dept, username=username, phone=phone, status=status)
return await paging_data(db, user_select)
@staticmethod
async def create(*, request: Request, obj: AddUserParam) -> None:
async def create(*, db: AsyncSession, obj: AddUserParam) -> None:
"""
创建用户
:param request: FastAPI 请求对象
:param db: 数据库会话
:param obj: 用户添加参数
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
if await user_dao.get_by_username(db, obj.username):
raise errors.ConflictError(msg='用户名已注册')
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(88888, 99999)}'
obj.nickname = obj.nickname or f'#{random.randrange(88888, 99999)}'
if not obj.password:
raise errors.RequestError(msg='密码不允许为空')
if not await dept_dao.get(db, obj.dept_id):
@@ -94,22 +94,19 @@ class UserService:
await user_dao.add(db, obj)
@staticmethod
async def update(*, request: Request, pk: int, obj: UpdateUserParam) -> int:
async def update(*, db: AsyncSession, pk: int, obj: UpdateUserParam) -> int:
"""
更新用户信息
:param request: FastAPI 请求对象
:param db: 数据库会话
:param pk: 用户 ID
:param obj: 用户更新参数
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
user = await user_dao.get_with_relation(db, user_id=pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if obj.username != user.username:
if await user_dao.get_by_username(db, obj.username):
if obj.username != user.username and await user_dao.get_by_username(db, obj.username):
raise errors.ConflictError(msg='用户名已注册')
for role_id in obj.roles:
if not await role_dao.get(db, role_id):
@@ -119,17 +116,16 @@ class UserService:
return count
@staticmethod
async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int:
async def update_permission(*, db: AsyncSession, request: Request, pk: int, type: UserPermissionType) -> int: # noqa: C901
"""
更新用户权限
:param db: 数据库会话
:param request: FastAPI 请求对象
:param pk: 用户 ID
:param type: 权限类型
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
match type:
case UserPermissionType.superuser:
user = await user_dao.get(db, pk)
@@ -137,14 +133,14 @@ class UserService:
raise errors.NotFoundError(msg='用户不存在')
if pk == request.user.id:
raise errors.ForbiddenError(msg='禁止修改自身权限')
count = await user_dao.set_super(db, pk, not user.status)
count = await user_dao.set_super(db, pk, is_super=not user.status)
case UserPermissionType.staff:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if pk == request.user.id:
raise errors.ForbiddenError(msg='禁止修改自身权限')
count = await user_dao.set_staff(db, pk, not user.is_staff)
count = await user_dao.set_staff(db, pk, is_staff=not user.is_staff)
case UserPermissionType.status:
user = await user_dao.get(db, pk)
if not user:
@@ -158,7 +154,7 @@ class UserService:
raise errors.NotFoundError(msg='用户不存在')
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
new_multi_login = not multi_login
count = await user_dao.set_multi_login(db, pk, new_multi_login)
count = await user_dao.set_multi_login(db, pk, multi_login=new_multi_login)
token = get_token(request)
token_payload = jwt_decode(token)
if pk == user.id:
@@ -166,7 +162,8 @@ class UserService:
if not new_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
await redis_client.delete_prefix(
key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}'
key_prefix,
exclude=f'{key_prefix}:{token_payload.session_uuid}',
)
else:
# 系统管理员修改他人时,他人 token 全部失效
@@ -180,17 +177,15 @@ class UserService:
return count
@staticmethod
async def reset_password(*, request: Request, pk: int, password: str) -> int:
async def reset_password(*, db: AsyncSession, pk: int, password: str) -> int:
"""
重置用户密码
:param request: FastAPI 请求对象
:param db: 数据库会话
:param pk: 用户 ID
:param password: 新密码
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
@@ -205,15 +200,15 @@ class UserService:
return count
@staticmethod
async def update_nickname(*, request: Request, nickname: str) -> int:
async def update_nickname(*, db: AsyncSession, request: Request, nickname: str) -> int:
"""
更新当前用户昵称
:param db: 数据库会话
:param request: FastAPI 请求对象
:param nickname: 用户昵称
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
@@ -224,15 +219,15 @@ class UserService:
return count
@staticmethod
async def update_avatar(*, request: Request, avatar: str) -> int:
async def update_avatar(*, db: AsyncSession, request: Request, avatar: str) -> int:
"""
更新当前用户头像
:param db: 数据库会话
:param request: FastAPI 请求对象
:param avatar: 头像地址
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
@@ -243,41 +238,41 @@ class UserService:
return count
@staticmethod
async def update_email(*, request: Request, captcha: str, email: str) -> int:
async def update_email(*, db: AsyncSession, request: Request, captcha: str, email: str) -> int:
"""
更新当前用户邮箱
:param db: 数据库会话
:param request: FastAPI 请求对象
:param captcha: 邮箱验证码
:param email: 邮箱
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
if not captcha_code:
raise errors.RequestError(msg='验证码已失效,请重新获取')
if captcha != captcha_code:
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
count = await user_dao.update_email(db, token_payload.id, email)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_password(*, request: Request, obj: ResetPasswordParam) -> int:
async def update_password(*, db: AsyncSession, request: Request, obj: ResetPasswordParam) -> int:
"""
更新当前用户密码
:param db: 数据库会话
:param request: FastAPI 请求对象
:param obj: 密码重置参数
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
@@ -298,14 +293,14 @@ class UserService:
return count
@staticmethod
async def delete(*, pk: int) -> int:
async def delete(*, db: AsyncSession, pk: int) -> int:
"""
删除用户
:param db: 数据库会话
:param pk: 用户 ID
:return:
"""
async with async_db_session.begin() as db:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from starlette.testclient import TestClient
+1 -3
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Generator
from collections.abc import Generator
import pytest
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+1 -3
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import AsyncGenerator
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio.session import AsyncSession
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.router import v1 as admin_v1
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from backend.core.path_conf import BASE_PATH
+1 -3
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from starlette.concurrency import run_in_threadpool
from backend.app.task.celery import celery_app
@@ -7,7 +5,7 @@ from backend.common.socketio.server import sio
@sio.event
async def task_worker_status(sid, data):
async def task_worker_status(sid, data) -> None: # noqa: ANN001
"""任务 Worker 状态事件"""
worker = await run_in_threadpool(celery_app.control.ping)
await sio.emit('task_worker_status', worker, sid)
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.task.api.v1.control import router as task_control_router
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+1 -3
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path
@@ -24,7 +22,7 @@ async def get_task_registered() -> ResponseSchemaModel[list[TaskRegisteredDetail
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
task_registered = []
celery_app_tasks = celery_app.tasks
for _, tasks in registered.items():
for tasks in registered.values():
for task in tasks:
task_ins = celery_app_tasks.get(task)
if task_ins:
+8 -10
View File
@@ -1,26 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.task.schema.result import DeleteTaskResultParam, GetTaskResultDetail
from backend.app.task.service.result_service import task_result_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.get('/{pk}', summary='获取任务结果详情', dependencies=[DependsJwtAuth])
async def get_task_result(
db: CurrentSession,
pk: Annotated[int, Path(description='任务结果 ID')],
) -> ResponseSchemaModel[GetTaskResultDetail]:
result = await task_result_service.get(pk=pk)
result = await task_result_service.get(db=db, pk=pk)
return response_base.success(data=result)
@@ -32,13 +31,12 @@ async def get_task_result(
DependsPagination,
],
)
async def get_task_results_paged(
async def get_task_results_paginated(
db: CurrentSession,
name: Annotated[str | None, Query(description='任务名称')] = None,
task_id: Annotated[str | None, Query(description='任务 ID')] = None,
) -> ResponseSchemaModel[PageData[GetTaskResultDetail]]:
result_select = await task_result_service.get_select(name=name, task_id=task_id)
page_data = await paging_data(db, result_select)
page_data = await task_result_service.get_list(db=db, name=name, task_id=task_id)
return response_base.success(data=page_data)
@@ -50,8 +48,8 @@ async def get_task_results_paged(
DependsRBAC,
],
)
async def delete_task_result(obj: DeleteTaskResultParam) -> ResponseModel:
count = await task_result_service.delete(obj=obj)
async def delete_task_result(db: CurrentSessionTransaction, obj: DeleteTaskResultParam) -> ResponseModel:
count = await task_result_service.delete(db=db, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
+30 -22
View File
@@ -1,32 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.task.schema.scheduler import CreateTaskSchedulerParam, GetTaskSchedulerDetail, UpdateTaskSchedulerParam
from backend.app.task.schema.scheduler import (
CreateTaskSchedulerParam,
GetTaskSchedulerDetail,
UpdateTaskSchedulerParam,
)
from backend.app.task.service.scheduler_service import task_scheduler_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.pagination import DependsPagination, PageData
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.database.db import CurrentSession, CurrentSessionTransaction
router = APIRouter()
@router.get('/all', summary='获取所有任务调度', dependencies=[DependsJwtAuth])
async def get_all_task_schedulers() -> ResponseSchemaModel[list[GetTaskSchedulerDetail]]:
schedulers = await task_scheduler_service.get_all()
async def get_all_task_schedulers(db: CurrentSession) -> ResponseSchemaModel[list[GetTaskSchedulerDetail]]:
schedulers = await task_scheduler_service.get_all(db=db)
return response_base.success(data=schedulers)
@router.get('/{pk}', summary='获取任务调度详情', dependencies=[DependsJwtAuth])
async def get_task_scheduler(
db: CurrentSession,
pk: Annotated[int, Path(description='任务调度 ID')],
) -> ResponseSchemaModel[GetTaskSchedulerDetail]:
task_scheduler = await task_scheduler_service.get(pk=pk)
task_scheduler = await task_scheduler_service.get(db=db, pk=pk)
return response_base.success(data=task_scheduler)
@@ -38,13 +41,12 @@ async def get_task_scheduler(
DependsPagination,
],
)
async def get_task_scheduler_paged(
async def get_task_scheduler_paginated(
db: CurrentSession,
name: Annotated[int, Path(description='任务调度名称')] = None,
name: Annotated[int | None, Path(description='任务调度名称')] = None,
type: Annotated[int | None, Query(description='任务调度类型')] = None,
) -> ResponseSchemaModel[PageData[GetTaskSchedulerDetail]]:
task_scheduler_select = await task_scheduler_service.get_select(name=name, type=type)
page_data = await paging_data(db, task_scheduler_select)
page_data = await task_scheduler_service.get_list(db=db, name=name, type=type)
return response_base.success(data=page_data)
@@ -56,8 +58,8 @@ async def get_task_scheduler_paged(
DependsRBAC,
],
)
async def create_task_scheduler(obj: CreateTaskSchedulerParam) -> ResponseModel:
await task_scheduler_service.create(obj=obj)
async def create_task_scheduler(db: CurrentSessionTransaction, obj: CreateTaskSchedulerParam) -> ResponseModel:
await task_scheduler_service.create(db=db, obj=obj)
return response_base.success()
@@ -70,9 +72,11 @@ async def create_task_scheduler(obj: CreateTaskSchedulerParam) -> ResponseModel:
],
)
async def update_task_scheduler(
pk: Annotated[int, Path(description='任务调度 ID')], obj: UpdateTaskSchedulerParam
db: CurrentSessionTransaction,
pk: Annotated[int, Path(description='任务调度 ID')],
obj: UpdateTaskSchedulerParam,
) -> ResponseModel:
count = await task_scheduler_service.update(pk=pk, obj=obj)
count = await task_scheduler_service.update(db=db, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -86,8 +90,10 @@ async def update_task_scheduler(
DependsRBAC,
],
)
async def update_task_scheduler_status(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
count = await task_scheduler_service.update_status(pk=pk)
async def update_task_scheduler_status(
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='任务调度 ID')]
) -> ResponseModel:
count = await task_scheduler_service.update_status(db=db, pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -101,8 +107,10 @@ async def update_task_scheduler_status(pk: Annotated[int, Path(description='任
DependsRBAC,
],
)
async def delete_task_scheduler(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
count = await task_scheduler_service.delete(pk=pk)
async def delete_task_scheduler(
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='任务调度 ID')]
) -> ResponseModel:
count = await task_scheduler_service.delete(db=db, pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -116,6 +124,6 @@ async def delete_task_scheduler(pk: Annotated[int, Path(description='任务调
DependsRBAC,
],
)
async def execute_task(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
await task_scheduler_service.execute(pk=pk)
async def execute_task(db: CurrentSession, pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
await task_scheduler_service.execute(db=db, pk=pk)
return response_base.success()
+3 -5
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import celery
@@ -10,10 +8,10 @@ from backend.core.conf import settings
from backend.core.path_conf import BASE_PATH
def find_task_packages():
def find_task_packages() -> list[str]:
packages = []
task_dir = os.path.join(BASE_PATH, 'app', 'task', 'tasks')
for root, dirs, files in os.walk(task_dir):
task_dir = BASE_PATH / 'app' / 'task' / 'tasks'
for root, _dirs, files in os.walk(task_dir):
if 'tasks.py' in files:
package = root.replace(str(BASE_PATH.parent) + os.path.sep, '').replace(os.path.sep, '.')
packages.append(package)
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+2 -4
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
@@ -20,9 +18,9 @@ class CRUDTaskResult(CRUDPlus[TaskResult]):
"""
return await self.select_model(db, pk)
async def get_list(self, name: str | None, task_id: str | None) -> Select:
async def get_select(self, name: str | None, task_id: str | None) -> Select:
"""
获取任务结果列表
获取任务结果列表查询表达式
:param name: 任务名称
:param task_id: 任务 ID
+5 -7
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from collections.abc import Sequence
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -33,9 +31,9 @@ class CRUDTaskScheduler(CRUDPlus[TaskScheduler]):
"""
return await self.select_models(db)
async def get_list(self, name: str | None, type: int | None) -> Select:
async def get_select(self, name: str | None, type: int | None) -> Select:
"""
获取任务调度列表
获取任务调度列表查询表达式
:param name: 任务调度名称
:param type: 任务调度类型
@@ -86,7 +84,7 @@ class CRUDTaskScheduler(CRUDPlus[TaskScheduler]):
TaskScheduler.no_changes = False
return 1
async def set_status(self, db: AsyncSession, pk: int, status: bool) -> int:
async def set_status(self, db: AsyncSession, pk: int, *, status: bool) -> int:
"""
设置任务调度状态
@@ -96,7 +94,7 @@ class CRUDTaskScheduler(CRUDPlus[TaskScheduler]):
:return:
"""
task_scheduler = await self.get(db, pk)
setattr(task_scheduler, 'enabled', status)
task_scheduler.enabled = status
TaskScheduler.no_changes = False
return 1
+34 -27
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from celery import states
from celery.backends.base import BaseBackend
from celery.backends.database import retry, session_cleanup
from celery.exceptions import ImproperlyConfigured
from celery.utils.time import maybe_timedelta
from sqlalchemy import PickleType
from sqlalchemy.orm import Session
from backend.app.task.model.result import Task, TaskExtended, TaskSet
from backend.app.task.session import SessionManager
@@ -24,7 +24,7 @@ class DatabaseBackend(BaseBackend):
task_cls = Task
taskset_cls = TaskSet
def __init__(self, dburi=None, engine_options=None, url=None, **kwargs):
def __init__(self, dburi=None, engine_options=None, url=None, **kwargs) -> None: # noqa: ANN001
# The `url` argument was added later and is used by
# the app to set backend by url (celery.app.backends.by_url)
super().__init__(expires_type=maybe_timedelta, url=url, **kwargs)
@@ -44,7 +44,7 @@ class DatabaseBackend(BaseBackend):
if not self.url:
raise ImproperlyConfigured(
'Missing connection string! Do you have the database_url setting set to a real value?'
'Missing connection string! Do you have the database_url setting set to a real value?',
)
self.session_manager = SessionManager()
@@ -54,24 +54,26 @@ class DatabaseBackend(BaseBackend):
self._create_tables()
@property
def extended_result(self):
def extended_result(self): # noqa: ANN201
return self.app.conf.find_value_for_key('extended', 'result')
def _create_tables(self):
def _create_tables(self) -> None:
"""Create the task and taskset tables."""
self.ResultSession()
self.result_session()
def ResultSession(self, session_manager=None):
def result_session(self, session_manager=None) -> Session: # noqa: ANN001
if session_manager is None:
session_manager = self.session_manager
return session_manager.session_factory(
dburi=self.url, short_lived_sessions=self.short_lived_sessions, **self.engine_options
dburi=self.url,
short_lived_sessions=self.short_lived_sessions,
**self.engine_options,
)
@retry
def _store_result(self, task_id, result, state, traceback=None, request=None, **kwargs):
def _store_result(self, task_id, result, state, traceback=None, request=None, **kwargs) -> None: # noqa: ANN001
"""Store return value and state of an executed task."""
session = self.ResultSession()
session = self.result_session()
with session_cleanup(session):
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
task = task and task[0]
@@ -84,9 +86,14 @@ class DatabaseBackend(BaseBackend):
self._update_result(task, result, state, traceback=traceback, request=request)
session.commit()
def _update_result(self, task, result, state, traceback=None, request=None):
def _update_result(self, task, result, state, traceback=None, request=None) -> None: # noqa: ANN001
meta = self._get_result_meta(
result=result, state=state, traceback=traceback, request=request, format_date=False, encode=True
result=result,
state=state,
traceback=traceback,
request=request,
format_date=False,
encode=True,
)
# Exclude the primary key id and task_id columns
@@ -101,9 +108,9 @@ class DatabaseBackend(BaseBackend):
setattr(task, column, value)
@retry
def _get_task_meta_for(self, task_id):
def _get_task_meta_for(self, task_id: str): # noqa: ANN202
"""Get task meta-data for a task by id."""
session = self.ResultSession()
session = self.result_session()
with session_cleanup(session):
task = list(session.query(self.task_cls).filter(self.task_cls.task_id == task_id))
task = task and task[0]
@@ -119,9 +126,9 @@ class DatabaseBackend(BaseBackend):
return self.meta_from_decoded(data)
@retry
def _save_group(self, group_id, result):
def _save_group(self, group_id: str, result: PickleType): # noqa: ANN202
"""Store the result of an executed group."""
session = self.ResultSession()
session = self.result_session()
with session_cleanup(session):
group = self.taskset_cls(group_id, result)
session.add(group)
@@ -130,34 +137,34 @@ class DatabaseBackend(BaseBackend):
return result
@retry
def _restore_group(self, group_id):
def _restore_group(self, group_id: str) -> dict | None:
"""Get meta-data for group by id."""
session = self.ResultSession()
session = self.result_session()
with session_cleanup(session):
group = session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).first()
if group:
return group.to_dict()
@retry
def _delete_group(self, group_id):
def _delete_group(self, group_id: str) -> None:
"""Delete meta-data for group by id."""
session = self.ResultSession()
session = self.result_session()
with session_cleanup(session):
session.query(self.taskset_cls).filter(self.taskset_cls.taskset_id == group_id).delete()
session.flush()
session.commit()
@retry
def _forget(self, task_id):
def _forget(self, task_id: str) -> None:
"""Forget about result."""
session = self.ResultSession()
session = self.result_session()
with session_cleanup(session):
session.query(self.task_cls).filter(self.task_cls.task_id == task_id).delete()
session.commit()
def cleanup(self):
def cleanup(self) -> None:
"""Delete expired meta-data."""
session = self.ResultSession()
session = self.result_session()
expires = self.expires
now = self.app.now()
with session_cleanup(session):
@@ -165,7 +172,7 @@ class DatabaseBackend(BaseBackend):
session.query(self.taskset_cls).filter(self.taskset_cls.date_done < (now - expires)).delete()
session.commit()
def __reduce__(self, args=(), kwargs=None):
kwargs = {} if not kwargs else kwargs
def __reduce__(self, args=(), kwargs=None): # noqa: ANN001, ANN204
kwargs = kwargs or {}
kwargs.update({'dburi': self.url, 'expires': self.expires, 'engine_options': self.engine_options})
return super().__reduce__(args, kwargs)
-2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import IntEnum, StrEnum
+2 -4
View File
@@ -1,4 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.task.model.result import TaskExtended as TaskResult
from backend.app.task.model.scheduler import TaskScheduler
from backend.app.task.model.result import TaskExtended as TaskResult # noqa: F401
from backend.app.task.model.scheduler import TaskScheduler as TaskScheduler
+14 -13
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timezone
import sqlalchemy as sa
@@ -25,14 +23,17 @@ class Task(MappedBase):
status = sa.Column(sa.String(50), default=states.PENDING)
result = sa.Column(PickleType, nullable=True)
date_done = sa.Column(
sa.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc), nullable=True
sa.DateTime,
default=datetime.now(timezone.utc),
onupdate=datetime.now(timezone.utc),
nullable=True,
)
traceback = sa.Column(sa.Text, nullable=True)
def __init__(self, task_id):
def __init__(self, task_id: str) -> None:
self.task_id = task_id
def to_dict(self):
def to_dict(self) -> dict:
return {
'task_id': self.task_id,
'status': self.status,
@@ -41,11 +42,11 @@ class Task(MappedBase):
'date_done': self.date_done,
}
def __repr__(self):
return '<Task {0.task_id} state: {0.status}>'.format(self)
def __repr__(self) -> str:
return f'<Task {self.task_id} state: {self.status}>'
@classmethod
def configure(cls, schema=None, name=None):
def configure(cls, schema=None, name=None) -> None: # noqa: ANN001
cls.__table__.schema = schema
cls.id.default.schema = schema
cls.__table__.name = name or cls.__tablename__
@@ -64,7 +65,7 @@ class TaskExtended(Task):
retries = sa.Column(sa.Integer, nullable=True)
queue = sa.Column(sa.String(155), nullable=True)
def to_dict(self):
def to_dict(self) -> dict:
task_dict = super().to_dict()
task_dict.update({
'name': self.name,
@@ -88,22 +89,22 @@ class TaskSet(MappedBase):
result = sa.Column(PickleType, nullable=True)
date_done = sa.Column(sa.DateTime, default=datetime.now(timezone.utc), nullable=True)
def __init__(self, taskset_id, result):
def __init__(self, taskset_id, result) -> None: # noqa: ANN001
self.taskset_id = taskset_id
self.result = result
def to_dict(self):
def to_dict(self) -> dict:
return {
'taskset_id': self.taskset_id,
'result': self.result,
'date_done': self.date_done,
}
def __repr__(self):
def __repr__(self) -> str:
return f'<TaskSet: {self.taskset_id}>'
@classmethod
def configure(cls, schema=None, name=None):
def configure(cls, schema=None, name=None) -> None: # noqa: ANN001
cls.__table__.schema = schema
cls.id.default.schema = schema
cls.__table__.name = name or cls.__tablename__
+20 -33
View File
@@ -1,21 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
from datetime import datetime
from sqlalchemy import (
JSON,
Boolean,
String,
event,
)
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import INTEGER, TEXT
import sqlalchemy as sa
from sqlalchemy import event
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.exception import errors
from backend.common.model import Base, TimeZone, id_key
from backend.common.model import Base, TimeZone, UniversalText, id_key
from backend.core.conf import settings
from backend.database.redis import redis_client
from backend.utils.timezone import timezone
@@ -27,51 +20,45 @@ class TaskScheduler(Base):
__tablename__ = 'task_scheduler'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), unique=True, comment='任务名称')
task: Mapped[str] = mapped_column(String(255), comment='要运行的 Celery 任务')
args: Mapped[str | None] = mapped_column(JSON(), comment='任务可接收的位置参数')
kwargs: Mapped[str | None] = mapped_column(JSON(), comment='任务可接收的关键字参数')
queue: Mapped[str | None] = mapped_column(String(255), comment='CELERY_TASK_QUEUES 中定义的队列')
exchange: Mapped[str | None] = mapped_column(String(255), comment='低级别 AMQP 路由的交换机')
routing_key: Mapped[str | None] = mapped_column(String(255), comment='低级别 AMQP 路由的路由密钥')
name: Mapped[str] = mapped_column(sa.String(50), unique=True, comment='任务名称')
task: Mapped[str] = mapped_column(sa.String(255), comment='要运行的 Celery 任务')
args: Mapped[str | None] = mapped_column(sa.JSON(), comment='任务可接收的位置参数')
kwargs: Mapped[str | None] = mapped_column(sa.JSON(), comment='任务可接收的关键字参数')
queue: Mapped[str | None] = mapped_column(sa.String(255), comment='CELERY_TASK_QUEUES 中定义的队列')
exchange: Mapped[str | None] = mapped_column(sa.String(255), comment='低级别 AMQP 路由的交换机')
routing_key: Mapped[str | None] = mapped_column(sa.String(255), comment='低级别 AMQP 路由的路由密钥')
start_time: Mapped[datetime | None] = mapped_column(TimeZone, comment='任务开始触发的时间')
expire_time: Mapped[datetime | None] = mapped_column(TimeZone, comment='任务不再触发的截止时间')
expire_seconds: Mapped[int | None] = mapped_column(comment='任务不再触发的秒数时间差')
type: Mapped[int] = mapped_column(comment='调度类型(0间隔 1定时)')
interval_every: Mapped[int | None] = mapped_column(comment='任务再次运行前的间隔周期数')
interval_period: Mapped[str | None] = mapped_column(String(255), comment='任务运行之间的周期类型')
crontab: Mapped[str | None] = mapped_column(String(50), default='* * * * *', comment='任务运行的 Crontab 计划')
one_off: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否仅运行一次'
)
enabled: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='是否启用任务'
)
interval_period: Mapped[str | None] = mapped_column(sa.String(255), comment='任务运行之间的周期类型')
crontab: Mapped[str | None] = mapped_column(sa.String(50), default='* * * * *', comment='任务运行的 Crontab 计划')
one_off: Mapped[bool] = mapped_column(default=False, comment='是否仅运行一次')
enabled: Mapped[bool] = mapped_column(default=True, comment='是否启用任务')
total_run_count: Mapped[int] = mapped_column(default=0, comment='任务触发的总次数')
last_run_time: Mapped[datetime | None] = mapped_column(TimeZone, default=None, comment='任务最后触发的时间')
remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注')
no_changes: bool = False
@staticmethod
def before_insert_or_update(mapper, connection, target):
def before_insert_or_update(mapper, connection, target) -> None: # noqa: ANN001
if target.expire_seconds is not None and target.expire_time:
raise errors.ConflictError(msg='expires 和 expire_seconds 只能设置一个')
@classmethod
def changed(cls, mapper, connection, target):
def changed(cls, mapper, connection, target) -> None: # noqa: ANN001
if not target.no_changes:
cls.update_changed(mapper, connection, target)
@classmethod
async def update_changed_async(cls):
async def update_changed_async(cls) -> None:
now = timezone.now()
await redis_client.set(f'{settings.CELERY_REDIS_PREFIX}:last_update', timezone.to_str(now))
@classmethod
def update_changed(cls, mapper, connection, target):
def update_changed(cls, mapper, connection, target) -> None: # noqa: ANN001
asyncio.create_task(cls.update_changed_async())
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

Some files were not shown because too many files have changed in this diff Show More