mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74df1d1ce2 | ||
|
|
54e09b68c7 | ||
|
|
5762834744 | ||
|
|
51354593d0 | ||
|
|
8751425e2b | ||
|
|
921f05fa19 | ||
|
|
5754c5449d | ||
|
|
2f535e3510 | ||
|
|
c25e447530 | ||
|
|
13359b736c | ||
|
|
500f20d91c | ||
|
|
d4de42fb46 | ||
|
|
3f0537a727 | ||
|
|
d1cdb9e462 | ||
|
|
6c029b9be7 | ||
|
|
df12a0c701 | ||
|
|
0be5685d9f | ||
|
|
3efca8af37 | ||
|
|
82b2f808ac | ||
|
|
f417374f3e | ||
|
|
237534c63e | ||
|
|
b525124fdf | ||
|
|
5217c95451 | ||
|
|
ea973cac90 | ||
|
|
1bedda0e66 | ||
|
|
c1087fa51d | ||
|
|
cfddf44e7e | ||
|
|
085b32e921 | ||
|
|
2099c33aad | ||
|
|
b809497aba | ||
|
|
a6fe8c0e8d | ||
|
|
629cf7ce45 | ||
|
|
fad6c05d41 | ||
|
|
77d59d3453 | ||
|
|
931d6b281a | ||
|
|
d929f0dbdc | ||
|
|
6d1b47903e | ||
|
|
8420ccf190 | ||
|
|
1e38e1918b | ||
|
|
52e78152a1 | ||
|
|
dcb07c60f2 | ||
|
|
33c9f78cf0 | ||
|
|
37b1033697 | ||
|
|
0aa4ebc635 | ||
|
|
30d4107670 | ||
|
|
dcf235db78 | ||
|
|
1dfcd7ae3c | ||
|
|
2b28244099 | ||
|
|
f71071f905 | ||
|
|
34c9c398ca | ||
|
|
f099777dd3 | ||
|
|
c95be82cb9 | ||
|
|
00ec039095 | ||
|
|
4d087355b4 | ||
|
|
d57fabcbb4 | ||
|
|
3a32522e2b | ||
|
|
22f8850bf7 | ||
|
|
eeb709c6aa | ||
|
|
a6bbf2971d | ||
|
|
cd48bb4210 | ||
|
|
4500dd0128 | ||
|
|
2b6d8222ad | ||
|
|
1b47ab7e83 | ||
|
|
bd804e0a38 | ||
|
|
8e8af2032a | ||
|
|
e09062eb39 | ||
|
|
8c00492e44 | ||
|
|
0237d4c7b1 | ||
|
|
fe3a3b4e86 | ||
|
|
65ec721a1c | ||
|
|
4eb76ad6ea | ||
|
|
1f8687155a | ||
|
|
8591d4e592 | ||
|
|
dedf4e7bae | ||
|
|
1e4aa88487 | ||
|
|
0dd745b7a7 | ||
|
|
6b2402f212 | ||
|
|
24a487eeea | ||
|
|
4f574189c7 | ||
|
|
b559a74cea | ||
|
|
83dcdbe59d | ||
|
|
d64f7c2911 | ||
|
|
53e64bce37 | ||
|
|
00a781357b | ||
|
|
d7f87ed0ad | ||
|
|
bda9b1d463 | ||
|
|
e0a106ec51 | ||
|
|
016361bd68 |
@@ -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
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
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.11.9
|
||||
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.7.3
|
||||
rev: 0.9.0
|
||||
hooks:
|
||||
- id: uv-lock
|
||||
- id: uv-export
|
||||
|
||||
+174
@@ -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"
|
||||
+162
@@ -1,3 +1,161 @@
|
||||
<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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
## What's Changed
|
||||
* Update changelog for v1.7.0 by [@wu-clan](https://github.com/wu-clan) in [#729](https://github.com/fastapi-practices/fastapi_best_architecture/pull/729)
|
||||
* Simplify task crontab expression validation by [@wu-clan](https://github.com/wu-clan) in [#733](https://github.com/fastapi-practices/fastapi_best_architecture/pull/733)
|
||||
* Add distributed lock for scheduled task by [@wu-clan](https://github.com/wu-clan) in [#732](https://github.com/fastapi-practices/fastapi_best_architecture/pull/732)
|
||||
* Update the default cache period for userinfo by [@wu-clan](https://github.com/wu-clan) in [#734](https://github.com/fastapi-practices/fastapi_best_architecture/pull/734)
|
||||
* Fix celery CLI option to required by [@wu-clan](https://github.com/wu-clan) in [#737](https://github.com/fastapi-practices/fastapi_best_architecture/pull/737)
|
||||
* Add auth whitelist regular expression config by [@wu-clan](https://github.com/wu-clan) in [#738](https://github.com/fastapi-practices/fastapi_best_architecture/pull/738)
|
||||
* Fix the opera log field encryption by [@wu-clan](https://github.com/wu-clan) in [#739](https://github.com/fastapi-practices/fastapi_best_architecture/pull/739)
|
||||
* Update the OAuth2 login password policy by [@wu-clan](https://github.com/wu-clan) in [#741](https://github.com/fastapi-practices/fastapi_best_architecture/pull/741)
|
||||
* Add update support for user email and phone by [@wu-clan](https://github.com/wu-clan) in [#742](https://github.com/fastapi-practices/fastapi_best_architecture/pull/742)
|
||||
* Fix the error trigger when model auto import by [@wu-clan](https://github.com/wu-clan) in [#743](https://github.com/fastapi-practices/fastapi_best_architecture/pull/743)
|
||||
* Simplify the plugin status update logic by [@wu-clan](https://github.com/wu-clan) in [#744](https://github.com/fastapi-practices/fastapi_best_architecture/pull/744)
|
||||
* Add some interfaces for user profiles by [@wu-clan](https://github.com/wu-clan) in [#745](https://github.com/fastapi-practices/fastapi_best_architecture/pull/745)
|
||||
* Add schedule task demo that contains params by [@wu-clan](https://github.com/wu-clan) in [#746](https://github.com/fastapi-practices/fastapi_best_architecture/pull/746)
|
||||
* Fix the kwargs params of schedule task by [@wu-clan](https://github.com/wu-clan) in [#747](https://github.com/fastapi-practices/fastapi_best_architecture/pull/747)
|
||||
* Refactor code generation files and routes by [@wu-clan](https://github.com/wu-clan) in [#748](https://github.com/fastapi-practices/fastapi_best_architecture/pull/748)
|
||||
* Refactor task routes and add control routes by [@wu-clan](https://github.com/wu-clan) in [#749](https://github.com/fastapi-practices/fastapi_best_architecture/pull/749)
|
||||
* Fix message format in validation exception handler by [@wu-clan](https://github.com/wu-clan) in [#755](https://github.com/fastapi-practices/fastapi_best_architecture/pull/755)
|
||||
* Update the opera log desensitization method by [@wu-clan](https://github.com/wu-clan) in [#756](https://github.com/fastapi-practices/fastapi_best_architecture/pull/756)
|
||||
* Add business pagination in the code generator by [@wu-clan](https://github.com/wu-clan) in [#757](https://github.com/fastapi-practices/fastapi_best_architecture/pull/757)
|
||||
* Optimize the data sort logic of tree nodes by [@wu-clan](https://github.com/wu-clan) in [#758](https://github.com/fastapi-practices/fastapi_best_architecture/pull/758)
|
||||
* Update log output config and format by [@wu-clan](https://github.com/wu-clan) in [#759](https://github.com/fastapi-practices/fastapi_best_architecture/pull/759)
|
||||
* Update the naming of table creation function by [@wu-clan](https://github.com/wu-clan) in [#760](https://github.com/fastapi-practices/fastapi_best_architecture/pull/760)
|
||||
* Optimize the opera log storage logic through queue by [@IAseven](https://github.com/IAseven) in [#750](https://github.com/fastapi-practices/fastapi_best_architecture/pull/750)
|
||||
* Optimize naming and preview in code generation by [@wu-clan](https://github.com/wu-clan) in [#764](https://github.com/fastapi-practices/fastapi_best_architecture/pull/764)
|
||||
* Update the description for the run file by [@wu-clan](https://github.com/wu-clan) in [#766](https://github.com/fastapi-practices/fastapi_best_architecture/pull/766)
|
||||
* Optimize the timezone datetime return encoder by [@wu-clan](https://github.com/wu-clan) in [#767](https://github.com/fastapi-practices/fastapi_best_architecture/pull/767)
|
||||
* Update the content layout of the config file by [@wu-clan](https://github.com/wu-clan) in [#768](https://github.com/fastapi-practices/fastapi_best_architecture/pull/768)
|
||||
* Add a standalone email sending plugin by [@wu-clan](https://github.com/wu-clan) in [#769](https://github.com/fastapi-practices/fastapi_best_architecture/pull/769)
|
||||
* Add i18n support for response message by [@downdawn](https://github.com/downdawn) in [#753](https://github.com/fastapi-practices/fastapi_best_architecture/pull/753)
|
||||
* Update the menu title in SQL scripts by [@wu-clan](https://github.com/wu-clan) in [#770](https://github.com/fastapi-practices/fastapi_best_architecture/pull/770)
|
||||
* Update the version number to 1.8.0 by [@wu-clan](https://github.com/wu-clan) in [#771](https://github.com/fastapi-practices/fastapi_best_architecture/pull/771)
|
||||
|
||||
## New Contributors
|
||||
* [@IAseven](https://github.com/IAseven) made their first contribution in [#750](https://github.com/fastapi-practices/fastapi_best_architecture/pull/750)
|
||||
|
||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.7.0...v1.8.0
|
||||
|
||||
[Changes][v1.8.0]
|
||||
|
||||
|
||||
<a id="v1.7.0"></a>
|
||||
# [v1.7.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.7.0) - 2025-07-16
|
||||
|
||||
## What's Changed
|
||||
* Update the changelog for v1.6.0 by [@wu-clan](https://github.com/wu-clan) in [#703](https://github.com/fastapi-practices/fastapi_best_architecture/pull/703)
|
||||
* Update the CLI to be executed async by [@wu-clan](https://github.com/wu-clan) in [#704](https://github.com/fastapi-practices/fastapi_best_architecture/pull/704)
|
||||
* Fix the code generation schema template by [@wu-clan](https://github.com/wu-clan) in [#706](https://github.com/fastapi-practices/fastapi_best_architecture/pull/706)
|
||||
* Replace gunicorn deployment to granian by [@wu-clan](https://github.com/wu-clan) in [#705](https://github.com/fastapi-practices/fastapi_best_architecture/pull/705)
|
||||
* Fix the code generation delete schema template by [@wu-clan](https://github.com/wu-clan) in [#708](https://github.com/fastapi-practices/fastapi_best_architecture/pull/708)
|
||||
* Update the refresh token verify mechanism by [@wu-clan](https://github.com/wu-clan) in [#710](https://github.com/fastapi-practices/fastapi_best_architecture/pull/710)
|
||||
* Update the reload excludes for CLI run by [@wu-clan](https://github.com/wu-clan) in [#709](https://github.com/fastapi-practices/fastapi_best_architecture/pull/709)
|
||||
* Add CLI support for execute sql scripts by [@wu-clan](https://github.com/wu-clan) in [#711](https://github.com/fastapi-practices/fastapi_best_architecture/pull/711)
|
||||
* Update the granian env to command params by [@wu-clan](https://github.com/wu-clan) in [#712](https://github.com/fastapi-practices/fastapi_best_architecture/pull/712)
|
||||
* Update the middleware logging accuracy by [@wu-clan](https://github.com/wu-clan) in [#713](https://github.com/fastapi-practices/fastapi_best_architecture/pull/713)
|
||||
* Update the log output default style by [@wu-clan](https://github.com/wu-clan) in [#714](https://github.com/fastapi-practices/fastapi_best_architecture/pull/714)
|
||||
* Optimize the analysis of get plugins by [@wu-clan](https://github.com/wu-clan) in [#716](https://github.com/fastapi-practices/fastapi_best_architecture/pull/716)
|
||||
* Simplify user permission database queries by [@wu-clan](https://github.com/wu-clan) in [#717](https://github.com/fastapi-practices/fastapi_best_architecture/pull/717)
|
||||
* Update the CLI startup service mode by [@wu-clan](https://github.com/wu-clan) in [#718](https://github.com/fastapi-practices/fastapi_best_architecture/pull/718)
|
||||
* Add support for celery dynamic tasks by [@wu-clan](https://github.com/wu-clan) in [#715](https://github.com/fastapi-practices/fastapi_best_architecture/pull/715)
|
||||
* Fix the celery task scheduler query by [@wu-clan](https://github.com/wu-clan) in [#719](https://github.com/fastapi-practices/fastapi_best_architecture/pull/719)
|
||||
* Update the celery task comment and name by [@wu-clan](https://github.com/wu-clan) in [#720](https://github.com/fastapi-practices/fastapi_best_architecture/pull/720)
|
||||
* Optimize celery integrations and events by [@wu-clan](https://github.com/wu-clan) in [#721](https://github.com/fastapi-practices/fastapi_best_architecture/pull/721)
|
||||
* Simplify celery task crontab config by [@wu-clan](https://github.com/wu-clan) in [#722](https://github.com/fastapi-practices/fastapi_best_architecture/pull/722)
|
||||
* Delete the default value of schema enum data by [@wu-clan](https://github.com/wu-clan) in [#723](https://github.com/fastapi-practices/fastapi_best_architecture/pull/723)
|
||||
* Fix the parsing of execution task params by [@wu-clan](https://github.com/wu-clan) in [#725](https://github.com/fastapi-practices/fastapi_best_architecture/pull/725)
|
||||
* Bump granian from 2.4.0 to 2.4.2 by [@wu-clan](https://github.com/wu-clan) in [#727](https://github.com/fastapi-practices/fastapi_best_architecture/pull/727)
|
||||
* Add CLI support for startup celery services by [@wu-clan](https://github.com/wu-clan) in [#724](https://github.com/fastapi-practices/fastapi_best_architecture/pull/724)
|
||||
* Fix login and operation log clearing by [@wu-clan](https://github.com/wu-clan) in [#728](https://github.com/fastapi-practices/fastapi_best_architecture/pull/728)
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
|
||||
|
||||
[Changes][v1.7.0]
|
||||
|
||||
|
||||
<a id="v1.6.0"></a>
|
||||
# [v1.6.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.6.0) - 2025-06-30
|
||||
|
||||
@@ -679,6 +837,10 @@
|
||||
[Changes][v1.0.0]
|
||||
|
||||
|
||||
[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
|
||||
[v1.5.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
|
||||
[v1.5.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+12
-8
@@ -1,4 +1,4 @@
|
||||
# Env: dev、pro
|
||||
# Env
|
||||
ENVIRONMENT='dev'
|
||||
# Database
|
||||
DATABASE_TYPE='mysql'
|
||||
@@ -15,13 +15,7 @@ REDIS_DATABASE=0
|
||||
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
||||
# Opera Log
|
||||
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
||||
# App Admin
|
||||
# OAuth2
|
||||
OAUTH2_GITHUB_CLIENT_ID='test'
|
||||
OAUTH2_GITHUB_CLIENT_SECRET='test'
|
||||
OAUTH2_LINUX_DO_CLIENT_ID='test'
|
||||
OAUTH2_LINUX_DO_CLIENT_SECRET='test'
|
||||
# App Task
|
||||
# [ App ] task
|
||||
# Celery
|
||||
CELERY_BROKER_REDIS_DATABASE=1
|
||||
# Rabbitmq
|
||||
@@ -29,3 +23,13 @@ CELERY_RABBITMQ_HOST='127.0.0.1'
|
||||
CELERY_RABBITMQ_PORT=5672
|
||||
CELERY_RABBITMQ_USERNAME='guest'
|
||||
CELERY_RABBITMQ_PASSWORD='guest'
|
||||
# [ Plugin ] oauth2
|
||||
OAUTH2_GITHUB_CLIENT_ID='test'
|
||||
OAUTH2_GITHUB_CLIENT_SECRET='test'
|
||||
OAUTH2_GOOGLE_CLIENT_ID='test'
|
||||
OAUTH2_GOOGLE_CLIENT_SECRET='test'
|
||||
OAUTH2_LINUX_DO_CLIENT_ID='test'
|
||||
OAUTH2_LINUX_DO_CLIENT_SECRET='test'
|
||||
# [ Plugin ] email
|
||||
EMAIL_USERNAME=''
|
||||
EMAIL_PASSWORD=''
|
||||
|
||||
@@ -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
|
||||
+4
-6
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend.utils.console import console
|
||||
from backend.common.i18n import i18n
|
||||
|
||||
__version__ = '1.7.0'
|
||||
__version__ = '1.8.2'
|
||||
|
||||
|
||||
def get_version() -> str | None:
|
||||
console.print(f'[cyan]{__version__}[/]')
|
||||
# 初始化 i18n
|
||||
i18n.load_locales()
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# ruff: noqa: F403, F401, I001, RUF100
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
@@ -11,8 +8,6 @@ from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
sys.path.append('../')
|
||||
|
||||
from backend.app import get_app_models
|
||||
from backend.common.model import MappedBase
|
||||
from backend.core import path_conf
|
||||
@@ -42,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
|
||||
@@ -74,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():
|
||||
|
||||
@@ -7,6 +7,7 @@ Create Date: ${create_date}
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import backend.common.model
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
+10
-24
@@ -1,39 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import inspect
|
||||
import os.path
|
||||
|
||||
from backend.common.log import log
|
||||
from backend.core.path_conf import BASE_PATH
|
||||
from backend.utils.import_parse import import_module_cached
|
||||
from backend.utils.import_parse import get_model_objects
|
||||
|
||||
|
||||
def get_app_models():
|
||||
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 = []
|
||||
apps = [d for d in list_dirs if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__']
|
||||
|
||||
for d in list_dirs:
|
||||
if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__':
|
||||
apps.append(d)
|
||||
|
||||
classes = []
|
||||
objs = []
|
||||
|
||||
for app in apps:
|
||||
try:
|
||||
module_path = f'backend.app.{app}.model'
|
||||
module = import_module_cached(module_path)
|
||||
except Exception as e:
|
||||
log.warning(f'应用 {app} 中不包含 model 相关配置: {e}')
|
||||
continue
|
||||
module_path = f'backend.app.{app}.model'
|
||||
obj = get_model_objects(module_path)
|
||||
if obj:
|
||||
objs.extend(obj)
|
||||
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj):
|
||||
classes.append(obj)
|
||||
|
||||
return classes
|
||||
return objs
|
||||
|
||||
|
||||
# import all app models for auto create db tables
|
||||
|
||||
@@ -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 fastapi import APIRouter
|
||||
|
||||
from backend.app.admin.api.v1.auth import router as auth_router
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
@@ -29,7 +27,10 @@ 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
|
||||
request: Request,
|
||||
response: Response,
|
||||
obj: AuthLoginParam,
|
||||
background_tasks: BackgroundTasks,
|
||||
) -> ResponseSchemaModel[GetLoginToken]:
|
||||
data = await auth_service.login(request=request, response=response, obj=obj, background_tasks=background_tasks)
|
||||
return response_base.success(data=data)
|
||||
@@ -41,7 +42,7 @@ async def get_codes(request: Request) -> ResponseSchemaModel[list[str]]:
|
||||
return response_base.success(data=codes)
|
||||
|
||||
|
||||
@router.post('/tokens', summary='刷新 token')
|
||||
@router.post('/refresh', summary='刷新 token')
|
||||
async def refresh_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
||||
data = await auth_service.refresh_token(request=request)
|
||||
return response_base.success(data=data)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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_limiter.depends import RateLimiter
|
||||
@@ -24,11 +24,11 @@ async def get_captcha(request: Request) -> ResponseSchemaModel[GetCaptchaDetail]
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from typing import Annotated
|
||||
@@ -37,8 +35,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:
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.common.response.response_schema import ResponseModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.utils.redis_info import redis_info
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='redis 监控',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:monitor:redis')),
|
||||
DependsJwtAuth,
|
||||
],
|
||||
)
|
||||
@router.get('', summary='redis 监控', dependencies=[DependsJwtAuth])
|
||||
async def get_redis_info() -> ResponseModel:
|
||||
data = {
|
||||
'info': await redis_info.get_info(),
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.common.response.response_schema import ResponseModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.utils.server_info import server_info
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='server 监控',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:monitor:server')),
|
||||
DependsJwtAuth,
|
||||
],
|
||||
)
|
||||
@router.get('', summary='server 监控', dependencies=[DependsJwtAuth])
|
||||
async def get_server_info() -> ResponseModel:
|
||||
data = {
|
||||
# 扔到线程池,避免阻塞
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
@@ -59,7 +57,8 @@ async def get_data_rule(
|
||||
],
|
||||
)
|
||||
async def get_data_rules_paged(
|
||||
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
|
||||
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)
|
||||
@@ -88,7 +87,8 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
|
||||
],
|
||||
)
|
||||
async def update_data_rule(
|
||||
pk: Annotated[int, Path(description='数据规则 ID')], obj: UpdateDataRuleParam
|
||||
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||
obj: UpdateDataRuleParam,
|
||||
) -> ResponseModel:
|
||||
count = await data_rule_service.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
@@ -85,7 +83,8 @@ async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
|
||||
],
|
||||
)
|
||||
async def update_data_scope(
|
||||
pk: Annotated[int, Path(description='数据范围 ID')], obj: UpdateDataScopeParam
|
||||
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||
obj: UpdateDataScopeParam,
|
||||
) -> ResponseModel:
|
||||
count = await data_scope_service.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@@ -102,8 +101,9 @@ async def update_data_scope(
|
||||
],
|
||||
)
|
||||
async def update_data_scope_rules(
|
||||
pk: Annotated[int, Path(description='数据范围 ID')], rule_ids: UpdateDataScopeRuleParam
|
||||
):
|
||||
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||
rule_ids: UpdateDataScopeRuleParam,
|
||||
) -> ResponseModel:
|
||||
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, UpdateDeptParam
|
||||
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, GetDeptTree, UpdateDeptParam
|
||||
from backend.app.admin.service.dept_service import dept_service
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
@@ -27,7 +25,7 @@ async def get_dept_tree(
|
||||
leader: Annotated[str | None, Query(description='部门负责人')] = None,
|
||||
phone: Annotated[str | None, Query(description='联系电话')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||
) -> ResponseSchemaModel[list[GetDeptTree]]:
|
||||
dept = await dept_service.get_tree(request=request, name=name, leader=leader, phone=phone, status=status)
|
||||
return response_base.success(data=dept)
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.menu import CreateMenuParam, GetMenuDetail, UpdateMenuParam
|
||||
from backend.app.admin.schema.menu import CreateMenuParam, GetMenuDetail, GetMenuTree, UpdateMenuParam
|
||||
from backend.app.admin.service.menu_service import menu_service
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
@@ -30,7 +28,7 @@ async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> Respons
|
||||
async def get_menu_tree(
|
||||
title: Annotated[str | None, Query(description='菜单标题')] = None,
|
||||
status: Annotated[int | None, Query(description='状体')] = None,
|
||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||
) -> ResponseSchemaModel[list[GetMenuTree]]:
|
||||
menu = await menu_service.get_tree(title=title, status=status)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
|
||||
@@ -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)移除相关配置并重启服务'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.admin.schema.menu import GetMenuTree
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
DeleteRoleParam,
|
||||
@@ -33,7 +32,7 @@ async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
@router.get('/{pk}/menus', summary='获取角色菜单树', dependencies=[DependsJwtAuth])
|
||||
async def get_role_menu_tree(
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||
) -> ResponseSchemaModel[list[GetMenuTree] | None]:
|
||||
menu = await role_service.get_menu_tree(pk=pk)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
@@ -105,7 +104,8 @@ 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
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
menu_ids: UpdateRoleMenuParam,
|
||||
) -> ResponseModel:
|
||||
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
|
||||
if count > 0:
|
||||
@@ -122,7 +122,8 @@ async def update_role_menus(
|
||||
],
|
||||
)
|
||||
async def update_role_scopes(
|
||||
pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
scope_ids: UpdateRoleScopeParam,
|
||||
) -> ResponseModel:
|
||||
count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
|
||||
if count > 0:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.role import GetRoleDetail
|
||||
from backend.app.admin.schema.user import (
|
||||
@@ -71,9 +69,11 @@ async def create_user(request: Request, obj: AddUserParam) -> ResponseSchemaMode
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
||||
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsRBAC])
|
||||
async def update_user(
|
||||
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: UpdateUserParam
|
||||
request: Request,
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
obj: UpdateUserParam,
|
||||
) -> ResponseModel:
|
||||
count = await user_service.update(request=request, pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@@ -93,11 +93,55 @@ async def update_user_permission(
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsJwtAuth])
|
||||
@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)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsRBAC])
|
||||
async def reset_user_password(
|
||||
pk: Annotated[int, Path(description='用户 ID')], obj: ResetPasswordParam
|
||||
request: Request,
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
password: Annotated[str, Body(embed=True, description='新密码')],
|
||||
) -> ResponseModel:
|
||||
count = await user_service.reset_pwd(pk=pk, obj=obj)
|
||||
count = await user_service.reset_password(request=request, pk=pk, password=password)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth])
|
||||
async def update_user_nickname(
|
||||
request: Request,
|
||||
nickname: Annotated[str, Body(embed=True, description='用户昵称')],
|
||||
) -> ResponseModel:
|
||||
count = await user_service.update_nickname(request=request, nickname=nickname)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
|
||||
async def update_user_avatar(
|
||||
request: Request,
|
||||
avatar: Annotated[str, Body(embed=True, description='用户头像地址')],
|
||||
) -> ResponseModel:
|
||||
count = await user_service.update_avatar(request=request, avatar=avatar)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/me/email', summary='更新当前用户邮箱', dependencies=[DependsJwtAuth])
|
||||
async def update_user_email(
|
||||
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)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -37,11 +35,21 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||
创建操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 创建操作日志参数
|
||||
:param obj: 操作日志创建参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def bulk_create(self, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
|
||||
"""
|
||||
批量创建操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param objs: 操作日志创建参数列表
|
||||
:return:
|
||||
"""
|
||||
await self.create_models(db, objs)
|
||||
|
||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||
"""
|
||||
批量删除操作日志
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import bcrypt
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -89,10 +87,8 @@ class CRUDUser(CRUDPlus[User]):
|
||||
:param obj: 注册用户参数
|
||||
:return:
|
||||
"""
|
||||
salt = bcrypt.gensalt()
|
||||
obj.password = get_hash_password(obj.password, salt)
|
||||
dict_obj = obj.model_dump()
|
||||
dict_obj.update({'is_staff': True, 'salt': salt})
|
||||
dict_obj.update({'is_staff': True, 'salt': None})
|
||||
new_user = self.model(**dict_obj)
|
||||
|
||||
stmt = select(Role)
|
||||
@@ -119,6 +115,17 @@ class CRUDUser(CRUDPlus[User]):
|
||||
input_user.roles = roles.scalars().all()
|
||||
return count
|
||||
|
||||
async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int:
|
||||
"""
|
||||
更新用户昵称
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param nickname: 用户昵称
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'nickname': nickname})
|
||||
|
||||
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||
"""
|
||||
更新用户头像
|
||||
@@ -130,6 +137,17 @@ class CRUDUser(CRUDPlus[User]):
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'avatar': avatar})
|
||||
|
||||
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
|
||||
"""
|
||||
更新用户邮箱
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param email: 邮箱
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'email': email})
|
||||
|
||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||
"""
|
||||
删除用户
|
||||
@@ -150,16 +168,18 @@ class CRUDUser(CRUDPlus[User]):
|
||||
"""
|
||||
return await self.select_model_by_column(db, email=email)
|
||||
|
||||
async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int:
|
||||
async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:param new_pwd: 新密码(已加密)
|
||||
:param password: 新密码
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, pk, {'password': new_pwd})
|
||||
salt = bcrypt.gensalt()
|
||||
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:
|
||||
"""
|
||||
@@ -178,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
|
||||
|
||||
@@ -192,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:
|
||||
"""
|
||||
设置用户超级管理员状态
|
||||
|
||||
@@ -203,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:
|
||||
"""
|
||||
设置用户后台登录状态
|
||||
|
||||
@@ -225,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:
|
||||
"""
|
||||
设置用户多端登录状态
|
||||
|
||||
@@ -237,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:
|
||||
"""
|
||||
获取用户关联信息
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -25,7 +23,7 @@ class DataRule(Base):
|
||||
column: Mapped[str] = mapped_column(String(20), comment='模型字段名')
|
||||
operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)')
|
||||
expression: Mapped[int] = mapped_column(
|
||||
comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)'
|
||||
comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)',
|
||||
)
|
||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import INTEGER
|
||||
@@ -27,15 +25,21 @@ class Dept(Base):
|
||||
email: Mapped[str | None] = mapped_column(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存在)'
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
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'
|
||||
BigInteger,
|
||||
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')
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.model import DataClassBase, id_key
|
||||
from backend.common.model import DataClassBase, TimeZone, id_key
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
@@ -29,7 +27,10 @@ class LoginLog(DataClassBase):
|
||||
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='提示消息')
|
||||
login_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), comment='登录时间')
|
||||
login_time: Mapped[datetime] = mapped_column(TimeZone, comment='登录时间')
|
||||
created_time: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='创建时间'
|
||||
TimeZone,
|
||||
init=False,
|
||||
default_factory=timezone.now,
|
||||
comment='创建时间',
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
||||
|
||||
from backend.common.model import MappedBase
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BigInteger, ForeignKey, String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
@@ -34,18 +32,26 @@ class Menu(Base):
|
||||
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='外链地址'
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
default=None,
|
||||
comment='外链地址',
|
||||
)
|
||||
remark: Mapped[str | None] = mapped_column(
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
default=None,
|
||||
comment='备注',
|
||||
)
|
||||
|
||||
# 父级菜单一对多
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger, ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
|
||||
BigInteger,
|
||||
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')
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, String
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.model import DataClassBase, id_key
|
||||
from backend.common.model import DataClassBase, TimeZone, id_key
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
@@ -26,7 +24,7 @@ class OperaLog(DataClassBase):
|
||||
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='请求头')
|
||||
user_agent: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), 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='设备')
|
||||
@@ -35,7 +33,10 @@ class OperaLog(DataClassBase):
|
||||
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
|
||||
msg: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms)')
|
||||
opera_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), comment='操作时间')
|
||||
opera_time: Mapped[datetime] = mapped_column(TimeZone, comment='操作时间')
|
||||
created_time: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='创建时间'
|
||||
TimeZone,
|
||||
init=False,
|
||||
default_factory=timezone.now,
|
||||
comment='创建时间',
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -25,10 +23,14 @@ class Role(Base):
|
||||
name: Mapped[str] = mapped_column(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是)'
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=True,
|
||||
comment='过滤数据权限(0否 1是)',
|
||||
)
|
||||
remark: Mapped[str | None] = mapped_column(
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
default=None,
|
||||
comment='备注',
|
||||
)
|
||||
|
||||
# 角色用户多对多
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
#!/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, DateTime, ForeignKey, String
|
||||
from sqlalchemy import VARBINARY, Boolean, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.m2m import sys_user_role
|
||||
from backend.common.model import Base, id_key
|
||||
from backend.common.model import Base, TimeZone, id_key
|
||||
from backend.database.db import uuid4_str
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
@@ -27,31 +25,40 @@ class User(Base):
|
||||
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] = mapped_column(String(255), comment='密码')
|
||||
salt: Mapped[bytes] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), 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='头像')
|
||||
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是)'
|
||||
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是)'
|
||||
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是)'
|
||||
)
|
||||
join_time: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='注册时间'
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
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(
|
||||
DateTime(timezone=True), init=False, onupdate=timezone.now, comment='上次登录'
|
||||
TimeZone,
|
||||
init=False,
|
||||
onupdate=timezone.now,
|
||||
comment='上次登录',
|
||||
)
|
||||
|
||||
# 部门用户一对多
|
||||
dept_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
|
||||
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||
default=None,
|
||||
comment='部门关联ID',
|
||||
)
|
||||
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -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='图片内容')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
@@ -37,3 +35,9 @@ class GetDeptDetail(DeptSchemaBase):
|
||||
del_flag: bool = Field(description='是否删除')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
|
||||
|
||||
class GetDeptTree(GetDeptDetail):
|
||||
"""获取部门树"""
|
||||
|
||||
children: list['GetDeptTree'] | None = Field(None, description='子菜单')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
@@ -43,3 +41,9 @@ class GetMenuDetail(MenuSchemaBase):
|
||||
id: int = Field(description='菜单 ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
|
||||
|
||||
class GetMenuTree(GetMenuDetail):
|
||||
"""获取菜单树"""
|
||||
|
||||
children: list['GetMenuTree'] | None = Field(None, description='子菜单')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
||||
from pydantic import ConfigDict, Field, HttpUrl, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from backend.app.admin.schema.dept import GetDeptDetail
|
||||
@@ -16,28 +14,32 @@ class AuthSchemaBase(SchemaBase):
|
||||
"""用户认证基础模型"""
|
||||
|
||||
username: str = Field(description='用户名')
|
||||
password: str | None = Field(description='密码')
|
||||
password: str = Field(description='密码')
|
||||
|
||||
|
||||
class AuthLoginParam(AuthSchemaBase):
|
||||
"""用户登录参数"""
|
||||
|
||||
uuid: str = Field(description='验证码 UUID')
|
||||
captcha: str = Field(description='验证码')
|
||||
|
||||
|
||||
class AddUserParam(AuthSchemaBase):
|
||||
"""添加用户参数"""
|
||||
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号码')
|
||||
dept_id: int = Field(description='部门 ID')
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
|
||||
|
||||
class AddOAuth2UserParam(AuthSchemaBase):
|
||||
"""添加 OAuth2 用户参数"""
|
||||
|
||||
password: str | None = Field(None, description='密码')
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: EmailStr = Field(description='邮箱')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||
|
||||
|
||||
@@ -56,6 +58,8 @@ class UserInfoSchemaBase(SchemaBase):
|
||||
username: str = Field(description='用户名')
|
||||
nickname: str = Field(description='昵称')
|
||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||
|
||||
|
||||
class UpdateUserParam(UserInfoSchemaBase):
|
||||
@@ -72,8 +76,6 @@ class GetUserInfoDetail(UserInfoSchemaBase):
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
id: int = Field(description='用户 ID')
|
||||
uuid: str = Field(description='用户 UUID')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||
status: StatusType = Field(description='状态')
|
||||
is_superuser: bool = Field(description='是否超级管理员')
|
||||
is_staff: bool = Field(description='是否管理员')
|
||||
|
||||
@@ -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 Request, Response
|
||||
from fastapi.security import HTTPBasicCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -13,6 +11,7 @@ from backend.app.admin.schema.user import AuthLoginParam
|
||||
from backend.app.admin.service.login_log_service import login_log_service
|
||||
from backend.common.enums import LoginLogStatusType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.i18n import t
|
||||
from backend.common.log import log
|
||||
from backend.common.response.response_code import CustomErrorCode
|
||||
from backend.common.security.jwt import (
|
||||
@@ -33,7 +32,7 @@ class AuthService:
|
||||
"""认证服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def user_verify(db: AsyncSession, username: str, password: str | None) -> User:
|
||||
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
|
||||
"""
|
||||
验证用户名和密码
|
||||
|
||||
@@ -48,9 +47,8 @@ class AuthService:
|
||||
|
||||
if user.password is None:
|
||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||
else:
|
||||
if not password_verify(password, user.password):
|
||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||
if not password_verify(password, user.password):
|
||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||
|
||||
if not user.status:
|
||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||
@@ -69,14 +67,19 @@ class AuthService:
|
||||
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,
|
||||
*,
|
||||
request: Request,
|
||||
response: Response,
|
||||
obj: AuthLoginParam,
|
||||
background_tasks: BackgroundTasks,
|
||||
) -> GetLoginToken:
|
||||
"""
|
||||
用户登录
|
||||
@@ -91,17 +94,17 @@ class AuthService:
|
||||
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='验证码失效,请重新获取')
|
||||
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,
|
||||
@@ -111,7 +114,11 @@ class AuthService:
|
||||
browser=request.state.browser,
|
||||
device=request.state.device,
|
||||
)
|
||||
refresh_token = await create_refresh_token(access_token.session_uuid, user.id, user.is_multi_login)
|
||||
refresh_token = await create_refresh_token(
|
||||
access_token.session_uuid,
|
||||
user.id,
|
||||
multi_login=user.is_multi_login,
|
||||
)
|
||||
response.set_cookie(
|
||||
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
||||
value=refresh_token.refresh_token,
|
||||
@@ -127,32 +134,28 @@ 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,
|
||||
),
|
||||
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(msg=e.msg, background=task)
|
||||
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='登录成功',
|
||||
),
|
||||
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,
|
||||
@@ -203,16 +206,15 @@ class AuthService:
|
||||
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}:*'):
|
||||
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
|
||||
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,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import Select
|
||||
|
||||
@@ -103,9 +101,8 @@ class DataRuleService:
|
||||
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):
|
||||
raise errors.ConflictError(msg='数据规则已存在')
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import Select
|
||||
|
||||
@@ -94,9 +92,8 @@ class DataScopeService:
|
||||
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):
|
||||
raise errors.ConflictError(msg='数据范围已存在')
|
||||
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:
|
||||
for user in await role.awaitable_attrs.users:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
@@ -33,7 +31,12 @@ class DeptService:
|
||||
|
||||
@staticmethod
|
||||
async def get_tree(
|
||||
*, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
|
||||
*,
|
||||
request: Request,
|
||||
name: str | None,
|
||||
leader: str | None,
|
||||
phone: str | None,
|
||||
status: int | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取部门树形结构
|
||||
@@ -81,9 +84,8 @@ class DeptService:
|
||||
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):
|
||||
raise errors.ConflictError(msg='部门名称已存在')
|
||||
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)
|
||||
if not parent_dept:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
@@ -61,8 +59,7 @@ 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
|
||||
@@ -98,9 +95,8 @@ class MenuService:
|
||||
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):
|
||||
raise errors.ConflictError(msg='菜单标题已存在')
|
||||
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)
|
||||
if not parent_menu:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select
|
||||
|
||||
from backend.app.admin.crud.crud_opera_log import opera_log_dao
|
||||
@@ -33,6 +31,17 @@ class OperaLogService:
|
||||
async with async_db_session.begin() as db:
|
||||
await opera_log_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def bulk_create(*, objs: list[CreateOperaLogParam]) -> None:
|
||||
"""
|
||||
批量创建操作日志
|
||||
|
||||
: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:
|
||||
"""
|
||||
|
||||
@@ -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}:info:*'):
|
||||
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,32 +57,31 @@ 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}:info:{plugin}')
|
||||
await redis_client.hdel(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin)
|
||||
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:
|
||||
"""
|
||||
更新插件状态
|
||||
|
||||
:param plugin: 插件名称
|
||||
:return:
|
||||
"""
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||
if not plugin_info:
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
plugin_info = json.loads(plugin_info)
|
||||
@@ -98,10 +93,7 @@ class PluginService:
|
||||
else str(StatusType.disable.value)
|
||||
)
|
||||
plugin_info['plugin']['enable'] = new_status
|
||||
await redis_client.set(
|
||||
f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(plugin_info, ensure_ascii=False)
|
||||
)
|
||||
await redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin, new_status)
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(plugin_info, ensure_ascii=False))
|
||||
|
||||
@staticmethod
|
||||
async def build(*, plugin: str) -> io.BytesIO:
|
||||
@@ -111,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()
|
||||
@@ -121,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)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/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
|
||||
|
||||
@@ -114,9 +113,8 @@ class RoleService:
|
||||
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):
|
||||
raise errors.ConflictError(msg='角色已存在')
|
||||
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:
|
||||
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import Select
|
||||
@@ -18,7 +16,8 @@ from backend.app.admin.schema.user import (
|
||||
)
|
||||
from backend.common.enums import UserPermissionType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.security.jwt import get_hash_password, get_token, jwt_decode, password_verify, superuser_verify
|
||||
from backend.common.response.response_code import CustomErrorCode
|
||||
from backend.common.security.jwt import get_token, jwt_decode, password_verify, superuser_verify
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import async_db_session
|
||||
from backend.database.redis import redis_client
|
||||
@@ -82,7 +81,7 @@ class UserService:
|
||||
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):
|
||||
@@ -103,14 +102,12 @@ class UserService:
|
||||
: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 request.user.username != user.username:
|
||||
raise errors.ForbiddenError(msg='只能修改自己的信息')
|
||||
if obj.username != user.username:
|
||||
if await user_dao.get_by_username(db, obj.username):
|
||||
raise errors.ConflictError(msg='用户名已注册')
|
||||
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):
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
@@ -119,98 +116,7 @@ class UserService:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_superuser(*, request: Request, pk: int) -> int:
|
||||
"""
|
||||
更新用户管理员状态
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
: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='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
count = await user_dao.set_super(db, pk, not user.status)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_staff(*, request: Request, pk: int) -> int:
|
||||
"""
|
||||
更新用户职员状态
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
: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='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
count = await user_dao.set_staff(db, pk, not user.is_staff)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_status(*, request: Request, pk: int) -> int:
|
||||
"""
|
||||
更新用户状态
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
: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='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_multi_login(*, request: Request, pk: int) -> int:
|
||||
"""
|
||||
更新用户多端登录状态
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
: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='用户不存在')
|
||||
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)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
if pk == user.id:
|
||||
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
|
||||
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}')
|
||||
else:
|
||||
# 系统管理员修改他人时,他人 token 全部失效
|
||||
if not new_multi_login:
|
||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||
await redis_client.delete_prefix(key_prefix)
|
||||
return count
|
||||
|
||||
async def update_permission(self, *, request: Request, pk: int, type: UserPermissionType) -> int:
|
||||
async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int: # noqa: C901
|
||||
"""
|
||||
更新用户权限
|
||||
|
||||
@@ -219,38 +125,167 @@ class UserService:
|
||||
:param type: 权限类型
|
||||
:return:
|
||||
"""
|
||||
match type:
|
||||
case UserPermissionType.superuser:
|
||||
count = await self.update_superuser(request=request, pk=pk)
|
||||
case UserPermissionType.staff:
|
||||
count = await self.update_staff(request=request, pk=pk)
|
||||
case UserPermissionType.status:
|
||||
count = await self.update_status(request=request, pk=pk)
|
||||
case UserPermissionType.multi_login:
|
||||
count = await self.update_multi_login(request=request, pk=pk)
|
||||
case _:
|
||||
raise errors.RequestError(msg='权限类型不存在')
|
||||
async with async_db_session.begin() as db:
|
||||
superuser_verify(request)
|
||||
match type:
|
||||
case UserPermissionType.superuser:
|
||||
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_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, is_staff=not user.is_staff)
|
||||
case UserPermissionType.status:
|
||||
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_status(db, pk, 0 if user.status == 1 else 1)
|
||||
case UserPermissionType.multi_login:
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
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, multi_login=new_multi_login)
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
if pk == user.id:
|
||||
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
|
||||
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}',
|
||||
)
|
||||
else:
|
||||
# 系统管理员修改他人时,他人 token 全部失效
|
||||
if not new_multi_login:
|
||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||
await redis_client.delete_prefix(key_prefix)
|
||||
case _:
|
||||
raise errors.RequestError(msg='权限类型不存在')
|
||||
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def reset_pwd(*, pk: int, obj: ResetPasswordParam) -> int:
|
||||
async def reset_password(*, request: Request, pk: int, password: str) -> int:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
: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='用户不存在')
|
||||
count = await user_dao.reset_password(db, user.id, password)
|
||||
key_prefix = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||
]
|
||||
for prefix in key_prefix:
|
||||
await redis_client.delete(prefix)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_nickname(*, request: Request, nickname: str) -> int:
|
||||
"""
|
||||
更新当前用户昵称
|
||||
|
||||
: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)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.update_nickname(db, token_payload.id, nickname)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_avatar(*, request: Request, avatar: str) -> int:
|
||||
"""
|
||||
更新当前用户头像
|
||||
|
||||
: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)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.update_avatar(db, token_payload.id, avatar)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_email(*, request: Request, captcha: str, email: str) -> int:
|
||||
"""
|
||||
更新当前用户邮箱
|
||||
|
||||
: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}')
|
||||
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}')
|
||||
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:
|
||||
"""
|
||||
更新当前用户密码
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param obj: 密码重置参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
user = await user_dao.get(db, pk)
|
||||
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='用户不存在')
|
||||
if not password_verify(obj.old_password, user.password):
|
||||
raise errors.RequestError(msg='原密码错误')
|
||||
if obj.new_password != obj.confirm_password:
|
||||
raise errors.RequestError(msg='密码输入不一致')
|
||||
new_pwd = get_hash_password(obj.new_password, user.salt)
|
||||
count = await user_dao.reset_password(db, user.id, new_pwd)
|
||||
count = await user_dao.reset_password(db, user.id, obj.new_password)
|
||||
key_prefix = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
from backend.core.path_conf import BASE_PATH
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.app.task.api.v1.control import router as task_control_router
|
||||
from backend.app.task.api.v1.result import router as task_result_router
|
||||
from backend.app.task.api.v1.scheduler import router as task_scheduler_router
|
||||
from backend.core.conf import settings
|
||||
|
||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/task', tags=['任务'])
|
||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/tasks', tags=['任务'])
|
||||
|
||||
v1.include_router(task_control_router)
|
||||
v1.include_router(task_result_router, prefix='/results')
|
||||
v1.include_router(task_scheduler_router, prefix='/schedulers')
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.app.task import celery_app
|
||||
from backend.app.task.schema.control import TaskRegisteredDetail
|
||||
from backend.common.exception import errors
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/registered', summary='获取已注册的任务', dependencies=[DependsJwtAuth])
|
||||
async def get_task_registered() -> ResponseSchemaModel[list[TaskRegisteredDetail]]:
|
||||
inspector = celery_app.control.inspect(timeout=0.5)
|
||||
registered = await run_in_threadpool(inspector.registered)
|
||||
if not registered:
|
||||
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
||||
task_registered = []
|
||||
celery_app_tasks = celery_app.tasks
|
||||
for tasks in registered.values():
|
||||
for task in tasks:
|
||||
task_ins = celery_app_tasks.get(task)
|
||||
if task_ins:
|
||||
task_doc = task_ins.__doc__
|
||||
task_registered.append({'name': task_doc or task_ins, 'task': task_ins})
|
||||
else:
|
||||
task_registered.append({'name': task, 'task': task})
|
||||
return response_base.success(data=task_registered)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{task_id}/cancel',
|
||||
summary='撤销任务',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:task:revoke')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def revoke_task(task_id: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
|
||||
workers = await run_in_threadpool(celery_app.control.ping, timeout=0.5)
|
||||
if not workers:
|
||||
raise errors.ServerError(msg='Celery Worker 暂不可用,请稍后重试')
|
||||
celery_app.control.revoke(task_id)
|
||||
return response_base.success()
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/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.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
@@ -40,7 +42,7 @@ async def get_task_scheduler(
|
||||
)
|
||||
async def get_task_scheduler_paged(
|
||||
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)
|
||||
@@ -70,7 +72,8 @@ async def create_task_scheduler(obj: CreateTaskSchedulerParam) -> ResponseModel:
|
||||
],
|
||||
)
|
||||
async def update_task_scheduler(
|
||||
pk: Annotated[int, Path(description='任务调度 ID')], obj: UpdateTaskSchedulerParam
|
||||
pk: Annotated[int, Path(description='任务调度 ID')],
|
||||
obj: UpdateTaskSchedulerParam,
|
||||
) -> ResponseModel:
|
||||
count = await task_scheduler_service.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
@@ -119,16 +122,3 @@ async def delete_task_scheduler(pk: Annotated[int, Path(description='任务调
|
||||
async def execute_task(pk: Annotated[int, Path(description='任务调度 ID')]) -> ResponseModel:
|
||||
await task_scheduler_service.execute(pk=pk)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{task_id}/cancel',
|
||||
summary='撤销任务',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:task:revoke')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def revoke_task(task_id: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
|
||||
await task_scheduler_service.revoke(task_id=task_id)
|
||||
return response_base.success()
|
||||
|
||||
+15
-14
@@ -1,19 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
import celery
|
||||
import celery_aio_pool
|
||||
|
||||
from backend.app.task.model.result import OVERWRITE_CELERY_RESULT_GROUP_TABLE_NAME, OVERWRITE_CELERY_RESULT_TABLE_NAME
|
||||
from backend.app.task.tasks.beat import LOCAL_BEAT_SCHEDULE
|
||||
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 = []
|
||||
for root, dirs, files in os.walk(os.path.join(BASE_PATH, 'app', 'task', 'tasks')):
|
||||
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)
|
||||
@@ -29,21 +27,19 @@ def init_celery() -> celery.Celery:
|
||||
celery.app.trace.build_tracer = celery_aio_pool.build_async_tracer
|
||||
celery.app.trace.reset_worker_optimizations()
|
||||
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html
|
||||
app = celery.Celery(
|
||||
'fba_celery',
|
||||
broker=f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.CELERY_BROKER_REDIS_DATABASE}'
|
||||
broker_url=f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.CELERY_BROKER_REDIS_DATABASE}'
|
||||
if settings.CELERY_BROKER == 'redis'
|
||||
else f'amqp://{settings.CELERY_RABBITMQ_USERNAME}:{settings.CELERY_RABBITMQ_PASSWORD}@{settings.CELERY_RABBITMQ_HOST}:{settings.CELERY_RABBITMQ_PORT}',
|
||||
broker_connection_retry_on_startup=True,
|
||||
backend=f'db+{settings.DATABASE_TYPE}+{"pymysql" if settings.DATABASE_TYPE == "mysql" else "psycopg"}'
|
||||
result_backend=f'db+{settings.DATABASE_TYPE}+{"pymysql" if settings.DATABASE_TYPE == "mysql" else "psycopg"}'
|
||||
f'://{settings.DATABASE_USER}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOST}:{settings.DATABASE_PORT}/{settings.DATABASE_SCHEMA}',
|
||||
database_engine_options={'echo': settings.DATABASE_ECHO},
|
||||
database_table_names={
|
||||
'task': OVERWRITE_CELERY_RESULT_TABLE_NAME,
|
||||
'group': OVERWRITE_CELERY_RESULT_GROUP_TABLE_NAME,
|
||||
},
|
||||
result_extended=True,
|
||||
# result_expires=0, # 任务结果自动清理,0 或 None 表示不清理
|
||||
database_engine_options={'echo': settings.DATABASE_ECHO},
|
||||
# result_expires=0,
|
||||
# beat_sync_every=1,
|
||||
beat_schedule=LOCAL_BEAT_SCHEDULE,
|
||||
beat_scheduler='backend.app.task.utils.schedulers:DatabaseScheduler',
|
||||
task_cls='backend.app.task.tasks.base:TaskBase',
|
||||
@@ -52,8 +48,13 @@ def init_celery() -> celery.Celery:
|
||||
timezone=settings.DATETIME_TIMEZONE,
|
||||
)
|
||||
|
||||
# 在 Celery 中设置此参数无效
|
||||
# 参数:https://github.com/celery/celery/issues/7270
|
||||
app.loader.override_backends = {'db': 'backend.app.task.database:DatabaseBackend'}
|
||||
|
||||
# 自动发现任务
|
||||
app.autodiscover_tasks(find_task_packages())
|
||||
packages = find_task_packages()
|
||||
app.autodiscover_tasks(packages)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.task.model.result import TaskResult
|
||||
from backend.app.task.model import TaskResult
|
||||
|
||||
|
||||
class CRUDTaskResult(CRUDPlus[TaskResult]):
|
||||
@@ -35,7 +33,7 @@ class CRUDTaskResult(CRUDPlus[TaskResult]):
|
||||
if task_id is not None:
|
||||
filters['task_id'] = task_id
|
||||
|
||||
return await self.select_order('id', **filters)
|
||||
return await self.select_order('id', 'desc', **filters)
|
||||
|
||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
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
|
||||
|
||||
"""
|
||||
重写 from celery.backends.database 内部 DatabaseBackend 类,此类实现与模型配合不佳,导致 fba 创建表和 alembic 迁移困难
|
||||
"""
|
||||
|
||||
|
||||
class DatabaseBackend(BaseBackend):
|
||||
"""The database result backend."""
|
||||
|
||||
# ResultSet.iterate should sleep this much between each pool,
|
||||
# to not bombard the database with queries.
|
||||
subpolling_interval = 0.5
|
||||
|
||||
task_cls = Task
|
||||
taskset_cls = TaskSet
|
||||
|
||||
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)
|
||||
conf = self.app.conf
|
||||
|
||||
if self.extended_result:
|
||||
self.task_cls = TaskExtended
|
||||
|
||||
self.url = url or dburi or conf.database_url
|
||||
self.engine_options = dict(engine_options or {}, **conf.database_engine_options or {})
|
||||
self.short_lived_sessions = kwargs.get('short_lived_sessions', conf.database_short_lived_sessions)
|
||||
|
||||
schemas = conf.database_table_schemas or {}
|
||||
tablenames = conf.database_table_names or {}
|
||||
self.task_cls.configure(schema=schemas.get('task'), name=tablenames.get('task'))
|
||||
self.taskset_cls.configure(schema=schemas.get('group'), name=tablenames.get('group'))
|
||||
|
||||
if not self.url:
|
||||
raise ImproperlyConfigured(
|
||||
'Missing connection string! Do you have the database_url setting set to a real value?',
|
||||
)
|
||||
|
||||
self.session_manager = SessionManager()
|
||||
|
||||
create_tables_at_setup = conf.database_create_tables_at_setup
|
||||
if create_tables_at_setup is True:
|
||||
self._create_tables()
|
||||
|
||||
@property
|
||||
def extended_result(self): # noqa: ANN201
|
||||
return self.app.conf.find_value_for_key('extended', 'result')
|
||||
|
||||
def _create_tables(self) -> None:
|
||||
"""Create the task and taskset tables."""
|
||||
self.result_session()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@retry
|
||||
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.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]
|
||||
if not task:
|
||||
task = self.task_cls(task_id)
|
||||
task.task_id = task_id
|
||||
session.add(task)
|
||||
session.flush()
|
||||
|
||||
self._update_result(task, result, state, traceback=traceback, request=request)
|
||||
session.commit()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# Exclude the primary key id and task_id columns
|
||||
# as we should not set it None
|
||||
columns = [column.name for column in self.task_cls.__table__.columns if column.name not in {'id', 'task_id'}]
|
||||
|
||||
# Iterate through the columns name of the table
|
||||
# to set the value from meta.
|
||||
# If the value is not present in meta, set None
|
||||
for column in columns:
|
||||
value = meta.get(column)
|
||||
setattr(task, column, value)
|
||||
|
||||
@retry
|
||||
def _get_task_meta_for(self, task_id: str): # noqa: ANN202
|
||||
"""Get task meta-data for a task by id."""
|
||||
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]
|
||||
if not task:
|
||||
task = self.task_cls(task_id)
|
||||
task.status = states.PENDING
|
||||
task.result = None
|
||||
data = task.to_dict()
|
||||
if data.get('args', None) is not None:
|
||||
data['args'] = self.decode(data['args'])
|
||||
if data.get('kwargs', None) is not None:
|
||||
data['kwargs'] = self.decode(data['kwargs'])
|
||||
return self.meta_from_decoded(data)
|
||||
|
||||
@retry
|
||||
def _save_group(self, group_id: str, result: PickleType): # noqa: ANN202
|
||||
"""Store the result of an executed group."""
|
||||
session = self.result_session()
|
||||
with session_cleanup(session):
|
||||
group = self.taskset_cls(group_id, result)
|
||||
session.add(group)
|
||||
session.flush()
|
||||
session.commit()
|
||||
return result
|
||||
|
||||
@retry
|
||||
def _restore_group(self, group_id: str) -> dict | None:
|
||||
"""Get meta-data for group by id."""
|
||||
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: str) -> None:
|
||||
"""Delete meta-data for group by id."""
|
||||
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: str) -> None:
|
||||
"""Forget about result."""
|
||||
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) -> None:
|
||||
"""Delete expired meta-data."""
|
||||
session = self.result_session()
|
||||
expires = self.expires
|
||||
now = self.app.now()
|
||||
with session_cleanup(session):
|
||||
session.query(self.task_cls).filter(self.task_cls.date_done < (now - expires)).delete()
|
||||
session.query(self.taskset_cls).filter(self.taskset_cls.date_done < (now - expires)).delete()
|
||||
session.commit()
|
||||
|
||||
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)
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend.common.enums import IntEnum, StrEnum
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
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
|
||||
|
||||
@@ -1,9 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from celery.backends.database.models import TaskExtended as TaskResult
|
||||
from datetime import datetime, timezone
|
||||
|
||||
OVERWRITE_CELERY_RESULT_TABLE_NAME = 'task_result'
|
||||
OVERWRITE_CELERY_RESULT_GROUP_TABLE_NAME = 'task_group_result'
|
||||
import sqlalchemy as sa
|
||||
|
||||
# 重写表名配置
|
||||
TaskResult.configure(name=OVERWRITE_CELERY_RESULT_TABLE_NAME)
|
||||
from celery import states
|
||||
from sqlalchemy.types import PickleType
|
||||
|
||||
from backend.common.model import MappedBase
|
||||
|
||||
"""
|
||||
重写 celery.backends.database.models 内部所有模型,适配 fba 创建表和 alembic 迁移
|
||||
"""
|
||||
|
||||
|
||||
class Task(MappedBase):
|
||||
"""Task result/status."""
|
||||
|
||||
__tablename__ = 'task_result'
|
||||
__table_args__ = {'comment': '任务结果表'}
|
||||
|
||||
id = sa.Column(sa.Integer, sa.Sequence('task_id_sequence'), primary_key=True, autoincrement=True)
|
||||
task_id = sa.Column(sa.String(155), unique=True)
|
||||
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,
|
||||
)
|
||||
traceback = sa.Column(sa.Text, nullable=True)
|
||||
|
||||
def __init__(self, task_id: str) -> None:
|
||||
self.task_id = task_id
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'task_id': self.task_id,
|
||||
'status': self.status,
|
||||
'result': self.result,
|
||||
'traceback': self.traceback,
|
||||
'date_done': self.date_done,
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Task {self.task_id} state: {self.status}>'
|
||||
|
||||
@classmethod
|
||||
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__
|
||||
|
||||
|
||||
class TaskExtended(Task):
|
||||
"""For the extend result."""
|
||||
|
||||
__tablename__ = 'task_result'
|
||||
__table_args__ = {'extend_existing': True, 'comment': '任务结果表'}
|
||||
|
||||
name = sa.Column(sa.String(155), nullable=True)
|
||||
args = sa.Column(sa.LargeBinary, nullable=True)
|
||||
kwargs = sa.Column(sa.LargeBinary, nullable=True)
|
||||
worker = sa.Column(sa.String(155), nullable=True)
|
||||
retries = sa.Column(sa.Integer, nullable=True)
|
||||
queue = sa.Column(sa.String(155), nullable=True)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
task_dict = super().to_dict()
|
||||
task_dict.update({
|
||||
'name': self.name,
|
||||
'args': self.args,
|
||||
'kwargs': self.kwargs,
|
||||
'worker': self.worker,
|
||||
'retries': self.retries,
|
||||
'queue': self.queue,
|
||||
})
|
||||
return task_dict
|
||||
|
||||
|
||||
class TaskSet(MappedBase):
|
||||
"""TaskSet result."""
|
||||
|
||||
__tablename__ = 'task_set_result'
|
||||
__table_args__ = {'comment': '任务集结果表'}
|
||||
|
||||
id = sa.Column(sa.Integer, sa.Sequence('taskset_id_sequence'), autoincrement=True, primary_key=True)
|
||||
taskset_id = sa.Column(sa.String(155), unique=True)
|
||||
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) -> None: # noqa: ANN001
|
||||
self.taskset_id = taskset_id
|
||||
self.result = result
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'taskset_id': self.taskset_id,
|
||||
'result': self.result,
|
||||
'date_done': self.date_done,
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<TaskSet: {self.taskset_id}>'
|
||||
|
||||
@classmethod
|
||||
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__
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
|
||||
from datetime import datetime
|
||||
@@ -7,7 +5,6 @@ from datetime import datetime
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
DateTime,
|
||||
String,
|
||||
event,
|
||||
)
|
||||
@@ -16,7 +13,7 @@ from sqlalchemy.dialects.postgresql import INTEGER, TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.common.model import Base, id_key
|
||||
from backend.common.model import Base, TimeZone, id_key
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
from backend.utils.timezone import timezone
|
||||
@@ -35,46 +32,50 @@ class TaskScheduler(Base):
|
||||
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 路由的路由密钥')
|
||||
start_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), comment='任务开始触发的时间')
|
||||
expire_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), comment='任务不再触发的截止时间')
|
||||
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='是否仅运行一次'
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=False,
|
||||
comment='是否仅运行一次',
|
||||
)
|
||||
enabled: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='是否启用任务'
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=True,
|
||||
comment='是否启用任务',
|
||||
)
|
||||
total_run_count: Mapped[int] = mapped_column(default=0, comment='任务触发的总次数')
|
||||
last_run_time: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), default=None, 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='备注'
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
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())
|
||||
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user