mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 05:02:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809879997c | ||
|
|
b153b7ac18 | ||
|
|
308ccf71ae | ||
|
|
c8f6d9da72 | ||
|
|
3f2c3e44ba | ||
|
|
b5e2a9e591 | ||
|
|
cdabe91092 | ||
|
|
69a59993e3 | ||
|
|
b6defc671e | ||
|
|
8cbea57a8a | ||
|
|
7845e1ff49 | ||
|
|
2b76b64e85 | ||
|
|
dd3165a082 | ||
|
|
407d12760b | ||
|
|
f8b244c3a9 | ||
|
|
dfc715aad8 | ||
|
|
4bc5ba53e6 | ||
|
|
6b4fd93e5f | ||
|
|
0c5956f828 | ||
|
|
122d17ab57 | ||
|
|
4fb14e480a | ||
|
|
ea4ac6ff41 | ||
|
|
bb5bcdf430 | ||
|
|
aad9afa9bb | ||
|
|
a0e6e12d85 | ||
|
|
5d680ff93f | ||
|
|
866b0e6ba4 | ||
|
|
5a49d20c9b | ||
|
|
7641d5993a | ||
|
|
07d66137cb | ||
|
|
f918dcd9c2 | ||
|
|
ed4e312cb1 | ||
|
|
0b539f868e | ||
|
|
8899832de9 | ||
|
|
d844aa323a | ||
|
|
ef640b0b5c | ||
|
|
4d43c6a3ea | ||
|
|
407c848145 | ||
|
|
d58260a0c0 | ||
|
|
16bcaf963d | ||
|
|
551dc51c52 | ||
|
|
0729061438 | ||
|
|
636d867544 | ||
|
|
cf9e5dc4f4 | ||
|
|
2c0acb1103 | ||
|
|
425bc202c2 | ||
|
|
3b24dca935 | ||
|
|
98db5f4984 | ||
|
|
2b56168ad0 | ||
|
|
b9255815e1 | ||
|
|
93e2f0f5f9 | ||
|
|
437c026da9 | ||
|
|
316079045f | ||
|
|
5ae9f8b288 | ||
|
|
bef65e1baa | ||
|
|
87a3cd8a9c | ||
|
|
be7b742891 | ||
|
|
1f98797a60 | ||
|
|
8505e32757 | ||
|
|
91297d1ccd | ||
|
|
0ea213edfd | ||
|
|
292f5d7e48 | ||
|
|
ef871f3b5c | ||
|
|
6b17da59f8 | ||
|
|
86d580edd8 | ||
|
|
cdbe37de7d | ||
|
|
f46974ddd9 | ||
|
|
a14d4243d9 | ||
|
|
90588ebae8 | ||
|
|
00bc02f49c | ||
|
|
7e9ce2e691 | ||
|
|
81ec0f0bd9 | ||
|
|
37f9716c4f | ||
|
|
4540cd9338 | ||
|
|
3c39d2f2f3 | ||
|
|
1e01ffc3d9 | ||
|
|
f4b1c43b55 | ||
|
|
02254938b4 | ||
|
|
054ff0cab2 | ||
|
|
4f898a0264 | ||
|
|
70d90c2c01 | ||
|
|
964143b30d | ||
|
|
ce9f59e7d6 | ||
|
|
1f958fc42a | ||
|
|
e6e53296a8 | ||
|
|
f533de8abd | ||
|
|
adc6427aab | ||
|
|
d034b78b48 | ||
|
|
6deda906de | ||
|
|
a063105de8 | ||
|
|
02b4215742 | ||
|
|
56b6e2113f | ||
|
|
4fcc78eb94 | ||
|
|
b28a4d7673 | ||
|
|
27383e4ff5 | ||
|
|
bf16f8916d | ||
|
|
75a62ac6aa | ||
|
|
bbc4aaa020 | ||
|
|
89e5d02fe2 | ||
|
|
a93973af06 | ||
|
|
15a9527866 | ||
|
|
9147d35c8c | ||
|
|
6a065797f5 | ||
|
|
dd08775c85 | ||
|
|
0cbe635ed8 | ||
|
|
e923f13e83 | ||
|
|
889da744ef | ||
|
|
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 | ||
|
|
f2d3c39425 | ||
|
|
e45d2d6fe7 | ||
|
|
4ddf84fa22 | ||
|
|
326a1883e8 | ||
|
|
9ff36d4498 | ||
|
|
98ef07ad32 | ||
|
|
802b0d456a | ||
|
|
6767f0e2e6 | ||
|
|
d72a05c965 | ||
|
|
ce3be1db8e | ||
|
|
e84ef04f15 | ||
|
|
adee3a2177 | ||
|
|
526e0aab9a | ||
|
|
2bbbbe764a | ||
|
|
ef5e921c08 | ||
|
|
a2fa59285a | ||
|
|
494942e87e | ||
|
|
aa2b76673f | ||
|
|
099880dd1c | ||
|
|
d906a103af | ||
|
|
4ed49d2d13 | ||
|
|
7b5ae4696f | ||
|
|
54ea301152 | ||
|
|
69c27232ac | ||
|
|
f36dcb3f5c | ||
|
|
63d088d62c | ||
|
|
a461f78224 | ||
|
|
c306432708 | ||
|
|
97f778cc90 | ||
|
|
88695ac6ad | ||
|
|
3b04329b04 | ||
|
|
bd4acf8121 | ||
|
|
b9a9b1efe6 | ||
|
|
c050f58ee9 | ||
|
|
d47375ae41 | ||
|
|
0602c6144d | ||
|
|
c84f0658fb | ||
|
|
a2902bd23a | ||
|
|
69a9b90711 | ||
|
|
b96402e11d | ||
|
|
0bc6f6a719 | ||
|
|
ebd65c8246 | ||
|
|
234bf708b3 | ||
|
|
6d5e741d94 | ||
|
|
f9bfe8f510 | ||
|
|
408c866dda | ||
|
|
bac41a46f8 | ||
|
|
8638c26db1 | ||
|
|
319ba13df1 | ||
|
|
0d1f05d307 | ||
|
|
e6608d18ce | ||
|
|
7afd8415cd | ||
|
|
845f2f0ff8 | ||
|
|
11d7792c0f | ||
|
|
6883ec34c6 | ||
|
|
4c45e7ff27 | ||
|
|
5621daea98 | ||
|
|
71d168dfea | ||
|
|
2d4365189d | ||
|
|
0c28a46e60 | ||
|
|
a4c7dae99c | ||
|
|
bfda0a226c | ||
|
|
c9bbb105a3 | ||
|
|
b415dc8912 | ||
|
|
05b74d71a3 | ||
|
|
10c0c691a7 | ||
|
|
0bc3786fc6 | ||
|
|
8e041ccb1b | ||
|
|
a82e9b7b9d | ||
|
|
7bab8cc636 | ||
|
|
7659b63d88 | ||
|
|
33ebc5723f | ||
|
|
86d9efd785 | ||
|
|
57ae7f03a6 | ||
|
|
6ee3983846 | ||
|
|
f05ed5cdda | ||
|
|
187dfa5344 | ||
|
|
918c8a9d33 |
@@ -3,7 +3,10 @@ __pycache__/
|
|||||||
.github/
|
.github/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.cursor/
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
file: CHANGELOG.md
|
file: CHANGELOG.md
|
||||||
pull_request: true
|
pull_request: true
|
||||||
|
commit_summary_template: 'Update the changelog for %s'
|
||||||
|
args: -c
|
||||||
github_token: ${{ secrets.GH_TOKEN }}
|
github_token: ${{ secrets.GH_TOKEN }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
name: lint ${{ matrix.python-version }}
|
name: lint ${{ matrix.python-version }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ venv/
|
|||||||
.python-version
|
.python-version
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
|
default_language_version:
|
||||||
|
python: '>= 3.10'
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
# - id: check-added-large-files
|
- id: end-of-file-fixer
|
||||||
# - id: end-of-file-fixer
|
- id: check-json
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
rev: v0.11.9
|
rev: v0.14.10
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff-check
|
||||||
args:
|
args:
|
||||||
# https://github.com/astral-sh/ruff-pre-commit/issues/64
|
# https://github.com/astral-sh/ruff-pre-commit/issues/64
|
||||||
- '--config'
|
|
||||||
- 'backend/.ruff.toml'
|
|
||||||
- '--fix'
|
- '--fix'
|
||||||
- '--unsafe-fixes'
|
- '--unsafe-fixes'
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
rev: 0.7.3
|
rev: 0.9.18
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
- id: uv-export
|
- 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"
|
||||||
+696
-1
@@ -1,3 +1,613 @@
|
|||||||
|
<a id="v1.12.0"></a>
|
||||||
|
# [v1.12.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.12.0) - 2025-12-15
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.11.2 by [@wu-clan](https://github.com/wu-clan) in [#942](https://github.com/fastapi-practices/fastapi_best_architecture/pull/942)
|
||||||
|
* Update celery related docker container independence by [@wu-clan](https://github.com/wu-clan) in [#943](https://github.com/fastapi-practices/fastapi_best_architecture/pull/943)
|
||||||
|
* Fix super value in update user permissions by [@wu-clan](https://github.com/wu-clan) in [#948](https://github.com/fastapi-practices/fastapi_best_architecture/pull/948)
|
||||||
|
* Optimize data permission logic and usage by [@wu-clan](https://github.com/wu-clan) in [#947](https://github.com/fastapi-practices/fastapi_best_architecture/pull/947)
|
||||||
|
* Update pre-commit to prek in pre-commit script by [@wu-clan](https://github.com/wu-clan) in [#949](https://github.com/fastapi-practices/fastapi_best_architecture/pull/949)
|
||||||
|
* Optimize the coupling of user social plugin by [@wu-clan](https://github.com/wu-clan) in [#950](https://github.com/fastapi-practices/fastapi_best_architecture/pull/950)
|
||||||
|
* Add the database primary key mode config by [@wu-clan](https://github.com/wu-clan) in [#953](https://github.com/fastapi-practices/fastapi_best_architecture/pull/953)
|
||||||
|
* Optimize the coupling of code generation CLI by [@wu-clan](https://github.com/wu-clan) in [#951](https://github.com/fastapi-practices/fastapi_best_architecture/pull/951)
|
||||||
|
* Add CLI init project database support by [@wu-clan](https://github.com/wu-clan) in [#952](https://github.com/fastapi-practices/fastapi_best_architecture/pull/952)
|
||||||
|
* Update the init project database CLI to subcommand by [@wu-clan](https://github.com/wu-clan) in [#954](https://github.com/fastapi-practices/fastapi_best_architecture/pull/954)
|
||||||
|
* Fix CLI command for code generation by [@wu-clan](https://github.com/wu-clan) in [#956](https://github.com/fastapi-practices/fastapi_best_architecture/pull/956)
|
||||||
|
* Fix the IP address in the request log by [@wuyuemushi](https://github.com/wuyuemushi) in [#959](https://github.com/fastapi-practices/fastapi_best_architecture/pull/959)
|
||||||
|
* Add the Grafana observability suite by [@wu-clan](https://github.com/wu-clan) in [#961](https://github.com/fastapi-practices/fastapi_best_architecture/pull/961)
|
||||||
|
* Update the version number to 1.12.0 by [@wu-clan](https://github.com/wu-clan) in [#962](https://github.com/fastapi-practices/fastapi_best_architecture/pull/962)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@wuyuemushi](https://github.com/wuyuemushi) made their first contribution in [#959](https://github.com/fastapi-practices/fastapi_best_architecture/pull/959)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.2...v1.12.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
<a href="https://github.com/wuyuemushi"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwuyuemushi.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wuyuemushi"></a>
|
||||||
|
|
||||||
|
[Changes][v1.12.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.11.2"></a>
|
||||||
|
# [v1.11.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.11.2) - 2025-11-28
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.11.1 by [@wu-clan](https://github.com/wu-clan) in [#923](https://github.com/fastapi-practices/fastapi_best_architecture/pull/923)
|
||||||
|
* Fix typos in data rule sql scripts by [@wu-clan](https://github.com/wu-clan) in [#926](https://github.com/fastapi-practices/fastapi_best_architecture/pull/926)
|
||||||
|
* Restore captcha uuid naming in the login params by [@wu-clan](https://github.com/wu-clan) in [#928](https://github.com/fastapi-practices/fastapi_best_architecture/pull/928)
|
||||||
|
* Add distributed deployment support for snowflake ID by [@downdawn](https://github.com/downdawn) in [#927](https://github.com/fastapi-practices/fastapi_best_architecture/pull/927)
|
||||||
|
* Add env reqs for plugin install and uninstall by [@wu-clan](https://github.com/wu-clan) in [#929](https://github.com/fastapi-practices/fastapi_best_architecture/pull/929)
|
||||||
|
* Optimize the use of some LRU caches by [@wu-clan](https://github.com/wu-clan) in [#932](https://github.com/fastapi-practices/fastapi_best_architecture/pull/932)
|
||||||
|
* Update the i18n language file init location by [@wu-clan](https://github.com/wu-clan) in [#934](https://github.com/fastapi-practices/fastapi_best_architecture/pull/934)
|
||||||
|
* Fix get column types in code generation by [@wu-clan](https://github.com/wu-clan) in [#935](https://github.com/fastapi-practices/fastapi_best_architecture/pull/935)
|
||||||
|
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#936](https://github.com/fastapi-practices/fastapi_best_architecture/pull/936)
|
||||||
|
* Update the files interface filename to file by [@wu-clan](https://github.com/wu-clan) in [#937](https://github.com/fastapi-practices/fastapi_best_architecture/pull/937)
|
||||||
|
* Update task application interface definitions by [@wu-clan](https://github.com/wu-clan) in [#938](https://github.com/fastapi-practices/fastapi_best_architecture/pull/938)
|
||||||
|
* Update code generation interface definitions by [@wu-clan](https://github.com/wu-clan) in [#939](https://github.com/fastapi-practices/fastapi_best_architecture/pull/939)
|
||||||
|
* Update the fba run CLI command output by [@wu-clan](https://github.com/wu-clan) in [#941](https://github.com/fastapi-practices/fastapi_best_architecture/pull/941)
|
||||||
|
* Update the version number to 1.11.2 by [@wu-clan](https://github.com/wu-clan) in [#940](https://github.com/fastapi-practices/fastapi_best_architecture/pull/940)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.1...v1.11.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.11.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.11.1"></a>
|
||||||
|
# [v1.11.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.11.1) - 2025-11-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.11.0 by [@wu-clan](https://github.com/wu-clan) in [#917](https://github.com/fastapi-practices/fastapi_best_architecture/pull/917)
|
||||||
|
* Fix missing table in alembic migration by [@wu-clan](https://github.com/wu-clan) in [#920](https://github.com/fastapi-practices/fastapi_best_architecture/pull/920)
|
||||||
|
* Add user social binding and unbinding by [@wu-clan](https://github.com/wu-clan) in [#919](https://github.com/fastapi-practices/fastapi_best_architecture/pull/919)
|
||||||
|
* Fix the user list query serialization by [@linrong](https://github.com/linrong) in [#921](https://github.com/fastapi-practices/fastapi_best_architecture/pull/921)
|
||||||
|
* Update user and login security configs by [@wu-clan](https://github.com/wu-clan) in [#922](https://github.com/fastapi-practices/fastapi_best_architecture/pull/922)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@linrong](https://github.com/linrong) made their first contribution in [#921](https://github.com/fastapi-practices/fastapi_best_architecture/pull/921)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.0...v1.11.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/linrong"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Flinrong.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@linrong"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.11.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.11.0"></a>
|
||||||
|
# [v1.11.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.11.0) - 2025-11-12
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.10.4 by [@wu-clan](https://github.com/wu-clan) in [#916](https://github.com/fastapi-practices/fastapi_best_architecture/pull/916)
|
||||||
|
* Refactor foreign keys and relationships to pure logic by [@wu-clan](https://github.com/wu-clan) in [#901](https://github.com/fastapi-practices/fastapi_best_architecture/pull/901)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.4...v1.11.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.11.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.10.4"></a>
|
||||||
|
# [v1.10.4](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.4) - 2025-11-12
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.10.3 by [@wu-clan](https://github.com/wu-clan) in [#895](https://github.com/fastapi-practices/fastapi_best_architecture/pull/895)
|
||||||
|
* Bump fastapi oauth2 from 0.0.1 to 0.0.2 by [@wu-clan](https://github.com/wu-clan) in [#896](https://github.com/fastapi-practices/fastapi_best_architecture/pull/896)
|
||||||
|
* Update the interface timing accuracy in log by [@wu-clan](https://github.com/wu-clan) in [#897](https://github.com/fastapi-practices/fastapi_best_architecture/pull/897)
|
||||||
|
* Optimize redis batch get and delete operations by [@wu-clan](https://github.com/wu-clan) in [#899](https://github.com/fastapi-practices/fastapi_best_architecture/pull/899)
|
||||||
|
* Update the time column type in the task by [@wu-clan](https://github.com/wu-clan) in [#900](https://github.com/fastapi-practices/fastapi_best_architecture/pull/900)
|
||||||
|
* Add the user social independent enum file by [@wu-clan](https://github.com/wu-clan) in [#902](https://github.com/fastapi-practices/fastapi_best_architecture/pull/902)
|
||||||
|
* Optimize the request params of the service layer by [@wu-clan](https://github.com/wu-clan) in [#903](https://github.com/fastapi-practices/fastapi_best_architecture/pull/903)
|
||||||
|
* Optimize the data permission condition build by [@wu-clan](https://github.com/wu-clan) in [#904](https://github.com/fastapi-practices/fastapi_best_architecture/pull/904)
|
||||||
|
* Fix response status codes in the request logs by [@wu-clan](https://github.com/wu-clan) in [#905](https://github.com/fastapi-practices/fastapi_best_architecture/pull/905)
|
||||||
|
* Add dept validation to user updates by [@wu-clan](https://github.com/wu-clan) in [#906](https://github.com/fastapi-practices/fastapi_best_architecture/pull/906)
|
||||||
|
* Update the version number to 1.10.4 by [@wu-clan](https://github.com/wu-clan) in [#907](https://github.com/fastapi-practices/fastapi_best_architecture/pull/907)
|
||||||
|
* Fix typo in the data permission prompt by [@wu-clan](https://github.com/wu-clan) in [#909](https://github.com/fastapi-practices/fastapi_best_architecture/pull/909)
|
||||||
|
* Fix user cache cleanup when operating data rules by [@wu-clan](https://github.com/wu-clan) in [#910](https://github.com/fastapi-practices/fastapi_best_architecture/pull/910)
|
||||||
|
* Fix create and delete department validations by [@wu-clan](https://github.com/wu-clan) in [#911](https://github.com/fastapi-practices/fastapi_best_architecture/pull/911)
|
||||||
|
* Fix the user menu sidebar parsing by [@wu-clan](https://github.com/wu-clan) in [#912](https://github.com/fastapi-practices/fastapi_best_architecture/pull/912)
|
||||||
|
* Add user social unbinding account interface by [@wu-clan](https://github.com/wu-clan) in [#913](https://github.com/fastapi-practices/fastapi_best_architecture/pull/913)
|
||||||
|
* Fix user cache cleanup when updating data scope by [@wu-clan](https://github.com/wu-clan) in [#915](https://github.com/fastapi-practices/fastapi_best_architecture/pull/915)
|
||||||
|
* Fix dept and menu table subqueries by [@wu-clan](https://github.com/wu-clan) in [#914](https://github.com/fastapi-practices/fastapi_best_architecture/pull/914)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.3...v1.10.4
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.10.4]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.10.3"></a>
|
||||||
|
# [v1.10.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.3) - 2025-10-30
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.10.2 by [@wu-clan](https://github.com/wu-clan) in [#873](https://github.com/fastapi-practices/fastapi_best_architecture/pull/873)
|
||||||
|
* Fix docker default database env variables by [@wu-clan](https://github.com/wu-clan) in [#874](https://github.com/fastapi-practices/fastapi_best_architecture/pull/874)
|
||||||
|
* Add port for fba sever in docker compose by [@wu-clan](https://github.com/wu-clan) in [#875](https://github.com/fastapi-practices/fastapi_best_architecture/pull/875)
|
||||||
|
* Update the container naming in docker scripts by [@wu-clan](https://github.com/wu-clan) in [#876](https://github.com/fastapi-practices/fastapi_best_architecture/pull/876)
|
||||||
|
* Fix the httpurl type compatibility with postgresql by [@wu-clan](https://github.com/wu-clan) in [#877](https://github.com/fastapi-practices/fastapi_best_architecture/pull/877)
|
||||||
|
* Update the default length of user email column by [@wu-clan](https://github.com/wu-clan) in [#878](https://github.com/fastapi-practices/fastapi_best_architecture/pull/878)
|
||||||
|
* Update the serializer of httpurl type by [@wu-clan](https://github.com/wu-clan) in [#879](https://github.com/fastapi-practices/fastapi_best_architecture/pull/879)
|
||||||
|
* Fix the OAuth2 link acquisition in HTTPS by [@wu-clan](https://github.com/wu-clan) in [#881](https://github.com/fastapi-practices/fastapi_best_architecture/pull/881)
|
||||||
|
* Add Google OAuth2 callback to opera log exclusion by [@wu-clan](https://github.com/wu-clan) in [#882](https://github.com/fastapi-practices/fastapi_best_architecture/pull/882)
|
||||||
|
* Update the length style of the model columns by [@wu-clan](https://github.com/wu-clan) in [#883](https://github.com/fastapi-practices/fastapi_best_architecture/pull/883)
|
||||||
|
* Bump dependencies to the latest version by [@wu-clan](https://github.com/wu-clan) in [#890](https://github.com/fastapi-practices/fastapi_best_architecture/pull/890)
|
||||||
|
* Fix import in code generation api template by [@wu-clan](https://github.com/wu-clan) in [#891](https://github.com/fastapi-practices/fastapi_best_architecture/pull/891)
|
||||||
|
* Fix celery compatibility with psycopg version by [@wu-clan](https://github.com/wu-clan) in [#892](https://github.com/fastapi-practices/fastapi_best_architecture/pull/892)
|
||||||
|
* Fix the venv pip availability in Linux by [@wu-clan](https://github.com/wu-clan) in [#893](https://github.com/fastapi-practices/fastapi_best_architecture/pull/893)
|
||||||
|
* Add the celery rabbitmq vhost config by [@wu-clan](https://github.com/wu-clan) in [#894](https://github.com/fastapi-practices/fastapi_best_architecture/pull/894)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.2...v1.10.3
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.10.3]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.10.2"></a>
|
||||||
|
# [v1.10.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.2) - 2025-10-21
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.10.1 by [@wu-clan](https://github.com/wu-clan) in [#869](https://github.com/fastapi-practices/fastapi_best_architecture/pull/869)
|
||||||
|
* Bump rtoml and uvicorn to support python 3.14 by [@wu-clan](https://github.com/wu-clan) in [#871](https://github.com/fastapi-practices/fastapi_best_architecture/pull/871)
|
||||||
|
* Optimize sqlalchemy types to simplify compatibility by [@wu-clan](https://github.com/wu-clan) in [#870](https://github.com/fastapi-practices/fastapi_best_architecture/pull/870)
|
||||||
|
* Bump fastapi to remove warning for python 3.14 by [@wu-clan](https://github.com/wu-clan) in [#872](https://github.com/fastapi-practices/fastapi_best_architecture/pull/872)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.1...v1.10.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.10.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.10.1"></a>
|
||||||
|
# [v1.10.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.1) - 2025-10-18
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.10.0 by [@wu-clan](https://github.com/wu-clan) in [#864](https://github.com/fastapi-practices/fastapi_best_architecture/pull/864)
|
||||||
|
* Fix tasks related to deleting db logs by [@wu-clan](https://github.com/wu-clan) in [#863](https://github.com/fastapi-practices/fastapi_best_architecture/pull/863)
|
||||||
|
* Fix the volume in the docker compose script by [@wu-clan](https://github.com/wu-clan) in [#865](https://github.com/fastapi-practices/fastapi_best_architecture/pull/865)
|
||||||
|
* Update the release changelog workflow by [@wu-clan](https://github.com/wu-clan) in [#867](https://github.com/fastapi-practices/fastapi_best_architecture/pull/867)
|
||||||
|
* Bump dependencies to the latest version by [@wu-clan](https://github.com/wu-clan) in [#868](https://github.com/fastapi-practices/fastapi_best_architecture/pull/868)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.0...v1.10.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.10.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.10.0"></a>
|
||||||
|
# [v1.10.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.10.0) - 2025-10-17
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Fix the import table cli command by [@wu-clan](https://github.com/wu-clan) in [#860](https://github.com/fastapi-practices/fastapi_best_architecture/pull/860)
|
||||||
|
* Update unique judgment logic for dict data by [@wu-clan](https://github.com/wu-clan) in [#859](https://github.com/fastapi-practices/fastapi_best_architecture/pull/859)
|
||||||
|
* Fix the api ninja in code generation by [@wu-clan](https://github.com/wu-clan) in [#858](https://github.com/fastapi-practices/fastapi_best_architecture/pull/858)
|
||||||
|
* Fix dict plugin pgsql init script missing by [@wu-clan](https://github.com/wu-clan) in [#857](https://github.com/fastapi-practices/fastapi_best_architecture/pull/857)
|
||||||
|
* Update the changelog for v1.9.0 by [@wu-clan](https://github.com/wu-clan) in [#856](https://github.com/fastapi-practices/fastapi_best_architecture/pull/856)
|
||||||
|
* Fix ctx in validation exception handler by [@wu-clan](https://github.com/wu-clan) in [#861](https://github.com/fastapi-practices/fastapi_best_architecture/pull/861)
|
||||||
|
* Update the primary database to postgresql by [@wu-clan](https://github.com/wu-clan) in [#829](https://github.com/fastapi-practices/fastapi_best_architecture/pull/829)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.9.0...v1.10.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.10.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.9.0"></a>
|
||||||
|
# [v1.9.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.9.0) - 2025-10-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.8.3 by [@wu-clan](https://github.com/wu-clan) in [#849](https://github.com/fastapi-practices/fastapi_best_architecture/pull/849)
|
||||||
|
* Bump fastapi and pydantic to the latest by [@wu-clan](https://github.com/wu-clan) in [#851](https://github.com/fastapi-practices/fastapi_best_architecture/pull/851)
|
||||||
|
* Update superuser verify to dependency injection by [@wu-clan](https://github.com/wu-clan) in [#852](https://github.com/fastapi-practices/fastapi_best_architecture/pull/852)
|
||||||
|
* Fix the superuser verify missing auth by [@wu-clan](https://github.com/wu-clan) in [#854](https://github.com/fastapi-practices/fastapi_best_architecture/pull/854)
|
||||||
|
* Update request state usage to context variable by [@wu-clan](https://github.com/wu-clan) in [#853](https://github.com/fastapi-practices/fastapi_best_architecture/pull/853)
|
||||||
|
* Fix safely of access ctx in exception handlers by [@wu-clan](https://github.com/wu-clan) in [#855](https://github.com/fastapi-practices/fastapi_best_architecture/pull/855)
|
||||||
|
* Refactor the service layer db session call method by [@wu-clan](https://github.com/wu-clan) in [#850](https://github.com/fastapi-practices/fastapi_best_architecture/pull/850)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.3...v1.9.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.9.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.8.3"></a>
|
||||||
|
# [v1.8.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.3) - 2025-10-13
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.8.2 by [@wu-clan](https://github.com/wu-clan) in [#841](https://github.com/fastapi-practices/fastapi_best_architecture/pull/841)
|
||||||
|
* Fix the pgsql script in dict plugin by [@wu-clan](https://github.com/wu-clan) in [#842](https://github.com/fastapi-practices/fastapi_best_architecture/pull/842)
|
||||||
|
* Fix SQL script syntax error in config plugin by [@wu-clan](https://github.com/wu-clan) in [#843](https://github.com/fastapi-practices/fastapi_best_architecture/pull/843)
|
||||||
|
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#845](https://github.com/fastapi-practices/fastapi_best_architecture/pull/845)
|
||||||
|
* Update the ruff rules and format the code by [@wu-clan](https://github.com/wu-clan) in [#846](https://github.com/fastapi-practices/fastapi_best_architecture/pull/846)
|
||||||
|
* Bump fastapi and sqlalchemy to support python 3.14 by [@wu-clan](https://github.com/wu-clan) in [#847](https://github.com/fastapi-practices/fastapi_best_architecture/pull/847)
|
||||||
|
* Bump sqlalchemy-crud-plus from 0.12.0 to 0.12.1 by [@wu-clan](https://github.com/wu-clan) in [#848](https://github.com/fastapi-practices/fastapi_best_architecture/pull/848)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.2...v1.8.3
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.8.3]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.8.2"></a>
|
||||||
|
# [v1.8.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.2) - 2025-09-26
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.8.1 by [@wu-clan](https://github.com/wu-clan) in [#804](https://github.com/fastapi-practices/fastapi_best_architecture/pull/804)
|
||||||
|
* Update the swagger docs version to dynamic by [@wu-clan](https://github.com/wu-clan) in [#805](https://github.com/fastapi-practices/fastapi_best_architecture/pull/805)
|
||||||
|
* Fix the subprocess ensurepip got stuck in Linux by [@byte-voyager](https://github.com/byte-voyager) in [#806](https://github.com/fastapi-practices/fastapi_best_architecture/pull/806)
|
||||||
|
* Update the offline location information parse by [@wu-clan](https://github.com/wu-clan) in [#807](https://github.com/fastapi-practices/fastapi_best_architecture/pull/807)
|
||||||
|
* Fix pgsql syntax error in code generation by [@byte-voyager](https://github.com/byte-voyager) in [#808](https://github.com/fastapi-practices/fastapi_best_architecture/pull/808)
|
||||||
|
* Fix summary default in opera log middleware by [@wu-clan](https://github.com/wu-clan) in [#809](https://github.com/fastapi-practices/fastapi_best_architecture/pull/809)
|
||||||
|
* Fix special character password support in alembic by [@MortyZhaoy](https://github.com/MortyZhaoy) in [#811](https://github.com/fastapi-practices/fastapi_best_architecture/pull/811)
|
||||||
|
* Update the startup progress information display by [@wu-clan](https://github.com/wu-clan) in [#812](https://github.com/fastapi-practices/fastapi_best_architecture/pull/812)
|
||||||
|
* Update the login captcha verify logic by [@wu-clan](https://github.com/wu-clan) in [#815](https://github.com/fastapi-practices/fastapi_best_architecture/pull/815)
|
||||||
|
* Fix the summary in opera log middleware by [@wu-clan](https://github.com/wu-clan) in [#816](https://github.com/fastapi-practices/fastapi_best_architecture/pull/816)
|
||||||
|
* Update the dict plugin table structure by [@wu-clan](https://github.com/wu-clan) in [#817](https://github.com/fastapi-practices/fastapi_best_architecture/pull/817)
|
||||||
|
* Add version number output to the startup CLI by [@wu-clan](https://github.com/wu-clan) in [#820](https://github.com/fastapi-practices/fastapi_best_architecture/pull/820)
|
||||||
|
* Update the user agent in opera log model by [@wu-clan](https://github.com/wu-clan) in [#831](https://github.com/fastapi-practices/fastapi_best_architecture/pull/831)
|
||||||
|
* Fix code generation template missing Decimal by [@wu-clan](https://github.com/wu-clan) in [#830](https://github.com/fastapi-practices/fastapi_best_architecture/pull/830)
|
||||||
|
* Fix the get model object method return logic by [@wu-clan](https://github.com/wu-clan) in [#832](https://github.com/fastapi-practices/fastapi_best_architecture/pull/832)
|
||||||
|
* Simplify the code generation CLI parameters by [@wu-clan](https://github.com/wu-clan) in [#837](https://github.com/fastapi-practices/fastapi_best_architecture/pull/837)
|
||||||
|
* Add the refresh token to demo mode exclusion by [@wu-clan](https://github.com/wu-clan) in [#834](https://github.com/fastapi-practices/fastapi_best_architecture/pull/834)
|
||||||
|
* Add extension parameters to paging data by [@wu-clan](https://github.com/wu-clan) in [#835](https://github.com/fastapi-practices/fastapi_best_architecture/pull/835)
|
||||||
|
* Fix the phone filter in the user list by [@wu-clan](https://github.com/wu-clan) in [#838](https://github.com/fastapi-practices/fastapi_best_architecture/pull/838)
|
||||||
|
* Update docker and deployment script comments by [@wu-clan](https://github.com/wu-clan) in [#839](https://github.com/fastapi-practices/fastapi_best_architecture/pull/839)
|
||||||
|
* Update the version number to 1.8.2 by [@wu-clan](https://github.com/wu-clan) in [#840](https://github.com/fastapi-practices/fastapi_best_architecture/pull/840)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@byte-voyager](https://github.com/byte-voyager) made their first contribution in [#806](https://github.com/fastapi-practices/fastapi_best_architecture/pull/806)
|
||||||
|
* [@MortyZhaoy](https://github.com/MortyZhaoy) made their first contribution in [#811](https://github.com/fastapi-practices/fastapi_best_architecture/pull/811)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.1...v1.8.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/MortyZhaoy"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FMortyZhaoy.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@MortyZhaoy"></a>
|
||||||
|
<a href="https://github.com/byte-voyager"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fbyte-voyager.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@byte-voyager"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.8.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.8.1"></a>
|
||||||
|
# [v1.8.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.1) - 2025-09-09
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.8.0 by [@wu-clan](https://github.com/wu-clan) in [#772](https://github.com/fastapi-practices/fastapi_best_architecture/pull/772)
|
||||||
|
* Optimize the celery crontab expression validation by [@yanlingsishao](https://github.com/yanlingsishao) in [#773](https://github.com/fastapi-practices/fastapi_best_architecture/pull/773)
|
||||||
|
* Add bulk update interface for config plugin by [@wu-clan](https://github.com/wu-clan) in [#774](https://github.com/fastapi-practices/fastapi_best_architecture/pull/774)
|
||||||
|
* Optimize email sending config update logic by [@wu-clan](https://github.com/wu-clan) in [#775](https://github.com/fastapi-practices/fastapi_best_architecture/pull/775)
|
||||||
|
* Add test data SQL script for config plugin by [@wu-clan](https://github.com/wu-clan) in [#776](https://github.com/fastapi-practices/fastapi_best_architecture/pull/776)
|
||||||
|
* Update the env parameter value in env file by [@wu-clan](https://github.com/wu-clan) in [#777](https://github.com/fastapi-practices/fastapi_best_architecture/pull/777)
|
||||||
|
* Fix the docker compose env configuration file by [@wu-clan](https://github.com/wu-clan) in [#778](https://github.com/fastapi-practices/fastapi_best_architecture/pull/778)
|
||||||
|
* Fix redis client not close after plugin parse by [@wu-clan](https://github.com/wu-clan) in [#780](https://github.com/fastapi-practices/fastapi_best_architecture/pull/780)
|
||||||
|
* Fix the celery beat distributed lock timeout by [@wu-clan](https://github.com/wu-clan) in [#779](https://github.com/fastapi-practices/fastapi_best_architecture/pull/779)
|
||||||
|
* Fix the plugin model object detection logic by [@wuyao4](https://github.com/wuyao4) in [#782](https://github.com/fastapi-practices/fastapi_best_architecture/pull/782)
|
||||||
|
* Update the celery task result table creation logic by [@wu-clan](https://github.com/wu-clan) in [#783](https://github.com/fastapi-practices/fastapi_best_architecture/pull/783)
|
||||||
|
* Fix code generation template params and filename by [@wu-clan](https://github.com/wu-clan) in [#784](https://github.com/fastapi-practices/fastapi_best_architecture/pull/784)
|
||||||
|
* Fix CLI openapi url in production environment by [@siyue-wang](https://github.com/siyue-wang) in [#785](https://github.com/fastapi-practices/fastapi_best_architecture/pull/785)
|
||||||
|
* Update the model datetime column type to custom by [@wu-clan](https://github.com/wu-clan) in [#786](https://github.com/fastapi-practices/fastapi_best_architecture/pull/786)
|
||||||
|
* Update the opera log exception message record by [@wu-clan](https://github.com/wu-clan) in [#788](https://github.com/fastapi-practices/fastapi_best_architecture/pull/788)
|
||||||
|
* Update the handling of CORS 500 status code by [@wu-clan](https://github.com/wu-clan) in [#789](https://github.com/fastapi-practices/fastapi_best_architecture/pull/789)
|
||||||
|
* Add the Google OAuth2 login by [@wu-clan](https://github.com/wu-clan) in [#790](https://github.com/fastapi-practices/fastapi_best_architecture/pull/790)
|
||||||
|
* Fix the tzinfo comparison in the timezone type by [@IAseven](https://github.com/IAseven) in [#787](https://github.com/fastapi-practices/fastapi_best_architecture/pull/787)
|
||||||
|
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#791](https://github.com/fastapi-practices/fastapi_best_architecture/pull/791)
|
||||||
|
* Update the notice model and pagination by [@wu-clan](https://github.com/wu-clan) in [#792](https://github.com/fastapi-practices/fastapi_best_architecture/pull/792)
|
||||||
|
* Add the dict type query all interface by [@wu-clan](https://github.com/wu-clan) in [#794](https://github.com/fastapi-practices/fastapi_best_architecture/pull/794)
|
||||||
|
* Fix sql script syntax error in config plugin by [@wu-clan](https://github.com/wu-clan) in [#793](https://github.com/fastapi-practices/fastapi_best_architecture/pull/793)
|
||||||
|
* Fix the psycopg dependency naming by [@wu-clan](https://github.com/wu-clan) in [#795](https://github.com/fastapi-practices/fastapi_best_architecture/pull/795)
|
||||||
|
* Add schema for the tree data by [@wu-clan](https://github.com/wu-clan) in [#796](https://github.com/fastapi-practices/fastapi_best_architecture/pull/796)
|
||||||
|
* Add the code generator to the CLI by [@wu-clan](https://github.com/wu-clan) in [#798](https://github.com/fastapi-practices/fastapi_best_architecture/pull/798)
|
||||||
|
* Optimize the code generation interaction logic by [@wu-clan](https://github.com/wu-clan) in [#799](https://github.com/fastapi-practices/fastapi_best_architecture/pull/799)
|
||||||
|
* Fix console import in main module by [@wu-clan](https://github.com/wu-clan) in [#800](https://github.com/fastapi-practices/fastapi_best_architecture/pull/800)
|
||||||
|
* Fix the subprocess check call failed in windows by [@yzbf-lin](https://github.com/yzbf-lin) in [#802](https://github.com/fastapi-practices/fastapi_best_architecture/pull/802)
|
||||||
|
* Fix alembic migration file missing import by [@wu-clan](https://github.com/wu-clan) in [#803](https://github.com/fastapi-practices/fastapi_best_architecture/pull/803)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@yanlingsishao](https://github.com/yanlingsishao) made their first contribution in [#773](https://github.com/fastapi-practices/fastapi_best_architecture/pull/773)
|
||||||
|
* [@wuyao4](https://github.com/wuyao4) made their first contribution in [#782](https://github.com/fastapi-practices/fastapi_best_architecture/pull/782)
|
||||||
|
* [@siyue-wang](https://github.com/siyue-wang) made their first contribution in [#785](https://github.com/fastapi-practices/fastapi_best_architecture/pull/785)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.0...v1.8.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/IAseven"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FIAseven.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@IAseven"></a>
|
||||||
|
<a href="https://github.com/siyue-wang"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fsiyue-wang.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@siyue-wang"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
<a href="https://github.com/wuyao4"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwuyao4.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wuyao4"></a>
|
||||||
|
<a href="https://github.com/yanlingsishao"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fyanlingsishao.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@yanlingsishao"></a>
|
||||||
|
<a href="https://github.com/yzbf-lin"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fyzbf-lin.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@yzbf-lin"></a>
|
||||||
|
|
||||||
|
[Changes][v1.8.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.8.0"></a>
|
||||||
|
# [v1.8.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.0) - 2025-08-15
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/IAseven"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FIAseven.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@IAseven"></a>
|
||||||
|
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.8.0]
|
||||||
|
|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.7.0]
|
||||||
|
|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.5.2 by [@wu-clan](https://github.com/wu-clan) in [#690](https://github.com/fastapi-practices/fastapi_best_architecture/pull/690)
|
||||||
|
* Optimize dict create and update logic by [@wu-clan](https://github.com/wu-clan) in [#691](https://github.com/fastapi-practices/fastapi_best_architecture/pull/691)
|
||||||
|
* Fix the OAuth2 redirect route names by [@wu-clan](https://github.com/wu-clan) in [#693](https://github.com/fastapi-practices/fastapi_best_architecture/pull/693)
|
||||||
|
* Update the SQL to adapt frontend plugin by [@wu-clan](https://github.com/wu-clan) in [#694](https://github.com/fastapi-practices/fastapi_best_architecture/pull/694)
|
||||||
|
* Update the extension plugin config by [@wu-clan](https://github.com/wu-clan) in [#695](https://github.com/fastapi-practices/fastapi_best_architecture/pull/695)
|
||||||
|
* Add the test user to SQL scripts by [@wu-clan](https://github.com/wu-clan) in [#696](https://github.com/fastapi-practices/fastapi_best_architecture/pull/696)
|
||||||
|
* Add custom CLI for service startup by [@wu-clan](https://github.com/wu-clan) in [#697](https://github.com/fastapi-practices/fastapi_best_architecture/pull/697)
|
||||||
|
* Add CLI support for plugin install by [@wu-clan](https://github.com/wu-clan) in [#698](https://github.com/fastapi-practices/fastapi_best_architecture/pull/698)
|
||||||
|
* Update the help for CLI run worker by [@wu-clan](https://github.com/wu-clan) in [#699](https://github.com/fastapi-practices/fastapi_best_architecture/pull/699)
|
||||||
|
* Optimize the installation of plugin dependencies by [@wu-clan](https://github.com/wu-clan) in [#700](https://github.com/fastapi-practices/fastapi_best_architecture/pull/700)
|
||||||
|
* Update the Dockerfile to adapt latest code by [@wu-clan](https://github.com/wu-clan) in [#701](https://github.com/fastapi-practices/fastapi_best_architecture/pull/701)
|
||||||
|
* Update the version number to 1.6.0 by [@wu-clan](https://github.com/wu-clan) in [#702](https://github.com/fastapi-practices/fastapi_best_architecture/pull/702)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.6.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.5.2"></a>
|
||||||
|
# [v1.5.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.2) - 2025-06-24
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.5.1 by [@wu-clan](https://github.com/wu-clan) in [#671](https://github.com/fastapi-practices/fastapi_best_architecture/pull/671)
|
||||||
|
* Fix some error class import by [@wu-clan](https://github.com/wu-clan) in [#672](https://github.com/fastapi-practices/fastapi_best_architecture/pull/672)
|
||||||
|
* Optimize routes to better align with RESTful by [@wu-clan](https://github.com/wu-clan) in [#673](https://github.com/fastapi-practices/fastapi_best_architecture/pull/673)
|
||||||
|
* Add the snowflake ID sql script by [@wu-clan](https://github.com/wu-clan) in [#675](https://github.com/fastapi-practices/fastapi_best_architecture/pull/675)
|
||||||
|
* Optimize token detection and caching logic by [@wu-clan](https://github.com/wu-clan) in [#677](https://github.com/fastapi-practices/fastapi_best_architecture/pull/677)
|
||||||
|
* Update cache cleanup for logout interface by [@wu-clan](https://github.com/wu-clan) in [#678](https://github.com/fastapi-practices/fastapi_best_architecture/pull/678)
|
||||||
|
* Add dictionary type and datas queries by [@wu-clan](https://github.com/wu-clan) in [#679](https://github.com/fastapi-practices/fastapi_best_architecture/pull/679)
|
||||||
|
* Optimize api with semantic HTTP status codes by [@downdawn](https://github.com/downdawn) in [#681](https://github.com/fastapi-practices/fastapi_best_architecture/pull/681)
|
||||||
|
* Fix the code with outdated system config by [@wu-clan](https://github.com/wu-clan) in [#683](https://github.com/fastapi-practices/fastapi_best_architecture/pull/683)
|
||||||
|
* Update dict data label column config by [@wu-clan](https://github.com/wu-clan) in [#684](https://github.com/fastapi-practices/fastapi_best_architecture/pull/684)
|
||||||
|
* Update the init test data for SQL scripts by [@wu-clan](https://github.com/wu-clan) in [#685](https://github.com/fastapi-practices/fastapi_best_architecture/pull/685)
|
||||||
|
* Simplify custom response status codes by [@wu-clan](https://github.com/wu-clan) in [#686](https://github.com/fastapi-practices/fastapi_best_architecture/pull/686)
|
||||||
|
* Optimize the zip plug-in file name parsing by [@wu-clan](https://github.com/wu-clan) in [#687](https://github.com/fastapi-practices/fastapi_best_architecture/pull/687)
|
||||||
|
* Add built-in plugin missing files by [@wu-clan](https://github.com/wu-clan) in [#688](https://github.com/fastapi-practices/fastapi_best_architecture/pull/688)
|
||||||
|
* Update the dict pagination query parameters by [@wu-clan](https://github.com/wu-clan) in [#689](https://github.com/fastapi-practices/fastapi_best_architecture/pull/689)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.5.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.5.1"></a>
|
||||||
|
# [v1.5.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.1) - 2025-06-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.5.0 by [@wu-clan](https://github.com/wu-clan) in [#664](https://github.com/fastapi-practices/fastapi_best_architecture/pull/664)
|
||||||
|
* Fix the sidebar menu type filtering by [@wu-clan](https://github.com/wu-clan) in [#667](https://github.com/fastapi-practices/fastapi_best_architecture/pull/667)
|
||||||
|
* Bump sqlalchemy crud plus version to 1.10.0 by [@wu-clan](https://github.com/wu-clan) in [#668](https://github.com/fastapi-practices/fastapi_best_architecture/pull/668)
|
||||||
|
* Fix the postgresql sql script syntax error by [@downdawn](https://github.com/downdawn) in [#669](https://github.com/fastapi-practices/fastapi_best_architecture/pull/669)
|
||||||
|
* Add Initial Snowflake ID Support by [@downdawn](https://github.com/downdawn) in [#670](https://github.com/fastapi-practices/fastapi_best_architecture/pull/670)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.5.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.5.0"></a>
|
||||||
|
# [v1.5.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.0) - 2025-06-09
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.3 by [@wu-clan](https://github.com/wu-clan) in [#651](https://github.com/fastapi-practices/fastapi_best_architecture/pull/651)
|
||||||
|
* Update OAuth2 callback interface return by [@wu-clan](https://github.com/wu-clan) in [#653](https://github.com/fastapi-practices/fastapi_best_architecture/pull/653)
|
||||||
|
* Update user email and phone operation logic by [@wu-clan](https://github.com/wu-clan) in [#654](https://github.com/fastapi-practices/fastapi_best_architecture/pull/654)
|
||||||
|
* Simplify OAuth2 model and optimize auth service by [@wu-clan](https://github.com/wu-clan) in [#655](https://github.com/fastapi-practices/fastapi_best_architecture/pull/655)
|
||||||
|
* Add OAuth2 user to auto bind a role by [@wu-clan](https://github.com/wu-clan) in [#656](https://github.com/fastapi-practices/fastapi_best_architecture/pull/656)
|
||||||
|
* Update data scope and rule to m2m by [@wu-clan](https://github.com/wu-clan) in [#657](https://github.com/fastapi-practices/fastapi_best_architecture/pull/657)
|
||||||
|
* Update code generate interface permission by [@wu-clan](https://github.com/wu-clan) in [#658](https://github.com/fastapi-practices/fastapi_best_architecture/pull/658)
|
||||||
|
* Update the plugin download interface permission by [@wu-clan](https://github.com/wu-clan) in [#659](https://github.com/fastapi-practices/fastapi_best_architecture/pull/659)
|
||||||
|
* Update auth failed default status code by [@wu-clan](https://github.com/wu-clan) in [#660](https://github.com/fastapi-practices/fastapi_best_architecture/pull/660)
|
||||||
|
* Update menu sort in init test sql by [@wu-clan](https://github.com/wu-clan) in [#661](https://github.com/fastapi-practices/fastapi_best_architecture/pull/661)
|
||||||
|
* Add data permission in init test sql by [@wu-clan](https://github.com/wu-clan) in [#662](https://github.com/fastapi-practices/fastapi_best_architecture/pull/662)
|
||||||
|
* Update the version to 1.5.0 by [@wu-clan](https://github.com/wu-clan) in [#663](https://github.com/fastapi-practices/fastapi_best_architecture/pull/663)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.5.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.4.3"></a>
|
||||||
|
# [v1.4.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.3) - 2025-06-02
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.2 by [@wu-clan](https://github.com/wu-clan) in [#639](https://github.com/fastapi-practices/fastapi_best_architecture/pull/639)
|
||||||
|
* Fix the role update business variables by [@wu-clan](https://github.com/wu-clan) in [#640](https://github.com/fastapi-practices/fastapi_best_architecture/pull/640)
|
||||||
|
* Fix the menu delete interface arg description by [@wu-clan](https://github.com/wu-clan) in [#641](https://github.com/fastapi-practices/fastapi_best_architecture/pull/641)
|
||||||
|
* Fix the filter of query all menus by [@wu-clan](https://github.com/wu-clan) in [#642](https://github.com/fastapi-practices/fastapi_best_architecture/pull/642)
|
||||||
|
* Refactor routes to better align with RESTful by [@wu-clan](https://github.com/wu-clan) in [#645](https://github.com/fastapi-practices/fastapi_best_architecture/pull/645)
|
||||||
|
* Update the server startup time to string by [@wu-clan](https://github.com/wu-clan) in [#646](https://github.com/fastapi-practices/fastapi_best_architecture/pull/646)
|
||||||
|
* Add get all data scope rules interface by [@wu-clan](https://github.com/wu-clan) in [#647](https://github.com/fastapi-practices/fastapi_best_architecture/pull/647)
|
||||||
|
* Add data permission condition for filter data by [@wu-clan](https://github.com/wu-clan) in [#648](https://github.com/fastapi-practices/fastapi_best_architecture/pull/648)
|
||||||
|
* Update default value for role filter scopes by [@wu-clan](https://github.com/wu-clan) in [#649](https://github.com/fastapi-practices/fastapi_best_architecture/pull/649)
|
||||||
|
* Fix data permission condition for filter data by [@wu-clan](https://github.com/wu-clan) in [#650](https://github.com/fastapi-practices/fastapi_best_architecture/pull/650)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.4.3]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.4.2"></a>
|
||||||
|
# [v1.4.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.2) - 2025-05-29
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.1 by [@wu-clan](https://github.com/wu-clan) in [#630](https://github.com/fastapi-practices/fastapi_best_architecture/pull/630)
|
||||||
|
* Update non-linked sidebar support by [@wu-clan](https://github.com/wu-clan) in [#633](https://github.com/fastapi-practices/fastapi_best_architecture/pull/633)
|
||||||
|
* Update the captcha invalidation error class by [@wu-clan](https://github.com/wu-clan) in [#634](https://github.com/fastapi-practices/fastapi_best_architecture/pull/634)
|
||||||
|
* Optimize role-related data processing performance by [@wu-clan](https://github.com/wu-clan) in [#635](https://github.com/fastapi-practices/fastapi_best_architecture/pull/635)
|
||||||
|
* Optimize install and build of plugin zip by [@wu-clan](https://github.com/wu-clan) in [#636](https://github.com/fastapi-practices/fastapi_best_architecture/pull/636)
|
||||||
|
* Fix auto-increment id for postgres init data by [@huyuwei1996](https://github.com/huyuwei1996) in [#632](https://github.com/fastapi-practices/fastapi_best_architecture/pull/632)
|
||||||
|
* Fix: prevent overwriting existing init files in code generator by [@lin-wu-1990](https://github.com/lin-wu-1990) in [#637](https://github.com/fastapi-practices/fastapi_best_architecture/pull/637)
|
||||||
|
* Simplify the user info update business by [@wu-clan](https://github.com/wu-clan) in [#638](https://github.com/fastapi-practices/fastapi_best_architecture/pull/638)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@lin-wu-1990](https://github.com/lin-wu-1990) made their first contribution in [#637](https://github.com/fastapi-practices/fastapi_best_architecture/pull/637)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
|
[Changes][v1.4.2]
|
||||||
|
|
||||||
|
|
||||||
<a id="v1.4.1"></a>
|
<a id="v1.4.1"></a>
|
||||||
# [v1.4.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.1) - 2025-05-25
|
# [v1.4.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.1) - 2025-05-25
|
||||||
|
|
||||||
@@ -12,6 +622,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.4.1]
|
[Changes][v1.4.1]
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +647,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.4.0]
|
[Changes][v1.4.0]
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +668,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.3.0]
|
[Changes][v1.3.0]
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +689,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.2.0]
|
[Changes][v1.2.0]
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +710,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.1.2]
|
[Changes][v1.1.2]
|
||||||
|
|
||||||
|
|
||||||
@@ -98,6 +728,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.1.1]
|
[Changes][v1.1.1]
|
||||||
|
|
||||||
|
|
||||||
@@ -117,6 +751,11 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.1.0]
|
[Changes][v1.1.0]
|
||||||
|
|
||||||
|
|
||||||
@@ -141,6 +780,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.0.5]
|
[Changes][v1.0.5]
|
||||||
|
|
||||||
|
|
||||||
@@ -169,6 +812,12 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/PoetryL"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FPoetryL.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@PoetryL"></a>
|
||||||
|
<a href="https://github.com/ThankCat"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FThankCat.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@ThankCat"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.0.4]
|
[Changes][v1.0.4]
|
||||||
|
|
||||||
|
|
||||||
@@ -187,6 +836,11 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.0.3]
|
[Changes][v1.0.3]
|
||||||
|
|
||||||
|
|
||||||
@@ -202,6 +856,10 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.0.2]
|
[Changes][v1.0.2]
|
||||||
|
|
||||||
|
|
||||||
@@ -221,6 +879,11 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/huyuwei1996"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fhuyuwei1996.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@huyuwei1996"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
|
||||||
[Changes][v1.0.1]
|
[Changes][v1.0.1]
|
||||||
|
|
||||||
|
|
||||||
@@ -546,9 +1209,41 @@
|
|||||||
|
|
||||||
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/commits/v1.0.0
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/commits/v1.0.0
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/Meepoljdx"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2FMeepoljdx.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@Meepoljdx"></a>
|
||||||
|
<a href="https://github.com/dependabot"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdependabot.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@dependabot"></a>
|
||||||
|
<a href="https://github.com/dividduang"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdividduang.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@dividduang"></a>
|
||||||
|
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
|
||||||
|
<a href="https://github.com/obrua"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fobrua.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@obrua"></a>
|
||||||
|
<a href="https://github.com/qhp13654398483"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fqhp13654398483.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@qhp13654398483"></a>
|
||||||
|
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
|
||||||
|
<a href="https://github.com/yshan2028"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fyshan2028.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@yshan2028"></a>
|
||||||
|
|
||||||
[Changes][v1.0.0]
|
[Changes][v1.0.0]
|
||||||
|
|
||||||
|
|
||||||
|
[v1.12.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.2...v1.12.0
|
||||||
|
[v1.11.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.1...v1.11.2
|
||||||
|
[v1.11.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.0...v1.11.1
|
||||||
|
[v1.11.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.4...v1.11.0
|
||||||
|
[v1.10.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.3...v1.10.4
|
||||||
|
[v1.10.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.2...v1.10.3
|
||||||
|
[v1.10.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.1...v1.10.2
|
||||||
|
[v1.10.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.0...v1.10.1
|
||||||
|
[v1.10.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.9.0...v1.10.0
|
||||||
|
[v1.9.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.3...v1.9.0
|
||||||
|
[v1.8.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.2...v1.8.3
|
||||||
|
[v1.8.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.1...v1.8.2
|
||||||
|
[v1.8.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.0...v1.8.1
|
||||||
|
[v1.8.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.7.0...v1.8.0
|
||||||
|
[v1.7.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
|
||||||
|
[v1.6.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
|
||||||
|
[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
|
||||||
|
[v1.5.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.0
|
||||||
|
[v1.4.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
|
||||||
|
[v1.4.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
|
||||||
[v1.4.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
[v1.4.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
||||||
[v1.4.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
[v1.4.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
||||||
[v1.3.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
[v1.3.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
||||||
@@ -563,4 +1258,4 @@
|
|||||||
[v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
|
[v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
|
||||||
[v1.0.0]: https://github.com/fastapi-practices/fastapi_best_architecture/tree/v1.0.0
|
[v1.0.0]: https://github.com/fastapi-practices/fastapi_best_architecture/tree/v1.0.0
|
||||||
|
|
||||||
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.0 -->
|
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.1 -->
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
## Backend
|
||||||
|
|
||||||
|
1. Prerequisites
|
||||||
|
|
||||||
|
- Python >= 3.10
|
||||||
|
- Git
|
||||||
|
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
|
- Fork this repository to your GitHub account
|
||||||
|
|
||||||
|
2. Installation and setup
|
||||||
|
|
||||||
|
Clone your forked repository:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/<your account>/fastapi_best_architecture.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the root directory of the project, open the terminal, and run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Checkout
|
||||||
|
|
||||||
|
Checkout a new branch and make your changes
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git checkout -b your-new-feature-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Format and Lint
|
||||||
|
|
||||||
|
Auto-formatting and lint via `prek`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
prek run --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Commit and push
|
||||||
|
|
||||||
|
Commit your changes and push your branch to the GitHub.
|
||||||
|
|
||||||
|
6. PR
|
||||||
|
|
||||||
|
Create a PR via GitHub
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> The following script may not apply to the Windows platform
|
||||||
|
>
|
||||||
|
> It is recommended to execute under the backend directory, and chmod authorization may be required
|
||||||
|
|
||||||
|
- `migrate.sh`: Perform automatic database migration
|
||||||
|
|
||||||
|
- `scripts/format.sh`: Perform ruff format check
|
||||||
|
|
||||||
|
- `scripts/lint.sh`: Perform prek formatting
|
||||||
|
|
||||||
|
- `scripts/export.sh`: Execute uv export dependency package
|
||||||
+35
-26
@@ -1,5 +1,5 @@
|
|||||||
# Select the image to build based on SERVER_TYPE, defaulting to fastapi_server, or docker-compose build args
|
# Select the image to build based on SERVER_TYPE, defaulting to fba_server, or docker-compose build args
|
||||||
ARG SERVER_TYPE=fastapi_server
|
ARG SERVER_TYPE=fba_server
|
||||||
|
|
||||||
# === Python environment from uv ===
|
# === Python environment from uv ===
|
||||||
FROM ghcr.io/astral-sh/uv:python3.10-bookworm-slim AS builder
|
FROM ghcr.io/astral-sh/uv:python3.10-bookworm-slim AS builder
|
||||||
@@ -10,6 +10,10 @@ RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debi
|
|||||||
&& apt-get install -y --no-install-recommends gcc python3-dev \
|
&& apt-get install -y --no-install-recommends gcc python3-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . /fba
|
||||||
|
|
||||||
|
WORKDIR /fba
|
||||||
|
|
||||||
# Configure uv environment
|
# Configure uv environment
|
||||||
ENV UV_COMPILE_BYTECODE=1 \
|
ENV UV_COMPILE_BYTECODE=1 \
|
||||||
UV_NO_CACHE=1 \
|
UV_NO_CACHE=1 \
|
||||||
@@ -18,58 +22,63 @@ ENV UV_COMPILE_BYTECODE=1 \
|
|||||||
|
|
||||||
# Install dependencies with cache
|
# Install dependencies with cache
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
|
||||||
uv sync --frozen --no-default-groups --group server
|
uv sync --frozen --no-default-groups --group server
|
||||||
|
|
||||||
# === Runtime base server image ===
|
# === Runtime base server image ===
|
||||||
FROM python:3.10-slim AS base_server
|
FROM python:3.10-slim AS base_server
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-c"]
|
|
||||||
|
|
||||||
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends supervisor \
|
&& apt-get install -y --no-install-recommends supervisor \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY . /fba
|
COPY --from=builder /fba /fba
|
||||||
|
|
||||||
COPY --from=builder /usr/local /usr/local
|
COPY --from=builder /usr/local /usr/local
|
||||||
|
|
||||||
# Install plugin dependencies
|
COPY deploy/backend/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
WORKDIR /fba
|
|
||||||
ENV PYTHONPATH=/fba
|
WORKDIR /fba/backend
|
||||||
RUN python3 backend/scripts/init_plugin.py
|
|
||||||
|
|
||||||
# === FastAPI server image ===
|
# === FastAPI server image ===
|
||||||
FROM base_server AS fastapi_server
|
FROM base_server AS fba_server
|
||||||
|
|
||||||
WORKDIR /fba
|
COPY deploy/backend/supervisor/fba_server.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
|
RUN mkdir -p /var/log/fba
|
||||||
COPY deploy/backend/fastapi_server.conf /etc/supervisor/conf.d/
|
|
||||||
|
|
||||||
RUN mkdir -p /var/log/fastapi_server
|
|
||||||
|
|
||||||
EXPOSE 8001
|
EXPOSE 8001
|
||||||
|
|
||||||
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port","8000"]
|
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|
||||||
# === Celery server image ===
|
# === Celery Worker image ===
|
||||||
FROM base_server AS celery
|
FROM base_server AS fba_celery_worker
|
||||||
|
|
||||||
WORKDIR /fba/backend/
|
COPY deploy/backend/supervisor/fba_celery_worker.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
|
RUN mkdir -p /var/log/fba
|
||||||
COPY deploy/backend/celery.conf /etc/supervisor/conf.d/
|
|
||||||
|
|
||||||
RUN mkdir -p /var/log/celery
|
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|
||||||
RUN chmod +x celery-start.sh
|
# === Celery Beat image ===
|
||||||
|
FROM base_server AS fba_celery_beat
|
||||||
|
|
||||||
|
COPY deploy/backend/supervisor/fba_celery_beat.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log/fba
|
||||||
|
|
||||||
|
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|
||||||
|
# === Celery Flower image ===
|
||||||
|
FROM base_server AS fba_celery_flower
|
||||||
|
|
||||||
|
COPY deploy/backend/supervisor/fba_celery_flower.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log/fba
|
||||||
|
|
||||||
EXPOSE 8555
|
EXPOSE 8555
|
||||||
|
|
||||||
CMD ["./celery-start.sh"]
|
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|
||||||
# Build image
|
# Build image
|
||||||
FROM ${SERVER_TYPE}
|
FROM ${SERVER_TYPE}
|
||||||
|
|||||||
+15
-12
@@ -1,10 +1,10 @@
|
|||||||
# Env: dev、pro
|
# Env
|
||||||
ENVIRONMENT='dev'
|
ENVIRONMENT='dev'
|
||||||
# Database
|
# Database
|
||||||
DATABASE_TYPE='mysql'
|
DATABASE_TYPE='postgresql'
|
||||||
DATABASE_HOST='127.0.0.1'
|
DATABASE_HOST='127.0.0.1'
|
||||||
DATABASE_PORT=3306
|
DATABASE_PORT=5432
|
||||||
DATABASE_USER='root'
|
DATABASE_USER='postgres'
|
||||||
DATABASE_PASSWORD='123456'
|
DATABASE_PASSWORD='123456'
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_HOST='127.0.0.1'
|
REDIS_HOST='127.0.0.1'
|
||||||
@@ -15,18 +15,21 @@ REDIS_DATABASE=0
|
|||||||
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
||||||
# Opera Log
|
# Opera Log
|
||||||
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
||||||
# App Admin
|
# [ App ] task
|
||||||
# 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
|
|
||||||
# Celery
|
# Celery
|
||||||
CELERY_BROKER_REDIS_DATABASE=1
|
CELERY_BROKER_REDIS_DATABASE=1
|
||||||
CELERY_BACKEND_REDIS_DATABASE=2
|
|
||||||
# Rabbitmq
|
# Rabbitmq
|
||||||
CELERY_RABBITMQ_HOST='127.0.0.1'
|
CELERY_RABBITMQ_HOST='127.0.0.1'
|
||||||
CELERY_RABBITMQ_PORT=5672
|
CELERY_RABBITMQ_PORT=5672
|
||||||
CELERY_RABBITMQ_USERNAME='guest'
|
CELERY_RABBITMQ_USERNAME='guest'
|
||||||
CELERY_RABBITMQ_PASSWORD='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
|
|
||||||
@@ -16,68 +16,3 @@
|
|||||||
```shell
|
```shell
|
||||||
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
|
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Prerequisites
|
|
||||||
|
|
||||||
- Python >= 3.10
|
|
||||||
- Git
|
|
||||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
|
||||||
- Fork this repository to your GitHub account
|
|
||||||
|
|
||||||
2. Installation and setup
|
|
||||||
|
|
||||||
Clone your forked repository:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git clone https://github.com/<your account>/fastapi_best_architecture.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Go to the root directory of the project, open the terminal, and run the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
uv sync --frozen
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Checkout
|
|
||||||
|
|
||||||
Checkout a new branch and make your changes
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git checkout -b your-new-feature-branch
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Format and Lint
|
|
||||||
|
|
||||||
Auto-formatting and lint via `pre-commit`
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pre-commit run --all-files
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Commit and push
|
|
||||||
|
|
||||||
Commit your changes and push your branch to the GitHub.
|
|
||||||
|
|
||||||
6. PR
|
|
||||||
|
|
||||||
Create a PR via GitHub
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
>
|
|
||||||
> The following script may not apply to the Windows platform
|
|
||||||
>
|
|
||||||
> It is recommended to execute under the backend directory, and chmod authorization may be required
|
|
||||||
|
|
||||||
- `pre_start.sh`: Perform automatic database migration and create database tables
|
|
||||||
|
|
||||||
- `celery-start.sh`: For celery docker script, implementation is not recommended
|
|
||||||
|
|
||||||
- `scripts/format.sh`: Perform ruff format check
|
|
||||||
|
|
||||||
- `scripts/lint.sh`: Perform pre-commit formatting
|
|
||||||
|
|
||||||
- `scripts/export.sh`: Execute uv export dependency package
|
|
||||||
|
|||||||
+17
-2
@@ -1,2 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
from backend.utils.import_parse import get_all_models
|
||||||
|
|
||||||
|
# import all models for auto create db tables
|
||||||
|
for cls in get_all_models():
|
||||||
|
if isinstance(cls, sa.Table):
|
||||||
|
table_name = cls.name
|
||||||
|
if table_name not in globals():
|
||||||
|
globals()[table_name] = cls
|
||||||
|
else:
|
||||||
|
class_name = cls.__name__
|
||||||
|
if class_name not in globals():
|
||||||
|
globals()[class_name] = cls
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = '1.12.0'
|
||||||
|
|||||||
+7
-22
@@ -1,9 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# ruff: noqa: F403, F401, I001, RUF100
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
@@ -11,24 +8,9 @@ from sqlalchemy import pool
|
|||||||
from sqlalchemy.engine import Connection
|
from sqlalchemy.engine import Connection
|
||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
|
|
||||||
sys.path.append('../')
|
|
||||||
|
|
||||||
from backend.common.model import MappedBase
|
from backend.common.model import MappedBase
|
||||||
from backend.core import path_conf
|
from backend.core import path_conf
|
||||||
from backend.database.db import SQLALCHEMY_DATABASE_URL
|
from backend.database.db import SQLALCHEMY_DATABASE_URL
|
||||||
from backend.plugin.tools import get_plugin_models
|
|
||||||
|
|
||||||
# import your new model here
|
|
||||||
from backend.app.admin.model import * # noqa: F401
|
|
||||||
from backend.plugin.code_generator.model import * # noqa: F401
|
|
||||||
|
|
||||||
# import plugin model
|
|
||||||
for cls in get_plugin_models():
|
|
||||||
class_name = cls.__name__
|
|
||||||
if class_name in globals():
|
|
||||||
print(f'\nWarning: Class "{class_name}" already exists in global namespace.')
|
|
||||||
else:
|
|
||||||
globals()[class_name] = cls
|
|
||||||
|
|
||||||
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
|
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
|
||||||
os.makedirs(path_conf.ALEMBIC_VERSION_DIR)
|
os.makedirs(path_conf.ALEMBIC_VERSION_DIR)
|
||||||
@@ -47,10 +29,13 @@ if alembic_config.config_file_name is not None:
|
|||||||
target_metadata = MappedBase.metadata
|
target_metadata = MappedBase.metadata
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# 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.
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
This configures the context with just a URL
|
This configures the context with just a URL
|
||||||
@@ -79,7 +64,7 @@ def run_migrations_offline():
|
|||||||
|
|
||||||
def do_run_migrations(connection: Connection) -> None:
|
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:
|
if alembic_config.cmd_opts.autogenerate:
|
||||||
script = directives[0]
|
script = directives[0]
|
||||||
if script.upgrade_ops.is_empty():
|
if script.upgrade_ops.is_empty():
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Create Date: ${create_date}
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
import backend.common.model
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -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 fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.auth import router as auth_router
|
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 fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.auth.auth import router as auth_router
|
from backend.app.admin.api.v1.auth.auth import router as auth_router
|
||||||
@@ -8,4 +6,4 @@ from backend.app.admin.api.v1.auth.captcha import router as captcha_router
|
|||||||
router = APIRouter(prefix='/auth')
|
router = APIRouter(prefix='/auth')
|
||||||
|
|
||||||
router.include_router(auth_router, tags=['授权'])
|
router.include_router(auth_router, tags=['授权'])
|
||||||
router.include_router(captcha_router, prefix='/captcha', tags=['验证码'])
|
router.include_router(captcha_router, tags=['验证码'])
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request, Response
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
@@ -11,13 +9,17 @@ from backend.app.admin.schema.token import GetLoginToken, GetNewToken, GetSwagge
|
|||||||
from backend.app.admin.schema.user import AuthLoginParam
|
from backend.app.admin.schema.user import AuthLoginParam
|
||||||
from backend.app.admin.service.auth_service import auth_service
|
from backend.app.admin.service.auth_service import auth_service
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证')
|
@router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证')
|
||||||
async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken:
|
async def login_swagger(
|
||||||
token, user = await auth_service.swagger_login(obj=obj)
|
db: CurrentSessionTransaction, obj: Annotated[HTTPBasicCredentials, Depends()]
|
||||||
|
) -> GetSwaggerToken:
|
||||||
|
token, user = await auth_service.swagger_login(db=db, obj=obj)
|
||||||
return GetSwaggerToken(access_token=token, user=user) # type: ignore
|
return GetSwaggerToken(access_token=token, user=user) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@@ -27,20 +29,29 @@ async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetS
|
|||||||
description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman',
|
description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman',
|
||||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
||||||
)
|
)
|
||||||
async def user_login(
|
async def login(
|
||||||
request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
|
db: CurrentSessionTransaction,
|
||||||
|
response: Response,
|
||||||
|
obj: AuthLoginParam,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
) -> ResponseSchemaModel[GetLoginToken]:
|
) -> ResponseSchemaModel[GetLoginToken]:
|
||||||
data = await auth_service.login(request=request, response=response, obj=obj, background_tasks=background_tasks)
|
data = await auth_service.login(db=db, response=response, obj=obj, background_tasks=background_tasks)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/token/new', summary='创建新 token')
|
@router.get('/codes', summary='获取所有授权码', description='适配 vben admin v5', dependencies=[DependsJwtAuth])
|
||||||
async def create_new_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
async def get_codes(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[str]]:
|
||||||
data = await auth_service.new_token(request=request)
|
codes = await auth_service.get_codes(db=db, request=request)
|
||||||
|
return response_base.success(data=codes)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/refresh', summary='刷新 token')
|
||||||
|
async def refresh_token(db: CurrentSession, request: Request) -> ResponseSchemaModel[GetNewToken]:
|
||||||
|
data = await auth_service.refresh_token(db=db, request=request)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/logout', summary='用户登出')
|
@router.post('/logout', summary='用户登出')
|
||||||
async def user_logout(request: Request, response: Response) -> ResponseModel:
|
async def logout(request: Request, response: Response) -> ResponseModel:
|
||||||
await auth_service.logout(request=request, response=response)
|
await auth_service.logout(request=request, response=response)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
#!/usr/bin/env python3
|
import uuid
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fast_captcha import img_captcha
|
from fast_captcha import img_captcha
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi_limiter.depends import RateLimiter
|
from fastapi_limiter.depends import RateLimiter
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from backend.app.admin.schema.captcha import GetCaptchaDetail
|
from backend.app.admin.schema.captcha import GetCaptchaDetail
|
||||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
|
from backend.database.db import CurrentSession
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
|
from backend.utils.dynamic_config import load_login_config
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'/captcha',
|
||||||
summary='获取登录验证码',
|
summary='获取登录验证码',
|
||||||
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
|
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
|
||||||
)
|
)
|
||||||
async def get_captcha(request: Request) -> ResponseSchemaModel[GetCaptchaDetail]:
|
async def get_captcha(db: CurrentSession) -> ResponseSchemaModel[GetCaptchaDetail]:
|
||||||
"""
|
await load_login_config(db)
|
||||||
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是IO密集型任务,使用线程池尽量减少性能损耗
|
img, code = await run_in_threadpool(img_captcha, img_byte='base64')
|
||||||
"""
|
captcha_uuid = str(uuid.uuid4())
|
||||||
img_type: str = 'base64'
|
|
||||||
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
|
|
||||||
ip = request.state.ip
|
|
||||||
await redis_client.set(
|
await redis_client.set(
|
||||||
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}',
|
f'{settings.LOGIN_CAPTCHA_REDIS_PREFIX}:{captcha_uuid}',
|
||||||
code,
|
code,
|
||||||
ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
|
ex=settings.LOGIN_CAPTCHA_EXPIRE_SECONDS,
|
||||||
|
)
|
||||||
|
data = GetCaptchaDetail(
|
||||||
|
is_enabled=settings.LOGIN_CAPTCHA_ENABLED,
|
||||||
|
expire_seconds=settings.LOGIN_CAPTCHA_EXPIRE_SECONDS,
|
||||||
|
uuid=captcha_uuid,
|
||||||
|
image=img,
|
||||||
)
|
)
|
||||||
data = GetCaptchaDetail(image_type=img_type, image=img)
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.log.login_log import router as login_log
|
from backend.app.admin.api.v1.log.login_log import router as login_log
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.login_log import GetLoginLogDetail
|
from backend.app.admin.schema.login_log import DeleteLoginLogParam, GetLoginLogDetail
|
||||||
from backend.app.admin.service.login_log_service import login_log_service
|
from backend.app.admin.service.login_log_service import login_log_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -24,14 +22,14 @@ router = APIRouter()
|
|||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_login_logs(
|
async def get_login_logs_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
status: Annotated[int | None, Query(description='状态')] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
|
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
|
||||||
log_select = await login_log_service.get_select(username=username, status=status, ip=ip)
|
page_data = await login_log_service.get_list(db=db, username=username, status=status, ip=ip)
|
||||||
page_data = await paging_data(db, log_select)
|
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,8 +41,8 @@ async def get_pagination_login_logs(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_login_log(pk: Annotated[list[int], Query(description='登录日志 ID 列表')]) -> ResponseModel:
|
async def delete_login_logs(db: CurrentSessionTransaction, obj: DeleteLoginLogParam) -> ResponseModel:
|
||||||
count = await login_log_service.delete(pk=pk)
|
count = await login_log_service.delete(db=db, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -54,12 +52,10 @@ async def delete_login_log(pk: Annotated[list[int], Query(description='登录日
|
|||||||
'/all',
|
'/all',
|
||||||
summary='清空登录日志',
|
summary='清空登录日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('log:login:empty')),
|
Depends(RequestPermission('log:login:clear')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_all_login_logs() -> ResponseModel:
|
async def delete_all_login_logs(db: CurrentSessionTransaction) -> ResponseModel:
|
||||||
count = await login_log_service.delete_all()
|
await login_log_service.delete_all(db=db)
|
||||||
if count > 0:
|
return response_base.success()
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.opera_log import GetOperaLogDetail
|
from backend.app.admin.schema.opera_log import DeleteOperaLogParam, GetOperaLogDetail
|
||||||
from backend.app.admin.service.opera_log_service import opera_log_service
|
from backend.app.admin.service.opera_log_service import opera_log_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -24,14 +22,14 @@ router = APIRouter()
|
|||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_opera_logs(
|
async def get_opera_logs_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
status: Annotated[int | None, Query(description='状态')] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
|
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
|
||||||
log_select = await opera_log_service.get_select(username=username, status=status, ip=ip)
|
page_data = await opera_log_service.get_list(db=db, username=username, status=status, ip=ip)
|
||||||
page_data = await paging_data(db, log_select)
|
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,8 +41,8 @@ async def get_pagination_opera_logs(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_opera_log(pk: Annotated[list[int], Query(description='操作日志 ID 列表')]) -> ResponseModel:
|
async def delete_opera_logs(db: CurrentSessionTransaction, obj: DeleteOperaLogParam) -> ResponseModel:
|
||||||
count = await opera_log_service.delete(pk=pk)
|
count = await opera_log_service.delete(db=db, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -54,12 +52,10 @@ async def delete_opera_log(pk: Annotated[list[int], Query(description='操作日
|
|||||||
'/all',
|
'/all',
|
||||||
summary='清空操作日志',
|
summary='清空操作日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('log:opera:empty')),
|
Depends(RequestPermission('log:opera:clear')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_all_opera_logs() -> ResponseModel:
|
async def delete_all_opera_logs(db: CurrentSessionTransaction) -> ResponseModel:
|
||||||
count = await opera_log_service.delete_all()
|
await opera_log_service.delete_all(db=db)
|
||||||
if count > 0:
|
return response_base.success()
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.monitor.online import router as token_router
|
from backend.app.admin.api.v1.monitor.online import router as token_router
|
||||||
@@ -10,4 +8,4 @@ router = APIRouter(prefix='/monitors')
|
|||||||
|
|
||||||
router.include_router(redis_router, prefix='/redis', tags=['redis监控'])
|
router.include_router(redis_router, prefix='/redis', tags=['redis监控'])
|
||||||
router.include_router(server_router, prefix='/server', tags=['服务器监控'])
|
router.include_router(server_router, prefix='/server', tags=['服务器监控'])
|
||||||
router.include_router(token_router, prefix='/online', tags=['在线用户'])
|
router.include_router(token_router, prefix='/sessions', tags=['会话监控'])
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Path, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.token import GetTokenDetail
|
from backend.app.admin.schema.token import GetTokenDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
|
from backend.common.security.jwt import DependsJwtAuth, DependsSuperUser, jwt_decode, revoke_token
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.common.security.rbac import DependsRBAC
|
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
|
|
||||||
@@ -19,10 +15,10 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取在线用户', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取在线用户', dependencies=[DependsJwtAuth])
|
||||||
async def get_online(
|
async def get_sessions(
|
||||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
||||||
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
token_keys = await redis_client.get_prefix(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
||||||
online_clients = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
|
online_clients = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
|
||||||
data: list[GetTokenDetail] = []
|
data: list[GetTokenDetail] = []
|
||||||
|
|
||||||
@@ -37,16 +33,17 @@ async def get_online(
|
|||||||
'browser': extra_info.get('browser', '未知'),
|
'browser': extra_info.get('browser', '未知'),
|
||||||
'device': extra_info.get('device', '未知'),
|
'device': extra_info.get('device', '未知'),
|
||||||
'last_login_time': extra_info.get('last_login_time', '未知'),
|
'last_login_time': extra_info.get('last_login_time', '未知'),
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
for key in token_keys:
|
for key in token_keys:
|
||||||
token = await redis_client.get(key)
|
token = await redis_client.get(key)
|
||||||
token_payload = jwt_decode(token)
|
token_payload = jwt_decode(token)
|
||||||
|
user_id = token_payload.id
|
||||||
session_uuid = token_payload.session_uuid
|
session_uuid = token_payload.session_uuid
|
||||||
token_detail = GetTokenDetail(
|
token_detail = GetTokenDetail(
|
||||||
id=token_payload.id,
|
id=user_id,
|
||||||
session_uuid=session_uuid,
|
session_uuid=session_uuid,
|
||||||
username='未知',
|
username='未知',
|
||||||
nickname='未知',
|
nickname='未知',
|
||||||
@@ -58,7 +55,7 @@ async def get_online(
|
|||||||
last_login_time='未知',
|
last_login_time='未知',
|
||||||
expire_time=token_payload.expire_time,
|
expire_time=token_payload.expire_time,
|
||||||
)
|
)
|
||||||
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
|
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||||
if extra_info:
|
if extra_info:
|
||||||
extra_info = json.loads(extra_info)
|
extra_info = json.loads(extra_info)
|
||||||
# 排除 swagger 登录生成的 token
|
# 排除 swagger 登录生成的 token
|
||||||
@@ -75,17 +72,12 @@ async def get_online(
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'/{pk}',
|
'/{pk}',
|
||||||
summary='踢下线',
|
summary='强制下线',
|
||||||
dependencies=[
|
dependencies=[DependsSuperUser],
|
||||||
Depends(RequestPermission('sys:token:kick')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
async def kick_out(
|
async def delete_session(
|
||||||
request: Request,
|
|
||||||
pk: Annotated[int, Path(description='用户 ID')],
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
session_uuid: Annotated[str, Query(description='会话 UUID')],
|
session_uuid: Annotated[str, Query(description='会话 UUID')],
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
superuser_verify(request)
|
await revoke_token(pk, session_uuid)
|
||||||
await revoke_token(str(pk), session_uuid)
|
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
from fastapi import APIRouter
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
|
||||||
from backend.common.response.response_schema import ResponseModel, response_base
|
from backend.common.response.response_schema import ResponseModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.utils.redis_info import redis_info
|
from backend.utils.redis_info import redis_info
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('', summary='redis 监控', dependencies=[DependsJwtAuth])
|
||||||
'',
|
|
||||||
summary='redis 监控',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:monitor:redis')),
|
|
||||||
DependsJwtAuth,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_redis_info() -> ResponseModel:
|
async def get_redis_info() -> ResponseModel:
|
||||||
data = {
|
data = {
|
||||||
'info': await redis_info.get_info(),
|
'info': await redis_info.get_info(),
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
from fastapi import APIRouter
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from backend.common.response.response_schema import ResponseModel, response_base
|
from backend.common.response.response_schema import ResponseModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.utils.server_info import server_info
|
from backend.utils.server_info import server_info
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('', summary='server 监控', dependencies=[DependsJwtAuth])
|
||||||
'',
|
|
||||||
summary='server 监控',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:monitor:server')),
|
|
||||||
DependsJwtAuth,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_server_info() -> ResponseModel:
|
async def get_server_info() -> ResponseModel:
|
||||||
data = {
|
data = {
|
||||||
# 扔到线程池,避免阻塞
|
# 扔到线程池,避免阻塞
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
# !/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
|
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
|
||||||
from backend.app.admin.api.v1.sys.data_scope import router as data_scope_router
|
from backend.app.admin.api.v1.sys.data_scope import router as data_scope_router
|
||||||
from backend.app.admin.api.v1.sys.dept import router as dept_router
|
from backend.app.admin.api.v1.sys.dept import router as dept_router
|
||||||
|
from backend.app.admin.api.v1.sys.file import router as file_router
|
||||||
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
||||||
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
|
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
|
||||||
from backend.app.admin.api.v1.sys.role import router as role_router
|
from backend.app.admin.api.v1.sys.role import router as role_router
|
||||||
from backend.app.admin.api.v1.sys.upload import router as upload_router
|
|
||||||
from backend.app.admin.api.v1.sys.user import router as user_router
|
from backend.app.admin.api.v1.sys.user import router as user_router
|
||||||
|
|
||||||
router = APIRouter(prefix='/sys')
|
router = APIRouter(prefix='/sys')
|
||||||
@@ -19,5 +17,5 @@ router.include_router(role_router, prefix='/roles', tags=['系统角色'])
|
|||||||
router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
||||||
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
|
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
|
||||||
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
|
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
|
||||||
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
|
router.include_router(file_router, prefix='/files', tags=['系统文件'])
|
||||||
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
|
router.include_router(plugin_router, prefix='/plugins', tags=['系统插件'])
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.data_rule import (
|
from backend.app.admin.schema.data_rule import (
|
||||||
CreateDataRuleParam,
|
CreateDataRuleParam,
|
||||||
|
DeleteDataRuleParam,
|
||||||
GetDataRuleColumnDetail,
|
GetDataRuleColumnDetail,
|
||||||
GetDataRuleDetail,
|
GetDataRuleDetail,
|
||||||
UpdateDataRuleParam,
|
UpdateDataRuleParam,
|
||||||
)
|
)
|
||||||
from backend.app.admin.service.data_rule_service import data_rule_service
|
from backend.app.admin.service.data_rule_service import data_rule_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
|||||||
return response_base.success(data=models)
|
return response_base.success(data=models)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/model/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
@router.get('/models/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_rule_model_columns(
|
async def get_data_rule_model_columns(
|
||||||
model: Annotated[str, Path(description='模型名称')],
|
model: Annotated[str, Path(description='模型名称')],
|
||||||
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
||||||
@@ -36,16 +35,17 @@ async def get_data_rule_model_columns(
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_data_rules() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
async def get_all_data_rules(db: CurrentSession) -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
||||||
data = await data_rule_service.get_all()
|
data = await data_rule_service.get_all(db=db)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_rule(
|
async def get_data_rule(
|
||||||
|
db: CurrentSession,
|
||||||
pk: Annotated[int, Path(description='数据规则 ID')],
|
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||||
) -> ResponseSchemaModel[GetDataRuleDetail]:
|
) -> ResponseSchemaModel[GetDataRuleDetail]:
|
||||||
data = await data_rule_service.get(pk=pk)
|
data = await data_rule_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,11 +57,11 @@ async def get_data_rule(
|
|||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_data_rules(
|
async def get_data_rules_paginated(
|
||||||
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
|
db: CurrentSession,
|
||||||
|
name: Annotated[str | None, Query(description='规则名称')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
||||||
data_rule_select = await data_rule_service.get_select(name=name)
|
page_data = await data_rule_service.get_list(db=db, name=name)
|
||||||
page_data = await paging_data(db, data_rule_select)
|
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ async def get_pagination_data_rules(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
|
async def create_data_rule(db: CurrentSessionTransaction, obj: CreateDataRuleParam) -> ResponseModel:
|
||||||
await data_rule_service.create(obj=obj)
|
await data_rule_service.create(db=db, obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -87,9 +87,11 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_rule(
|
async def update_data_rule(
|
||||||
pk: Annotated[int, Path(description='数据规则 ID')], obj: UpdateDataRuleParam
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||||
|
obj: UpdateDataRuleParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await data_rule_service.update(pk=pk, obj=obj)
|
count = await data_rule_service.update(db=db, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -103,8 +105,8 @@ async def update_data_rule(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_data_rule(pk: Annotated[list[int], Query(description='数据规则 ID 列表')]) -> ResponseModel:
|
async def delete_data_rules(db: CurrentSessionTransaction, obj: DeleteDataRuleParam) -> ResponseModel:
|
||||||
count = await data_rule_service.delete(pk=pk)
|
count = await data_rule_service.delete(db=db, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -1,40 +1,47 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.data_scope import (
|
from backend.app.admin.schema.data_scope import (
|
||||||
CreateDataScopeParam,
|
CreateDataScopeParam,
|
||||||
|
DeleteDataScopeParam,
|
||||||
GetDataScopeDetail,
|
GetDataScopeDetail,
|
||||||
GetDataScopeWithRelationDetail,
|
GetDataScopeWithRelationDetail,
|
||||||
UpdateDataScopeParam,
|
UpdateDataScopeParam,
|
||||||
UpdateDataScopeRuleParam,
|
UpdateDataScopeRuleParam,
|
||||||
)
|
)
|
||||||
from backend.app.admin.service.data_scope_service import data_scope_service
|
from backend.app.admin.service.data_scope_service import data_scope_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/all', summary='获取所有数据范围', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_all_data_scope(db: CurrentSession) -> ResponseSchemaModel[list[GetDataScopeDetail]]:
|
||||||
|
data = await data_scope_service.get_all(db=db)
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_scope(
|
async def get_data_scope(
|
||||||
|
db: CurrentSession,
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')],
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
) -> ResponseSchemaModel[GetDataScopeDetail]:
|
) -> ResponseSchemaModel[GetDataScopeDetail]:
|
||||||
data = await data_scope_service.get(pk=pk)
|
data = await data_scope_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/rules', summary='获取数据范围所有规则', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/rules', summary='获取数据范围所有规则', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_scope_rules(
|
async def get_data_scope_rules(
|
||||||
|
db: CurrentSession,
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')],
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
) -> ResponseSchemaModel[GetDataScopeWithRelationDetail]:
|
) -> ResponseSchemaModel[GetDataScopeWithRelationDetail]:
|
||||||
data = await data_scope_service.get_rules(pk=pk)
|
data = await data_scope_service.get_rules(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -46,13 +53,12 @@ async def get_data_scope_rules(
|
|||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_data_scopes(
|
async def get_data_scopes_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[str | None, Query(description='范围名称')] = None,
|
name: Annotated[str | None, Query(description='范围名称')] = None,
|
||||||
status: Annotated[int | None, Query(description='状态')] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetDataScopeDetail]]:
|
) -> ResponseSchemaModel[PageData[GetDataScopeDetail]]:
|
||||||
data_scope_select = await data_scope_service.get_select(name=name, status=status)
|
page_data = await data_scope_service.get_list(db=db, name=name, status=status)
|
||||||
page_data = await paging_data(db, data_scope_select)
|
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,8 +70,8 @@ async def get_pagination_data_scopes(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
|
async def create_data_scope(db: CurrentSessionTransaction, obj: CreateDataScopeParam) -> ResponseModel:
|
||||||
await data_scope_service.create(obj=obj)
|
await data_scope_service.create(db=db, obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -78,9 +84,11 @@ async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_scope(
|
async def update_data_scope(
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')], obj: UpdateDataScopeParam
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
obj: UpdateDataScopeParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await data_scope_service.update(pk=pk, obj=obj)
|
count = await data_scope_service.update(db=db, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -95,9 +103,11 @@ async def update_data_scope(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_scope_rules(
|
async def update_data_scope_rules(
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')], rule_ids: UpdateDataScopeRuleParam
|
db: CurrentSessionTransaction,
|
||||||
):
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
|
rule_ids: UpdateDataScopeRuleParam,
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await data_scope_service.update_data_scope_rule(db=db, pk=pk, rule_ids=rule_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -111,8 +121,8 @@ async def update_data_scope_rules(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_data_scope(pk: Annotated[list[int], Query(description='数据范围 ID 列表')]) -> ResponseModel:
|
async def delete_data_scopes(db: CurrentSessionTransaction, obj: DeleteDataScopeParam) -> ResponseModel:
|
||||||
count = await data_scope_service.delete(pk=pk)
|
count = await data_scope_service.delete(db=db, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -1,34 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
from typing import Annotated
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated, Any
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
from sqlalchemy import ColumnElement
|
||||||
|
|
||||||
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, UpdateDeptParam
|
from backend.app.admin.model import Dept
|
||||||
|
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, GetDeptTree, UpdateDeptParam
|
||||||
from backend.app.admin.service.dept_service import dept_service
|
from backend.app.admin.service.dept_service import dept_service
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import DataPermissionFilter, RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseSchemaModel[GetDeptDetail]:
|
async def get_dept(
|
||||||
data = await dept_service.get(pk=pk)
|
db: CurrentSession, pk: Annotated[int, Path(description='部门 ID')]
|
||||||
|
) -> ResponseSchemaModel[GetDeptDetail]:
|
||||||
|
data = await dept_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取部门树', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_depts(
|
async def get_dept_tree(
|
||||||
request: Request,
|
db: CurrentSession,
|
||||||
|
data_filter: Annotated[ColumnElement[bool], Depends(DataPermissionFilter(Dept))],
|
||||||
name: Annotated[str | None, Query(description='部门名称')] = None,
|
name: Annotated[str | None, Query(description='部门名称')] = None,
|
||||||
leader: Annotated[str | None, Query(description='部门负责人')] = None,
|
leader: Annotated[str | None, Query(description='部门负责人')] = None,
|
||||||
phone: Annotated[str | None, Query(description='联系电话')] = None,
|
phone: Annotated[str | None, Query(description='联系电话')] = None,
|
||||||
status: Annotated[int | None, Query(description='状态')] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
) -> ResponseSchemaModel[list[GetDeptTree]]:
|
||||||
dept = await dept_service.get_dept_tree(request=request, name=name, leader=leader, phone=phone, status=status)
|
dept = await dept_service.get_tree(
|
||||||
|
db=db, data_filter=data_filter, name=name, leader=leader, phone=phone, status=status
|
||||||
|
)
|
||||||
return response_base.success(data=dept)
|
return response_base.success(data=dept)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,8 +46,8 @@ async def get_all_depts(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def create_dept(obj: CreateDeptParam) -> ResponseModel:
|
async def create_dept(db: CurrentSessionTransaction, obj: CreateDeptParam) -> ResponseModel:
|
||||||
await dept_service.create(obj=obj)
|
await dept_service.create(db=db, obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -53,8 +59,10 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam) -> ResponseModel:
|
async def update_dept(
|
||||||
count = await dept_service.update(pk=pk, obj=obj)
|
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await dept_service.update(db=db, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -68,8 +76,8 @@ async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: Up
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
|
async def delete_dept(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
|
||||||
count = await dept_service.delete(pk=pk)
|
count = await dept_service.delete(db=db, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
|
|
||||||
|
from backend.common.dataclasses import UploadUrl
|
||||||
|
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||||
|
from backend.common.security.permission import RequestPermission
|
||||||
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
from backend.utils.file_ops import upload_file, upload_file_verify
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'/upload',
|
||||||
|
summary='本地文件上传',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:file:upload')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def upload_files(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
|
||||||
|
upload_file_verify(file)
|
||||||
|
filename = await upload_file(file)
|
||||||
|
return response_base.success(data={'url': f'/static/upload/{filename}'})
|
||||||
@@ -1,37 +1,39 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
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.app.admin.service.menu_service import menu_service
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
|
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='已适配 vben admin v5', dependencies=[DependsJwtAuth])
|
||||||
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
async def get_user_sidebar(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||||
menu = await menu_service.get_sidebar(request=request)
|
menu = await menu_service.get_sidebar(db=db, request=request)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseSchemaModel[GetMenuDetail]:
|
async def get_menu(
|
||||||
data = await menu_service.get(pk=pk)
|
db: CurrentSession, pk: Annotated[int, Path(description='菜单 ID')]
|
||||||
|
) -> ResponseSchemaModel[GetMenuDetail]:
|
||||||
|
data = await menu_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取所有菜单展示树', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取菜单树', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_menus(
|
async def get_menu_tree(
|
||||||
|
db: CurrentSession,
|
||||||
title: Annotated[str | None, Query(description='菜单标题')] = None,
|
title: Annotated[str | None, Query(description='菜单标题')] = None,
|
||||||
status: Annotated[int | None, Query(description='状体')] = None,
|
status: Annotated[int | None, Query(description='状体')] = None,
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
) -> ResponseSchemaModel[list[GetMenuTree]]:
|
||||||
menu = await menu_service.get_menu_tree(title=title, status=status)
|
menu = await menu_service.get_tree(db=db, title=title, status=status)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,8 +45,8 @@ async def get_all_menus(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def create_menu(obj: CreateMenuParam) -> ResponseModel:
|
async def create_menu(db: CurrentSessionTransaction, obj: CreateMenuParam) -> ResponseModel:
|
||||||
await menu_service.create(obj=obj)
|
await menu_service.create(db=db, obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -56,8 +58,10 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam) -> ResponseModel:
|
async def update_menu(
|
||||||
count = await menu_service.update(pk=pk, obj=obj)
|
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await menu_service.update(db=db, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -71,8 +75,8 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
|
async def delete_menu(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
|
||||||
count = await menu_service.delete(pk=pk)
|
count = await menu_service.delete(db=db, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
||||||
@@ -7,7 +5,8 @@ from fastapi.params import Query
|
|||||||
from starlette.responses import StreamingResponse
|
from starlette.responses import StreamingResponse
|
||||||
|
|
||||||
from backend.app.admin.service.plugin_service import plugin_service
|
from backend.app.admin.service.plugin_service import plugin_service
|
||||||
from backend.common.response.response_code import CustomResponseCode
|
from backend.common.enums import PluginType
|
||||||
|
from backend.common.response.response_code import CustomResponse
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
@@ -22,42 +21,37 @@ async def get_all_plugins() -> ResponseSchemaModel[list[dict[str, Any]]]:
|
|||||||
return response_base.success(data=plugins)
|
return response_base.success(data=plugins)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/changed', summary='插件状态是否变更', dependencies=[DependsJwtAuth])
|
@router.get('/changed', summary='是否存在插件变更', dependencies=[DependsJwtAuth])
|
||||||
async def plugin_changed() -> ResponseSchemaModel[bool]:
|
async def plugin_changed() -> ResponseSchemaModel[bool]:
|
||||||
plugins = await plugin_service.changed()
|
plugins = await plugin_service.changed()
|
||||||
return response_base.success(data=bool(plugins))
|
return response_base.success(data=bool(plugins))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/install/zip',
|
'',
|
||||||
summary='安装 zip 插件',
|
summary='安装插件',
|
||||||
description='使用插件 zip 压缩包进行安装',
|
description='使用插件 zip 压缩包或 git 仓库地址进行安装',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:plugin:install')),
|
Depends(RequestPermission('sys:plugin:install')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def install_zip_plugin(file: Annotated[UploadFile, File()]) -> ResponseModel:
|
async def install_plugin(
|
||||||
await plugin_service.install_zip(file=file)
|
type: Annotated[PluginType, Query(description='插件类型')],
|
||||||
return response_base.success(res=CustomResponseCode.PLUGIN_INSTALL_SUCCESS)
|
file: Annotated[UploadFile | None, File()] = None,
|
||||||
|
repo_url: Annotated[str | None, Query(description='插件 git 仓库地址')] = None,
|
||||||
|
) -> ResponseModel:
|
||||||
@router.post(
|
plugin_name = await plugin_service.install(type=type, file=file, repo_url=repo_url)
|
||||||
'/install/git',
|
return response_base.success(
|
||||||
summary='安装 git 插件',
|
res=CustomResponse(
|
||||||
description='使用插件 git 仓库地址进行安装,不限制平台;如果需要凭证,需在 git 仓库地址中添加凭证信息',
|
code=200,
|
||||||
dependencies=[
|
msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务',
|
||||||
Depends(RequestPermission('sys:plugin:install')),
|
),
|
||||||
DependsRBAC,
|
)
|
||||||
],
|
|
||||||
)
|
|
||||||
async def install_git_plugin(repo_url: Annotated[str, Query(description='插件 git 仓库地址')]) -> ResponseModel:
|
|
||||||
await plugin_service.install_git(repo_url=repo_url)
|
|
||||||
return response_base.success(res=CustomResponseCode.PLUGIN_INSTALL_SUCCESS)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'/uninstall',
|
'/{plugin}',
|
||||||
summary='卸载插件',
|
summary='卸载插件',
|
||||||
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
@@ -65,33 +59,28 @@ async def install_git_plugin(repo_url: Annotated[str, Query(description='插件
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def uninstall_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> ResponseModel:
|
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
await plugin_service.uninstall(plugin=plugin)
|
await plugin_service.uninstall(plugin=plugin)
|
||||||
return response_base.success(res=CustomResponseCode.PLUGIN_UNINSTALL_SUCCESS)
|
return response_base.success(
|
||||||
|
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.put(
|
||||||
'/status',
|
'/{plugin}/status',
|
||||||
summary='更新插件状态',
|
summary='更新插件状态',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:plugin:status')),
|
Depends(RequestPermission('sys:plugin:edit')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_plugin_status(plugin: Annotated[str, Query(description='插件名称')]) -> ResponseModel:
|
async def update_plugin_status(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
await plugin_service.update_status(plugin=plugin)
|
await plugin_service.update_status(plugin=plugin)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('/{plugin}', summary='下载插件', dependencies=[DependsJwtAuth])
|
||||||
'/zip/{plugin}',
|
async def download_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
|
||||||
summary='打包插件',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:plugin:zip')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def build_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
|
|
||||||
bio = await plugin_service.build(plugin=plugin)
|
bio = await plugin_service.build(plugin=plugin)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
bio,
|
bio,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
from typing import Annotated
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated, Any
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
|
from backend.app.admin.schema.menu import GetMenuTree
|
||||||
from backend.app.admin.schema.role import (
|
from backend.app.admin.schema.role import (
|
||||||
CreateRoleParam,
|
CreateRoleParam,
|
||||||
|
DeleteRoleParam,
|
||||||
GetRoleDetail,
|
GetRoleDetail,
|
||||||
GetRoleWithRelationDetail,
|
GetRoleWithRelationDetail,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
@@ -13,49 +13,44 @@ from backend.app.admin.schema.role import (
|
|||||||
UpdateRoleScopeParam,
|
UpdateRoleScopeParam,
|
||||||
)
|
)
|
||||||
from backend.app.admin.service.role_service import role_service
|
from backend.app.admin.service.role_service import role_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
|
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
async def get_all_roles(db: CurrentSession) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||||
data = await role_service.get_all()
|
data = await role_service.get_all(db=db)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/menus', summary='获取角色菜单树', dependencies=[DependsJwtAuth])
|
||||||
async def get_user_all_roles(
|
async def get_role_menu_tree(
|
||||||
pk: Annotated[int, Path(description='用户 ID')],
|
db: CurrentSession,
|
||||||
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
|
||||||
data = await role_service.get_users(pk=pk)
|
|
||||||
return response_base.success(data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_role_all_menus(
|
|
||||||
pk: Annotated[int, Path(description='角色 ID')],
|
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)
|
menu = await role_service.get_menu_tree(db=db, pk=pk)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
|
||||||
async def get_role_all_scopes(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
|
async def get_role_scopes(
|
||||||
rule = await role_service.get_scopes(pk=pk)
|
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
|
||||||
|
) -> ResponseSchemaModel[list[int]]:
|
||||||
|
rule = await role_service.get_scopes(db=db, pk=pk)
|
||||||
return response_base.success(data=rule)
|
return response_base.success(data=rule)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_role(
|
async def get_role(
|
||||||
pk: Annotated[int, Path(description='角色 ID')],
|
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
|
||||||
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
|
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
|
||||||
data = await role_service.get(pk=pk)
|
data = await role_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,13 +62,12 @@ async def get_role(
|
|||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_roles(
|
async def get_roles_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[str | None, Query(description='角色名称')] = None,
|
name: Annotated[str | None, Query(description='角色名称')] = None,
|
||||||
status: Annotated[int | None, Query(description='状态')] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
|
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
|
||||||
role_select = await role_service.get_select(name=name, status=status)
|
page_data = await role_service.get_list(db=db, name=name, status=status)
|
||||||
page_data = await paging_data(db, role_select)
|
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -85,8 +79,8 @@ async def get_pagination_roles(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def create_role(obj: CreateRoleParam) -> ResponseModel:
|
async def create_role(db: CurrentSessionTransaction, obj: CreateRoleParam) -> ResponseModel:
|
||||||
await role_service.create(obj=obj)
|
await role_service.create(db=db, obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -98,15 +92,17 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam) -> ResponseModel:
|
async def update_role(
|
||||||
count = await role_service.update(pk=pk, obj=obj)
|
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await role_service.update(db=db, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}/menu',
|
'/{pk}/menus',
|
||||||
summary='更新角色菜单',
|
summary='更新角色菜单',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:menu:edit')),
|
Depends(RequestPermission('sys:role:menu:edit')),
|
||||||
@@ -114,16 +110,18 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_menus(
|
async def update_role_menus(
|
||||||
pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
menu_ids: UpdateRoleMenuParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
|
count = await role_service.update_role_menu(db=db, pk=pk, menu_ids=menu_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}/scope',
|
'/{pk}/scopes',
|
||||||
summary='更新角色数据范围',
|
summary='更新角色数据范围',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:scope:edit')),
|
Depends(RequestPermission('sys:role:scope:edit')),
|
||||||
@@ -131,9 +129,11 @@ async def update_role_menus(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_scopes(
|
async def update_role_scopes(
|
||||||
pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
scope_ids: UpdateRoleScopeParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
|
count = await role_service.update_role_scope(db=db, pk=pk, scope_ids=scope_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -147,8 +147,8 @@ async def update_role_scopes(
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_role(pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
|
async def delete_roles(db: CurrentSessionTransaction, obj: DeleteRoleParam) -> ResponseModel:
|
||||||
count = await role_service.delete(pk=pk)
|
count = await role_service.delete(db=db, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, File, UploadFile
|
|
||||||
|
|
||||||
from backend.common.dataclasses import UploadUrl
|
|
||||||
from backend.common.enums import FileType
|
|
||||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
|
||||||
from backend.utils.file_ops import file_verify, upload_file
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/image', summary='上传图片', dependencies=[DependsJwtAuth])
|
|
||||||
async def upload_image(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
|
|
||||||
file_verify(file, FileType.image)
|
|
||||||
filename = await upload_file(file)
|
|
||||||
return response_base.success(data={'url': f'/static/upload/{filename}'})
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/video', summary='上传视频', dependencies=[DependsJwtAuth])
|
|
||||||
async def upload_video(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
|
|
||||||
file_verify(file, FileType.video)
|
|
||||||
filename = await upload_file(file)
|
|
||||||
return response_base.success(data={'url': f'/static/upload/{filename}'})
|
|
||||||
@@ -1,71 +1,48 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
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 (
|
from backend.app.admin.schema.user import (
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
GetCurrentUserInfoWithRelationDetail,
|
GetCurrentUserInfoWithRelationDetail,
|
||||||
GetUserInfoWithRelationDetail,
|
GetUserInfoWithRelationDetail,
|
||||||
RegisterUserParam,
|
|
||||||
ResetPasswordParam,
|
ResetPasswordParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
)
|
)
|
||||||
from backend.app.admin.service.user_service import user_service
|
from backend.app.admin.service.user_service import user_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.enums import UserPermissionType
|
||||||
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth, DependsSuperUser
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post('/register', summary='注册用户')
|
|
||||||
async def register_user(obj: RegisterUserParam) -> ResponseModel:
|
|
||||||
await user_service.register(obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
|
||||||
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
|
||||||
await user_service.add(request=request, obj=obj)
|
|
||||||
data = await user_service.get_userinfo(username=obj.username)
|
|
||||||
return response_base.success(data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth])
|
|
||||||
async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseModel:
|
|
||||||
count = await user_service.pwd_reset(request=request, obj=obj)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
|
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
|
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
|
||||||
data = request.user.model_dump()
|
data = request.user.model_dump()
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def get_user(
|
async def get_userinfo(
|
||||||
username: Annotated[str, Path(description='用户名')],
|
db: CurrentSession,
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||||
data = await user_service.get_userinfo(username=username)
|
data = await user_service.get_userinfo(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||||
async def update_user(
|
async def get_user_roles(
|
||||||
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserParam
|
db: CurrentSession, pk: Annotated[int, Path(description='用户 ID')]
|
||||||
) -> ResponseModel:
|
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||||
count = await user_service.update(request=request, username=username, obj=obj)
|
data = await user_service.get_roles(db=db, pk=pk)
|
||||||
if count > 0:
|
return response_base.success(data=data)
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -76,61 +53,120 @@ async def update_user(
|
|||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_users(
|
async def get_users_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
dept: Annotated[int | None, Query(description='部门 ID')] = None,
|
dept: Annotated[int | None, Query(description='部门 ID')] = None,
|
||||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
phone: Annotated[str | None, Query(description='手机号')] = None,
|
phone: Annotated[str | None, Query(description='手机号')] = None,
|
||||||
status: Annotated[int | None, Query(description='状态')] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
|
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
|
||||||
user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status)
|
page_data = await user_service.get_list(db=db, dept=dept, username=username, phone=phone, status=status)
|
||||||
page_data = await paging_data(db, user_select)
|
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
|
@router.post('', summary='创建用户', dependencies=[DependsSuperUser])
|
||||||
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
async def create_user(
|
||||||
count = await user_service.update_permission(request=request, pk=pk)
|
db: CurrentSessionTransaction, obj: AddUserParam
|
||||||
|
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||||
|
await user_service.create(db=db, obj=obj)
|
||||||
|
data = await user_service.get_userinfo(db=db, username=obj.username)
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsSuperUser])
|
||||||
|
async def update_user(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
|
obj: UpdateUserParam,
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update(db=db, pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/staff', summary='修改用户后台登录权限', dependencies=[DependsRBAC])
|
@router.put('/{pk}/permissions', summary='更新用户权限', dependencies=[DependsSuperUser])
|
||||||
async def staff_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
async def update_user_permission(
|
||||||
count = await user_service.update_staff(request=request, pk=pk)
|
db: CurrentSessionTransaction,
|
||||||
|
request: Request,
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
|
type: Annotated[UserPermissionType, Query(description='权限类型')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_permission(db=db, request=request, pk=pk, type=type)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsRBAC])
|
@router.put('/me/password', summary='更新当前用户密码', dependencies=[DependsJwtAuth])
|
||||||
async def status_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
async def update_user_password(
|
||||||
count = await user_service.update_status(request=request, pk=pk)
|
db: CurrentSessionTransaction, request: Request, obj: ResetPasswordParam
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_password(db=db, user_id=request.user.id, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/multi', summary='修改用户多端登录状态', dependencies=[DependsRBAC])
|
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsSuperUser])
|
||||||
async def multi_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
async def reset_user_password(
|
||||||
count = await user_service.update_multi_login(request=request, pk=pk)
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
|
password: Annotated[str, Body(embed=True, description='新密码')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.reset_password(db=db, 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(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
request: Request,
|
||||||
|
nickname: Annotated[str, Body(embed=True, description='用户昵称')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_nickname(db=db, user_id=request.user.id, 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(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
request: Request,
|
||||||
|
avatar: Annotated[str, Body(embed=True, description='用户头像地址')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_avatar(db=db, user_id=request.user.id, 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(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
request: Request,
|
||||||
|
captcha: Annotated[str, Body(embed=True, description='邮箱验证码')],
|
||||||
|
email: Annotated[str, Body(embed=True, description='用户邮箱')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_email(db=db, user_id=request.user.id, captcha=captcha, email=email)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
path='/{username}',
|
path='/{pk}',
|
||||||
summary='用户注销',
|
summary='删除用户',
|
||||||
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
|
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:user:del')),
|
Depends(RequestPermission('sys:user:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_user(username: Annotated[str, Path(description='用户名')]) -> ResponseModel:
|
async def delete_user(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||||
count = await user_service.delete(username=username)
|
count = await user_service.delete(db=db, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select, and_, desc, select
|
from sqlalchemy import Select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.app.admin.model import DataRule
|
from backend.app.admin.model import DataRule
|
||||||
@@ -24,23 +21,19 @@ class CRUDDataRule(CRUDPlus[DataRule]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model(db, pk)
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
async def get_list(self, name: str | None) -> Select:
|
async def get_select(self, name: str | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取规则列表
|
获取规则列表查询表达式
|
||||||
|
|
||||||
:param name: 规则名称
|
:param name: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(noload(self.model.scope)).order_by(desc(self.model.created_time))
|
filters = {}
|
||||||
|
|
||||||
filters = []
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
filters.append(self.model.name.like(f'%{name}%'))
|
filters['name__like'] = f'%{name}%'
|
||||||
|
|
||||||
if filters:
|
return await self.select_order('id', **filters)
|
||||||
stmt = stmt.where(and_(*filters))
|
|
||||||
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
||||||
"""
|
"""
|
||||||
@@ -82,15 +75,15 @@ class CRUDDataRule(CRUDPlus[DataRule]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除规则
|
批量删除规则
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 规则 ID 列表
|
:param pks: 规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||||
|
|
||||||
|
|
||||||
data_rule_dao: CRUDDataRule = CRUDDataRule(DataRule)
|
data_rule_dao: CRUDDataRule = CRUDDataRule(DataRule)
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from sqlalchemy import Select, and_, desc, select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.orm import noload, selectinload
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.model import DataRule, DataScope
|
from sqlalchemy import Select, delete, insert
|
||||||
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
|
||||||
|
|
||||||
|
from backend.app.admin.model import DataRule, DataScope, data_scope_rule
|
||||||
|
from backend.app.admin.schema.data_scope import (
|
||||||
|
CreateDataScopeParam,
|
||||||
|
CreateDataScopeRuleParam,
|
||||||
|
UpdateDataScopeParam,
|
||||||
|
UpdateDataScopeRuleParam,
|
||||||
|
)
|
||||||
|
from backend.utils.serializers import select_join_serialize
|
||||||
|
|
||||||
|
|
||||||
class CRUDDataScope(CRUDPlus[DataScope]):
|
class CRUDDataScope(CRUDPlus[DataScope]):
|
||||||
@@ -32,7 +38,7 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name)
|
return await self.select_model_by_column(db, name=name)
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope:
|
async def get_join(self, db: AsyncSession, pk: int) -> Any:
|
||||||
"""
|
"""
|
||||||
获取数据范围关联数据
|
获取数据范围关联数据
|
||||||
|
|
||||||
@@ -40,34 +46,42 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
|||||||
:param pk: 范围 ID
|
:param pk: 范围 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.rules)).where(self.model.id == pk)
|
result = await self.select_models(
|
||||||
data_scope = await db.execute(stmt)
|
db,
|
||||||
return data_scope.scalars().first()
|
id=pk,
|
||||||
|
join_conditions=[
|
||||||
|
JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == self.model.id),
|
||||||
|
JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
async def get_list(self, name: str | None, status: int | None) -> Select:
|
return select_join_serialize(result, relationships=['DataScope-m2m-DataRule:rules'])
|
||||||
|
|
||||||
|
async def get_all(self, db: AsyncSession) -> Sequence[DataScope]:
|
||||||
"""
|
"""
|
||||||
获取数据范围列表
|
获取所有数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_models(db)
|
||||||
|
|
||||||
|
async def get_select(self, name: str | None, status: int | None) -> Select:
|
||||||
|
"""
|
||||||
|
获取数据范围列表查询表达式
|
||||||
|
|
||||||
:param name: 范围名称
|
:param name: 范围名称
|
||||||
:param status: 范围状态
|
:param status: 范围状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
filters = {}
|
||||||
select(self.model)
|
|
||||||
.options(noload(self.model.rules), noload(self.model.roles))
|
|
||||||
.order_by(desc(self.model.created_time))
|
|
||||||
)
|
|
||||||
|
|
||||||
filters = []
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
filters.append(self.model.name.like(f'%{name}%'))
|
filters['name__like'] = f'%{name}%'
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.append(self.model.status == status)
|
filters['status'] = status
|
||||||
|
|
||||||
if filters:
|
return await self.select_order('id', **filters)
|
||||||
stmt = stmt.where(and_(*filters))
|
|
||||||
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -90,7 +104,8 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
|
@staticmethod
|
||||||
|
async def update_rules(db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据范围规则
|
更新数据范围规则
|
||||||
|
|
||||||
@@ -99,21 +114,26 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
|||||||
:param rule_ids: 数据规则 ID 列表
|
:param rule_ids: 数据规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_data_scope = await self.get_with_relation(db, pk)
|
data_scope_rule_stmt = delete(data_scope_rule).where(data_scope_rule.c.data_scope_id == pk)
|
||||||
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
|
await db.execute(data_scope_rule_stmt)
|
||||||
rules = await db.execute(stmt)
|
|
||||||
current_data_scope.rules = rules.scalars().all()
|
|
||||||
return len(current_data_scope.rules)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
data_scope_rule_data = [
|
||||||
|
CreateDataScopeRuleParam(data_scope_id=pk, data_rule_id=rule_id).model_dump() for rule_id in rule_ids.rules
|
||||||
|
]
|
||||||
|
data_scope_rule_stmt = insert(data_scope_rule)
|
||||||
|
await db.execute(data_scope_rule_stmt, data_scope_rule_data)
|
||||||
|
|
||||||
|
return len(rule_ids.rules)
|
||||||
|
|
||||||
|
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除数据范围
|
批量删除数据范围
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 范围 ID 列表
|
:param pks: 范围 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||||
|
|
||||||
|
|
||||||
data_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
|
data_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from fastapi import Request
|
from sqlalchemy import ColumnElement
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.model import Dept
|
from backend.app.admin.model import Dept, User
|
||||||
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
||||||
from backend.common.security.permission import filter_data_permission
|
from backend.utils.serializers import select_join_serialize
|
||||||
|
|
||||||
|
|
||||||
class CRUDDept(CRUDPlus[Dept]):
|
class CRUDDept(CRUDPlus[Dept]):
|
||||||
@@ -24,7 +21,7 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
:param dept_id: 部门 ID
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, id=dept_id, del_flag=0)
|
return await self.select_model_by_column(db, id=dept_id, del_flag=False)
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
@@ -34,12 +31,12 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
:param name: 部门名称
|
:param name: 部门名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name, del_flag=0)
|
return await self.select_model_by_column(db, name=name, del_flag=False)
|
||||||
|
|
||||||
async def get_all(
|
async def get_all(
|
||||||
self,
|
self,
|
||||||
request: Request,
|
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
|
data_filter: ColumnElement[bool],
|
||||||
name: str | None,
|
name: str | None,
|
||||||
leader: str | None,
|
leader: str | None,
|
||||||
phone: str | None,
|
phone: str | None,
|
||||||
@@ -48,24 +45,26 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
"""
|
"""
|
||||||
获取所有部门
|
获取所有部门
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
|
:param data_filter: 请求用户
|
||||||
:param name: 部门名称
|
:param name: 部门名称
|
||||||
:param leader: 负责人
|
:param leader: 负责人
|
||||||
:param phone: 联系电话
|
:param phone: 联系电话
|
||||||
:param status: 部门状态
|
:param status: 部门状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {'del_flag__eq': 0}
|
filters = {'del_flag': False}
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
filters.update(name__like=f'%{name}%')
|
filters['name__like'] = f'%{name}%'
|
||||||
if leader is not None:
|
if leader is not None:
|
||||||
filters.update(leader__like=f'%{leader}%')
|
filters['leader__like'] = f'%{leader}%'
|
||||||
if phone is not None:
|
if phone is not None:
|
||||||
filters.update(phone__startswith=phone)
|
filters['phone__startswith'] = phone
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.update(status=status)
|
filters['status'] = status
|
||||||
return await self.select_models_order(db, 'sort', None, await filter_data_permission(db, request), **filters)
|
|
||||||
|
return await self.select_models_order(db, 'sort', 'desc', data_filter, **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -98,7 +97,7 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
|
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
async def get_join(self, db: AsyncSession, dept_id: int) -> Any | None:
|
||||||
"""
|
"""
|
||||||
获取部门及关联数据
|
获取部门及关联数据
|
||||||
|
|
||||||
@@ -106,9 +105,12 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
:param dept_id: 部门 ID
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
|
result = await self.select_model(
|
||||||
result = await db.execute(stmt)
|
db,
|
||||||
return result.scalars().first()
|
dept_id,
|
||||||
|
join_conditions=[JoinConfig(model=User, join_on=User.dept_id == self.model.id, fill_result=True)],
|
||||||
|
)
|
||||||
|
return select_join_serialize(result, relationships=['Dept-o2m-User'])
|
||||||
|
|
||||||
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
|
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
|
||||||
"""
|
"""
|
||||||
@@ -118,9 +120,7 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
:param dept_id: 部门 ID
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).where(self.model.parent_id == dept_id, self.model.del_flag == 0)
|
return await self.select_models(db, parent_id=dept_id, del_flag=False)
|
||||||
result = await db.execute(stmt)
|
|
||||||
return result.scalars().all()
|
|
||||||
|
|
||||||
|
|
||||||
dept_dao: CRUDDept = CRUDDept(Dept)
|
dept_dao: CRUDDept = CRUDDept(Dept)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
from sqlalchemy import delete as sa_delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
@@ -11,9 +10,9 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
|
|||||||
class CRUDLoginLog(CRUDPlus[LoginLog]):
|
class CRUDLoginLog(CRUDPlus[LoginLog]):
|
||||||
"""登录日志数据库操作类"""
|
"""登录日志数据库操作类"""
|
||||||
|
|
||||||
async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取登录日志列表
|
获取登录日志列表查询表达式
|
||||||
|
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:param status: 登录状态
|
:param status: 登录状态
|
||||||
@@ -21,12 +20,14 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if username is not None:
|
if username is not None:
|
||||||
filters.update(username__like=f'%{username}%')
|
filters['username__like'] = f'%{username}%'
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.update(status=status)
|
filters['status'] = status
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
filters.update(ip__like=f'%{ip}%')
|
filters['ip__like'] = f'%{ip}%'
|
||||||
|
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
return await self.select_order('created_time', 'desc', **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
|
||||||
@@ -39,24 +40,25 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
|
|||||||
"""
|
"""
|
||||||
await self.create_model(db, obj, commit=True)
|
await self.create_model(db, obj, commit=True)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除登录日志
|
批量删除登录日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 登录日志 ID 列表
|
:param pks: 登录日志 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||||
|
|
||||||
async def delete_all(self, db: AsyncSession) -> int:
|
@staticmethod
|
||||||
|
async def delete_all(db: AsyncSession) -> None:
|
||||||
"""
|
"""
|
||||||
删除所有日志
|
删除所有日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True)
|
await db.execute(sa_delete(LoginLog))
|
||||||
|
|
||||||
|
|
||||||
login_log_dao: CRUDLoginLog = CRUDLoginLog(LoginLog)
|
login_log_dao: CRUDLoginLog = CRUDLoginLog(LoginLog)
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import and_, asc, select
|
from sqlalchemy import delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.app.admin.model import Menu
|
from backend.app.admin.model import Menu, role_menu
|
||||||
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
||||||
|
|
||||||
|
|
||||||
@@ -44,28 +41,28 @@ class CRUDMenu(CRUDPlus[Menu]):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if title is not None:
|
if title is not None:
|
||||||
filters.update(title_like=f'%{title}%')
|
filters['title__like'] = f'%{title}%'
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.update(status=status)
|
filters['status'] = status
|
||||||
|
|
||||||
return await self.select_models_order(db, 'sort', **filters)
|
return await self.select_models_order(db, 'sort', **filters)
|
||||||
|
|
||||||
async def get_sidebar(self, db: AsyncSession, superuser: bool, menu_ids: list[int | None]) -> Sequence[Menu]:
|
async def get_sidebar(self, db: AsyncSession, menu_ids: list[int] | None) -> Sequence[Menu]:
|
||||||
"""
|
"""
|
||||||
获取角色菜单列表
|
获取用户的菜单侧边栏
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param superuser: 是否超级管理员
|
|
||||||
:param menu_ids: 菜单 ID 列表
|
:param menu_ids: 菜单 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).order_by(asc(self.model.sort))
|
filters = {'type__in': [0, 1, 3, 4]}
|
||||||
filters = [self.model.type.in_([0, 1])]
|
|
||||||
if not superuser:
|
if menu_ids:
|
||||||
filters.append(self.model.id.in_(menu_ids))
|
filters['id__in'] = menu_ids
|
||||||
stmt = stmt.where(and_(*filters))
|
|
||||||
menu = await db.execute(stmt)
|
return await self.select_models_order(db, 'sort', 'asc', **filters)
|
||||||
return menu.scalars().all()
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -96,9 +93,12 @@ class CRUDMenu(CRUDPlus[Menu]):
|
|||||||
:param menu_id: 菜单 ID
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
role_menu_stmt = delete(role_menu).where(role_menu.c.menu_id == menu_id)
|
||||||
|
await db.execute(role_menu_stmt)
|
||||||
|
|
||||||
return await self.delete_model(db, menu_id)
|
return await self.delete_model(db, menu_id)
|
||||||
|
|
||||||
async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None]:
|
async def get_children(self, db: AsyncSession, menu_id: int) -> Sequence[Menu | None]:
|
||||||
"""
|
"""
|
||||||
获取子菜单列表
|
获取子菜单列表
|
||||||
|
|
||||||
@@ -106,10 +106,7 @@ class CRUDMenu(CRUDPlus[Menu]):
|
|||||||
:param menu_id: 菜单 ID
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
|
return await self.select_models(db, parent_id=menu_id)
|
||||||
result = await db.execute(stmt)
|
|
||||||
menu = result.scalars().first()
|
|
||||||
return menu.children
|
|
||||||
|
|
||||||
|
|
||||||
menu_dao: CRUDMenu = CRUDMenu(Menu)
|
menu_dao: CRUDMenu = CRUDMenu(Menu)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
from sqlalchemy import delete as sa_delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
@@ -11,9 +10,9 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
|
|||||||
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||||
"""操作日志数据库操作类"""
|
"""操作日志数据库操作类"""
|
||||||
|
|
||||||
async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取操作日志列表
|
获取操作日志列表查询表达式
|
||||||
|
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:param status: 操作状态
|
:param status: 操作状态
|
||||||
@@ -21,12 +20,14 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if username is not None:
|
if username is not None:
|
||||||
filters.update(username__like=f'%{username}%')
|
filters['username__like'] = f'%{username}%'
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.update(status=status)
|
filters['status__eq'] = status
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
filters.update(ip__like=f'%{ip}%')
|
filters['ip__like'] = f'%{ip}%'
|
||||||
|
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
return await self.select_order('created_time', 'desc', **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||||
@@ -34,29 +35,40 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
|||||||
创建操作日志
|
创建操作日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param obj: 创建操作日志参数
|
:param obj: 操作日志创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def bulk_create(self, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
|
||||||
"""
|
"""
|
||||||
删除操作日志
|
批量创建操作日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 操作日志 ID 列表
|
:param objs: 操作日志创建参数列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
await self.create_models(db, objs)
|
||||||
|
|
||||||
async def delete_all(self, db: AsyncSession) -> int:
|
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
批量删除操作日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pks: 操作日志 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_all(db: AsyncSession) -> None:
|
||||||
"""
|
"""
|
||||||
删除所有日志
|
删除所有日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True)
|
await db.execute(sa_delete(OperaLog))
|
||||||
|
|
||||||
|
|
||||||
opera_log_dao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog)
|
opera_log_dao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog)
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select, and_, desc, select
|
from sqlalchemy import Select, delete, insert, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.model import DataScope, Menu, Role, User
|
from backend.app.admin.model import DataScope, Menu, Role, role_data_scope, role_menu
|
||||||
from backend.app.admin.schema.role import (
|
from backend.app.admin.schema.role import (
|
||||||
|
CreateRoleMenuParam,
|
||||||
CreateRoleParam,
|
CreateRoleParam,
|
||||||
|
CreateRoleScopeParam,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
UpdateRoleParam,
|
UpdateRoleParam,
|
||||||
UpdateRoleScopeParam,
|
UpdateRoleScopeParam,
|
||||||
)
|
)
|
||||||
|
from backend.utils.serializers import select_join_serialize
|
||||||
|
|
||||||
|
|
||||||
class CRUDRole(CRUDPlus[Role]):
|
class CRUDRole(CRUDPlus[Role]):
|
||||||
@@ -29,7 +30,20 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model(db, role_id)
|
return await self.select_model(db, role_id)
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
|
@staticmethod
|
||||||
|
async def get_menus(db: AsyncSession, role_id: int) -> Sequence[Menu] | None:
|
||||||
|
"""
|
||||||
|
获取角色菜单
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param role_id: 角色 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
menu_stmt = select(Menu).join(role_menu, Menu.id == role_menu.c.menu_id).where(role_menu.c.role_id == role_id)
|
||||||
|
result = await db.execute(menu_stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def get_join(self, db: AsyncSession, role_id: int) -> Any:
|
||||||
"""
|
"""
|
||||||
获取角色及关联数据
|
获取角色及关联数据
|
||||||
|
|
||||||
@@ -37,13 +51,18 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
:param role_id: 角色 ID
|
:param role_id: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
result = await self.select_models(
|
||||||
select(self.model)
|
db,
|
||||||
.options(selectinload(self.model.menus), selectinload(self.model.scopes))
|
id=role_id,
|
||||||
.where(self.model.id == role_id)
|
join_conditions=[
|
||||||
|
JoinConfig(model=role_menu, join_on=role_menu.c.role_id == self.model.id),
|
||||||
|
JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True),
|
||||||
|
JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == self.model.id),
|
||||||
|
JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
role = await db.execute(stmt)
|
|
||||||
return role.scalars().first()
|
return select_join_serialize(result, relationships=['Role-m2m-Menu', 'Role-m2m-DataScope:scopes'])
|
||||||
|
|
||||||
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
|
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
|
||||||
"""
|
"""
|
||||||
@@ -54,42 +73,23 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def get_users(self, db: AsyncSession, user_id: int) -> Sequence[Role]:
|
async def get_select(self, name: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取用户角色列表
|
获取角色列表查询表达式
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param user_id: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stmt = select(self.model).join(self.model.users).where(User.id == user_id)
|
|
||||||
roles = await db.execute(stmt)
|
|
||||||
return roles.scalars().all()
|
|
||||||
|
|
||||||
async def get_list(self, name: str | None, status: int | None) -> Select:
|
|
||||||
"""
|
|
||||||
获取角色列表
|
|
||||||
|
|
||||||
:param name: 角色名称
|
:param name: 角色名称
|
||||||
:param status: 角色状态
|
:param status: 角色状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
|
||||||
select(self.model)
|
|
||||||
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.scopes))
|
|
||||||
.order_by(desc(self.model.created_time))
|
|
||||||
)
|
|
||||||
|
|
||||||
filters = []
|
filters = {}
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
filters.append(self.model.name.like(f'%{name}%'))
|
filters['name__like'] = f'%{name}%'
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.append(self.model.status == status)
|
filters['status'] = status
|
||||||
|
|
||||||
if filters:
|
return await self.select_order('id', **filters)
|
||||||
stmt = stmt.where(and_(*filters))
|
|
||||||
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
|
||||||
"""
|
"""
|
||||||
@@ -122,7 +122,8 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, role_id, obj)
|
return await self.update_model(db, role_id, obj)
|
||||||
|
|
||||||
async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
@staticmethod
|
||||||
|
async def update_menus(db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色菜单
|
更新角色菜单
|
||||||
|
|
||||||
@@ -131,13 +132,19 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
:param menu_ids: 菜单 ID 列表
|
:param menu_ids: 菜单 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_role = await self.get_with_relation(db, role_id)
|
role_menu_stmt = delete(role_menu).where(role_menu.c.role_id == role_id)
|
||||||
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
|
await db.execute(role_menu_stmt)
|
||||||
menus = await db.execute(stmt)
|
|
||||||
current_role.menus = menus.scalars().all()
|
|
||||||
return len(current_role.menus)
|
|
||||||
|
|
||||||
async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
|
role_menu_data = [
|
||||||
|
CreateRoleMenuParam(role_id=role_id, menu_id=menu_id).model_dump() for menu_id in menu_ids.menus
|
||||||
|
]
|
||||||
|
role_menu_stmt = insert(role_menu)
|
||||||
|
await db.execute(role_menu_stmt, role_menu_data)
|
||||||
|
|
||||||
|
return len(menu_ids.menus)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_scopes(db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色数据范围
|
更新角色数据范围
|
||||||
|
|
||||||
@@ -146,21 +153,26 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
:param scope_ids: 权限范围 ID 列表
|
:param scope_ids: 权限范围 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_role = await self.get_with_relation(db, role_id)
|
role_scope_stmt = delete(role_data_scope).where(role_data_scope.c.role_id == role_id)
|
||||||
stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
|
await db.execute(role_scope_stmt)
|
||||||
scopes = await db.execute(stmt)
|
|
||||||
current_role.scopes = scopes.scalars().all()
|
|
||||||
return len(current_role.scopes)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, role_id: list[int]) -> int:
|
role_scope_data = [
|
||||||
|
CreateRoleScopeParam(role_id=role_id, data_scope_id=scope_id).model_dump() for scope_id in scope_ids.scopes
|
||||||
|
]
|
||||||
|
role_scope_stmt = insert(role_data_scope)
|
||||||
|
await db.execute(role_scope_stmt, role_scope_data)
|
||||||
|
|
||||||
|
return len(scope_ids.scopes)
|
||||||
|
|
||||||
|
async def delete(self, db: AsyncSession, role_ids: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除角色
|
批量删除角色
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param role_id: 角色 ID 列表
|
:param role_ids: 角色 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_id)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_ids)
|
||||||
|
|
||||||
|
|
||||||
role_dao: CRUDRole = CRUDRole(Role)
|
role_dao: CRUDRole = CRUDRole(Role)
|
||||||
|
|||||||
+207
-149
@@ -1,20 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
from typing import Any
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
from sqlalchemy import and_, desc, select
|
from sqlalchemy import Select, delete, insert, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
|
||||||
from sqlalchemy.sql import Select
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.model import Dept, Role, User
|
from backend.app.admin.model import (
|
||||||
|
DataRule,
|
||||||
|
DataScope,
|
||||||
|
Dept,
|
||||||
|
Menu,
|
||||||
|
Role,
|
||||||
|
User,
|
||||||
|
data_scope_rule,
|
||||||
|
role_data_scope,
|
||||||
|
role_menu,
|
||||||
|
user_role,
|
||||||
|
)
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
|
AddOAuth2UserParam,
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
RegisterUserParam,
|
AddUserRoleParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
)
|
)
|
||||||
from backend.common.security.jwt import get_hash_password
|
from backend.app.admin.utils.password_security import get_hash_password
|
||||||
|
from backend.utils.import_parse import import_module_cached
|
||||||
|
from backend.utils.serializers import select_join_serialize
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -51,35 +63,47 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, nickname=nickname)
|
return await self.select_model_by_column(db, nickname=nickname)
|
||||||
|
|
||||||
async def update_login_time(self, db: AsyncSession, username: str) -> int:
|
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
更新用户最后登录时间
|
检查邮箱是否已被绑定
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
|
:param email: 电子邮箱
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_model_by_column(db, email=email)
|
||||||
|
|
||||||
|
async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
|
||||||
|
"""
|
||||||
|
获取用户列表查询表达式
|
||||||
|
|
||||||
|
:param dept: 部门 ID
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
|
:param phone: 电话号码
|
||||||
|
:param status: 用户状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
filters = {}
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool = False) -> None:
|
if dept:
|
||||||
"""
|
filters['dept_id'] = dept
|
||||||
创建用户
|
if username:
|
||||||
|
filters['username__like'] = f'%{username}%'
|
||||||
|
if phone:
|
||||||
|
filters['phone__like'] = f'%{phone}%'
|
||||||
|
if status is not None:
|
||||||
|
filters['status'] = status
|
||||||
|
|
||||||
:param db: 数据库会话
|
return await self.select_order(
|
||||||
:param obj: 注册用户参数
|
'id',
|
||||||
:param social: 是否社交用户
|
'desc',
|
||||||
:return:
|
join_conditions=[
|
||||||
"""
|
JoinConfig(model=Dept, join_on=Dept.id == self.model.dept_id, fill_result=True),
|
||||||
if not social:
|
JoinConfig(model=user_role, join_on=user_role.c.user_id == self.model.id),
|
||||||
salt = bcrypt.gensalt()
|
JoinConfig(model=Role, join_on=Role.id == user_role.c.role_id, fill_result=True),
|
||||||
obj.password = get_hash_password(obj.password, salt)
|
],
|
||||||
dict_obj = obj.model_dump()
|
**filters,
|
||||||
dict_obj.update({'is_staff': True, 'salt': salt})
|
)
|
||||||
else:
|
|
||||||
dict_obj = obj.model_dump()
|
|
||||||
dict_obj.update({'is_staff': True, 'salt': None})
|
|
||||||
new_user = self.model(**dict_obj)
|
|
||||||
db.add(new_user)
|
|
||||||
|
|
||||||
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -91,144 +115,136 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
obj.password = get_hash_password(obj.password, salt)
|
obj.password = get_hash_password(obj.password, salt)
|
||||||
|
|
||||||
dict_obj = obj.model_dump(exclude={'roles'})
|
dict_obj = obj.model_dump(exclude={'roles'})
|
||||||
dict_obj.update({'salt': salt})
|
dict_obj.update({'salt': salt})
|
||||||
new_user = self.model(**dict_obj)
|
new_user = self.model(**dict_obj)
|
||||||
|
|
||||||
stmt = select(Role).where(Role.id.in_(obj.roles))
|
|
||||||
roles = await db.execute(stmt)
|
|
||||||
new_user.roles = roles.scalars().all()
|
|
||||||
|
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int:
|
role_stmt = select(Role).where(Role.id.in_(obj.roles))
|
||||||
|
result = await db.execute(role_stmt)
|
||||||
|
roles = result.scalars().all()
|
||||||
|
|
||||||
|
user_role_data = [AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump() for role in roles]
|
||||||
|
user_role_stmt = insert(user_role)
|
||||||
|
await db.execute(user_role_stmt, user_role_data)
|
||||||
|
|
||||||
|
async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None:
|
||||||
|
"""
|
||||||
|
通过 OAuth2 添加用户
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 注册用户参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
dict_obj = obj.model_dump()
|
||||||
|
dict_obj.update({'is_staff': True, 'salt': None})
|
||||||
|
new_user = self.model(**dict_obj)
|
||||||
|
db.add(new_user)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
role_stmt = select(Role)
|
||||||
|
result = await db.execute(role_stmt)
|
||||||
|
role = result.scalars().first() # 默认绑定第一个角色
|
||||||
|
|
||||||
|
user_role_stmt = insert(user_role).values(AddUserRoleParam(user_id=new_user.id, role_id=role.id).model_dump())
|
||||||
|
await db.execute(user_role_stmt)
|
||||||
|
|
||||||
|
async def update(self, db: AsyncSession, user_id: int, obj: UpdateUserParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户信息
|
更新用户信息
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param input_user: 用户 ID
|
:param user_id: 用户 ID
|
||||||
:param obj: 更新用户参数
|
:param obj: 更新用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
role_ids = obj.roles
|
role_ids = obj.roles
|
||||||
del obj.roles
|
del obj.roles
|
||||||
count = await self.update_model(db, input_user.id, obj)
|
|
||||||
|
|
||||||
stmt = select(Role).where(Role.id.in_(role_ids))
|
count = await self.update_model(db, user_id, obj)
|
||||||
roles = await db.execute(stmt)
|
|
||||||
input_user.roles = roles.scalars().all()
|
role_stmt = select(Role).where(Role.id.in_(role_ids))
|
||||||
|
result = await db.execute(role_stmt)
|
||||||
|
roles = result.scalars().all()
|
||||||
|
|
||||||
|
user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id)
|
||||||
|
await db.execute(user_role_stmt)
|
||||||
|
|
||||||
|
user_role_data = [AddUserRoleParam(user_id=user_id, role_id=role.id).model_dump() for role in roles]
|
||||||
|
user_role_stmt = insert(user_role)
|
||||||
|
await db.execute(user_role_stmt, user_role_data)
|
||||||
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
async def update_login_time(self, db: AsyncSession, username: str) -> int:
|
||||||
"""
|
"""
|
||||||
删除用户
|
更新用户上次登录时间
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param username: 用户名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
||||||
|
|
||||||
|
async def update_password_changed_time(self, db: AsyncSession, user_id: int) -> int:
|
||||||
|
"""
|
||||||
|
更新用户上次密码变更时间
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param user_id: 用户 ID
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, user_id)
|
return await self.update_model(db, user_id, {'last_password_changed_time': timezone.now()})
|
||||||
|
|
||||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int:
|
||||||
"""
|
"""
|
||||||
检查邮箱是否已被注册
|
更新用户昵称
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param email: 电子邮箱
|
:param user_id: 用户 ID
|
||||||
|
:param nickname: 用户昵称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, email=email)
|
return await self.update_model(db, user_id, {'nickname': nickname})
|
||||||
|
|
||||||
async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int:
|
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||||
|
"""
|
||||||
|
更新用户头像
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param avatar: 头像地址
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
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 reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
|
||||||
"""
|
"""
|
||||||
重置用户密码
|
重置用户密码
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 用户 ID
|
:param pk: 用户 ID
|
||||||
:param new_pwd: 新密码(已加密)
|
:param password: 新密码
|
||||||
:return:
|
: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}, flush=True)
|
||||||
|
|
||||||
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
|
async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int:
|
||||||
"""
|
|
||||||
获取用户列表
|
|
||||||
|
|
||||||
:param dept: 部门 ID
|
|
||||||
:param username: 用户名
|
|
||||||
:param phone: 电话号码
|
|
||||||
:param status: 用户状态
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stmt = (
|
|
||||||
select(self.model)
|
|
||||||
.options(
|
|
||||||
selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)),
|
|
||||||
selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)),
|
|
||||||
)
|
|
||||||
.order_by(desc(self.model.join_time))
|
|
||||||
)
|
|
||||||
|
|
||||||
filters = []
|
|
||||||
if dept:
|
|
||||||
filters.append(self.model.dept_id == dept)
|
|
||||||
if username:
|
|
||||||
filters.append(self.model.username.like(f'%{username}%'))
|
|
||||||
if phone:
|
|
||||||
filters.append(self.model.phone.like(f'%{phone}%'))
|
|
||||||
if status is not None:
|
|
||||||
filters.append(self.model.status == status)
|
|
||||||
|
|
||||||
if filters:
|
|
||||||
stmt = stmt.where(and_(*filters))
|
|
||||||
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_super(self, db: AsyncSession, user_id: int) -> bool:
|
|
||||||
"""
|
|
||||||
获取用户是否为超级管理员
|
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param user_id: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
user = await self.get(db, user_id)
|
|
||||||
return user.is_superuser
|
|
||||||
|
|
||||||
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
|
|
||||||
"""
|
|
||||||
获取用户是否可以登录后台
|
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param user_id: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
user = await self.get(db, user_id)
|
|
||||||
return user.is_staff
|
|
||||||
|
|
||||||
async def get_status(self, db: AsyncSession, user_id: int) -> int:
|
|
||||||
"""
|
|
||||||
获取用户状态
|
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param user_id: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
user = await self.get(db, user_id)
|
|
||||||
return user.status
|
|
||||||
|
|
||||||
async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool:
|
|
||||||
"""
|
|
||||||
获取用户是否允许多端登录
|
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param user_id: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
user = await self.get(db, user_id)
|
|
||||||
return user.is_multi_login
|
|
||||||
|
|
||||||
async def set_super(self, db: AsyncSession, user_id: int, is_super: bool) -> int:
|
|
||||||
"""
|
"""
|
||||||
设置用户超级管理员状态
|
设置用户超级管理员状态
|
||||||
|
|
||||||
@@ -239,7 +255,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'is_superuser': is_super})
|
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:
|
||||||
"""
|
"""
|
||||||
设置用户后台登录状态
|
设置用户后台登录状态
|
||||||
|
|
||||||
@@ -261,7 +277,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'status': status})
|
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:
|
||||||
"""
|
"""
|
||||||
设置用户多端登录状态
|
设置用户多端登录状态
|
||||||
|
|
||||||
@@ -272,9 +288,34 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
||||||
|
|
||||||
async def get_with_relation(
|
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||||
self, db: AsyncSession, *, user_id: int | None = None, username: str | None = None
|
"""
|
||||||
) -> User | None:
|
删除用户
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id)
|
||||||
|
await db.execute(user_role_stmt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_social = import_module_cached('backend.plugin.oauth2.crud.crud_user_social')
|
||||||
|
user_social_dao = user_social.user_social_dao
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
await user_social_dao.delete_by_user_id(db, user_id)
|
||||||
|
|
||||||
|
return await self.delete_model(db, user_id)
|
||||||
|
|
||||||
|
async def get_join(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
user_id: int | None = None,
|
||||||
|
username: str | None = None,
|
||||||
|
) -> Any | None:
|
||||||
"""
|
"""
|
||||||
获取用户关联信息
|
获取用户关联信息
|
||||||
|
|
||||||
@@ -283,22 +324,39 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(
|
filters = {}
|
||||||
selectinload(self.model.dept),
|
|
||||||
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes)),
|
if user_id:
|
||||||
|
filters['id'] = user_id
|
||||||
|
if username:
|
||||||
|
filters['username'] = username
|
||||||
|
|
||||||
|
result = await self.select_models(
|
||||||
|
db,
|
||||||
|
join_conditions=[
|
||||||
|
JoinConfig(model=Dept, join_on=Dept.id == self.model.dept_id, fill_result=True),
|
||||||
|
JoinConfig(model=user_role, join_on=user_role.c.user_id == self.model.id),
|
||||||
|
JoinConfig(model=Role, join_on=Role.id == user_role.c.role_id, fill_result=True),
|
||||||
|
JoinConfig(model=role_menu, join_on=role_menu.c.role_id == Role.id),
|
||||||
|
JoinConfig(model=Menu, join_on=Menu.id == role_menu.c.menu_id, fill_result=True),
|
||||||
|
JoinConfig(model=role_data_scope, join_on=role_data_scope.c.role_id == Role.id),
|
||||||
|
JoinConfig(model=DataScope, join_on=DataScope.id == role_data_scope.c.data_scope_id, fill_result=True),
|
||||||
|
JoinConfig(model=data_scope_rule, join_on=data_scope_rule.c.data_scope_id == DataScope.id),
|
||||||
|
JoinConfig(model=DataRule, join_on=DataRule.id == data_scope_rule.c.data_rule_id, fill_result=True),
|
||||||
|
],
|
||||||
|
**filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
filters = []
|
return select_join_serialize(
|
||||||
if user_id:
|
result,
|
||||||
filters.append(self.model.id == user_id)
|
relationships=[
|
||||||
if username:
|
'User-m2o-Dept',
|
||||||
filters.append(self.model.username == username)
|
'User-m2m-Role',
|
||||||
|
'Role-m2m-Menu',
|
||||||
if filters:
|
'Role-m2m-DataScope:scopes',
|
||||||
stmt = stmt.where(and_(*filters))
|
'DataScope-m2m-DataRule:rules',
|
||||||
|
],
|
||||||
user = await db.execute(stmt)
|
)
|
||||||
return user.scalars().first()
|
|
||||||
|
|
||||||
|
|
||||||
user_dao: CRUDUser = CRUDUser(User)
|
user_dao: CRUDUser = CRUDUser(User)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
|
from backend.app.admin.model.user_password_history import UserPasswordHistory
|
||||||
|
from backend.app.admin.schema.user_password_history import CreateUserPasswordHistoryParam
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDUserPasswordHistory(CRUDPlus[UserPasswordHistory]):
|
||||||
|
"""用户密码历史记录数据库操作类"""
|
||||||
|
|
||||||
|
async def create(self, db: AsyncSession, obj: CreateUserPasswordHistoryParam) -> None:
|
||||||
|
"""
|
||||||
|
创建密码历史记录
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 创建密码历史记录参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
|
async def get_by_user_id(self, db: AsyncSession, user_id: int) -> Sequence[UserPasswordHistory]:
|
||||||
|
"""
|
||||||
|
获取用户的密码历史记录
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_models_order(db, 'id', 'desc', self.model.user_id == user_id)
|
||||||
|
|
||||||
|
|
||||||
|
user_password_history_dao: CRUDUserPasswordHistory = CRUDUserPasswordHistory(UserPasswordHistory)
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
from backend.app.admin.model.data_rule import DataRule as DataRule
|
||||||
# -*- coding: utf-8 -*-
|
from backend.app.admin.model.data_scope import DataScope as DataScope
|
||||||
from backend.app.admin.model.data_rule import DataRule
|
from backend.app.admin.model.dept import Dept as Dept
|
||||||
from backend.app.admin.model.data_scope import DataScope
|
from backend.app.admin.model.login_log import LoginLog as LoginLog
|
||||||
from backend.app.admin.model.dept import Dept
|
from backend.app.admin.model.m2m import data_scope_rule as data_scope_rule
|
||||||
from backend.app.admin.model.login_log import LoginLog
|
from backend.app.admin.model.m2m import role_data_scope as role_data_scope
|
||||||
from backend.app.admin.model.menu import Menu
|
from backend.app.admin.model.m2m import role_menu as role_menu
|
||||||
from backend.app.admin.model.opera_log import OperaLog
|
from backend.app.admin.model.m2m import user_role as user_role
|
||||||
from backend.app.admin.model.role import Role
|
from backend.app.admin.model.menu import Menu as Menu
|
||||||
from backend.app.admin.model.user import User
|
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
|
||||||
|
from backend.app.admin.model.user_password_history import UserPasswordHistory as UserPasswordHistory
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import DataScope
|
|
||||||
|
|
||||||
|
|
||||||
class DataRule(Base):
|
class DataRule(Base):
|
||||||
"""数据规则表"""
|
"""数据规则表"""
|
||||||
@@ -19,17 +11,11 @@ class DataRule(Base):
|
|||||||
__tablename__ = 'sys_data_rule'
|
__tablename__ = 'sys_data_rule'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
|
name: Mapped[str] = mapped_column(sa.String(512), unique=True, comment='名称')
|
||||||
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
|
model: Mapped[str] = mapped_column(sa.String(64), comment='模型名称')
|
||||||
column: Mapped[str] = mapped_column(String(20), comment='模型字段名')
|
column: Mapped[str] = mapped_column(sa.String(32), comment='模型字段名')
|
||||||
operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)')
|
operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)')
|
||||||
expression: Mapped[int] = mapped_column(
|
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='规则值')
|
value: Mapped[str] = mapped_column(sa.String(256), comment='规则值')
|
||||||
|
|
||||||
# 数据范围规则一对多
|
|
||||||
scope_id: Mapped[int | None] = mapped_column(
|
|
||||||
ForeignKey('sys_data_scope.id', ondelete='SET NULL'), default=None, comment='数据范围关联 ID'
|
|
||||||
)
|
|
||||||
scope: Mapped[DataScope] = relationship(init=False, back_populates='rules')
|
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_scope
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import DataRule, Role
|
|
||||||
|
|
||||||
|
|
||||||
class DataScope(Base):
|
class DataScope(Base):
|
||||||
"""数据范围表"""
|
"""数据范围表"""
|
||||||
@@ -20,11 +11,5 @@ class DataScope(Base):
|
|||||||
__tablename__ = 'sys_data_scope'
|
__tablename__ = 'sys_data_scope'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
|
name: Mapped[str] = mapped_column(sa.String(64), unique=True, comment='名称')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
||||||
|
|
||||||
# 数据范围规则一对多
|
|
||||||
rules: Mapped[list[DataRule]] = relationship(init=False, back_populates='scope')
|
|
||||||
|
|
||||||
# 角色数据范围多对多
|
|
||||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes')
|
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from sqlalchemy import Boolean, ForeignKey, String
|
|
||||||
from sqlalchemy.dialects.postgresql import INTEGER
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import User
|
|
||||||
|
|
||||||
|
|
||||||
class Dept(Base):
|
class Dept(Base):
|
||||||
"""部门表"""
|
"""部门表"""
|
||||||
@@ -20,22 +11,13 @@ class Dept(Base):
|
|||||||
__tablename__ = 'sys_dept'
|
__tablename__ = 'sys_dept'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
name: Mapped[str] = mapped_column(String(50), comment='部门名称')
|
name: Mapped[str] = mapped_column(sa.String(64), comment='部门名称')
|
||||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||||
leader: Mapped[str | None] = mapped_column(String(20), default=None, comment='负责人')
|
leader: Mapped[str | None] = mapped_column(sa.String(32), default=None, comment='负责人')
|
||||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机')
|
phone: Mapped[str | None] = mapped_column(sa.String(11), default=None, comment='手机')
|
||||||
email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(sa.String(64), default=None, comment='邮箱')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
|
||||||
del_flag: Mapped[bool] = mapped_column(
|
del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在)')
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='删除标志(0删除 1存在)'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 父级部门一对多
|
# 父级部门
|
||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(sa.BigInteger, default=None, index=True, comment='父部门ID')
|
||||||
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')
|
|
||||||
|
|
||||||
# 部门用户一对多
|
|
||||||
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, String
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from backend.common.model import DataClassBase, id_key
|
from backend.common.model import DataClassBase, TimeZone, UniversalText, id_key
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -17,19 +14,22 @@ class LoginLog(DataClassBase):
|
|||||||
__tablename__ = 'sys_login_log'
|
__tablename__ = 'sys_login_log'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
user_uuid: Mapped[str] = mapped_column(String(50), comment='用户UUID')
|
user_uuid: Mapped[str] = mapped_column(sa.String(64), comment='用户UUID')
|
||||||
username: Mapped[str] = mapped_column(String(20), comment='用户名')
|
username: Mapped[str] = mapped_column(sa.String(64), comment='用户名')
|
||||||
status: Mapped[int] = mapped_column(insert_default=0, comment='登录状态(0失败 1成功)')
|
status: Mapped[int] = mapped_column(insert_default=0, comment='登录状态(0失败 1成功)')
|
||||||
ip: Mapped[str] = mapped_column(String(50), comment='登录IP地址')
|
ip: Mapped[str] = mapped_column(sa.String(64), comment='登录IP地址')
|
||||||
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
country: Mapped[str | None] = mapped_column(sa.String(64), comment='国家')
|
||||||
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
region: Mapped[str | None] = mapped_column(sa.String(64), comment='地区')
|
||||||
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
city: Mapped[str | None] = mapped_column(sa.String(64), comment='城市')
|
||||||
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
|
user_agent: Mapped[str | None] = mapped_column(sa.String(256), comment='请求头')
|
||||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
os: Mapped[str | None] = mapped_column(sa.String(64), comment='操作系统')
|
||||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
browser: Mapped[str | None] = mapped_column(sa.String(64), comment='浏览器')
|
||||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
device: Mapped[str | None] = mapped_column(sa.String(64), comment='设备')
|
||||||
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
msg: Mapped[str] = mapped_column(UniversalText, 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(
|
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,35 +1,39 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
|
|
||||||
|
|
||||||
from backend.common.model import MappedBase
|
from backend.common.model import MappedBase
|
||||||
|
|
||||||
sys_user_role = Table(
|
# 用户角色表
|
||||||
|
user_role = sa.Table(
|
||||||
'sys_user_role',
|
'sys_user_role',
|
||||||
MappedBase.metadata,
|
MappedBase.metadata,
|
||||||
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||||
Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
|
sa.Column('user_id', sa.BigInteger, primary_key=True, comment='用户ID'),
|
||||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色ID'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sys_role_menu = Table(
|
# 角色菜单表
|
||||||
|
role_menu = sa.Table(
|
||||||
'sys_role_menu',
|
'sys_role_menu',
|
||||||
MappedBase.metadata,
|
MappedBase.metadata,
|
||||||
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色ID'),
|
||||||
Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
|
sa.Column('menu_id', sa.BigInteger, primary_key=True, comment='菜单ID'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sys_role_data_scope = Table(
|
# 角色数据范围表
|
||||||
|
role_data_scope = sa.Table(
|
||||||
'sys_role_data_scope',
|
'sys_role_data_scope',
|
||||||
MappedBase.metadata,
|
MappedBase.metadata,
|
||||||
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
|
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
|
||||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
|
sa.Column('role_id', sa.BigInteger, primary_key=True, comment='角色 ID'),
|
||||||
Column(
|
sa.Column('data_scope_id', sa.BigInteger, primary_key=True, comment='数据范围 ID'),
|
||||||
'data_scope_id',
|
)
|
||||||
Integer,
|
|
||||||
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
# 数据范围规则表
|
||||||
primary_key=True,
|
data_scope_rule = sa.Table(
|
||||||
comment='数据范围 ID',
|
'sys_data_scope_rule',
|
||||||
),
|
MappedBase.metadata,
|
||||||
|
sa.Column('id', sa.BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||||
|
sa.Column('data_scope_id', sa.BigInteger, primary_key=True, comment='数据范围 ID'),
|
||||||
|
sa.Column('data_rule_id', sa.BigInteger, primary_key=True, comment='数据规则 ID'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from backend.common.model import Base, UniversalText, id_key
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_menu
|
|
||||||
from backend.common.model import Base, id_key
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import Role
|
|
||||||
|
|
||||||
|
|
||||||
class Menu(Base):
|
class Menu(Base):
|
||||||
@@ -22,30 +11,19 @@ class Menu(Base):
|
|||||||
__tablename__ = 'sys_menu'
|
__tablename__ = 'sys_menu'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
|
title: Mapped[str] = mapped_column(sa.String(64), comment='菜单标题')
|
||||||
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
name: Mapped[str] = mapped_column(sa.String(64), comment='菜单名称')
|
||||||
path: Mapped[str | None] = mapped_column(String(200), comment='路由地址')
|
path: Mapped[str | None] = mapped_column(sa.String(200), comment='路由地址')
|
||||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||||
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
|
icon: Mapped[str | None] = mapped_column(sa.String(128), default=None, comment='菜单图标')
|
||||||
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||||
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
component: Mapped[str | None] = mapped_column(sa.String(256), default=None, comment='组件路径')
|
||||||
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
perms: Mapped[str | None] = mapped_column(sa.String(128), default=None, comment='权限标识')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
||||||
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
|
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
|
||||||
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
|
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
|
||||||
link: Mapped[str | None] = mapped_column(
|
link: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='外链地址')
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='外链地址'
|
remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注')
|
||||||
)
|
|
||||||
remark: Mapped[str | None] = mapped_column(
|
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 父级菜单一对多
|
# 父级菜单
|
||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(sa.BigInteger, default=None, index=True, comment='父菜单ID')
|
||||||
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')
|
|
||||||
|
|
||||||
# 菜单角色多对多
|
|
||||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, String
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
|
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from backend.common.model import DataClassBase, id_key
|
from backend.common.model import DataClassBase, TimeZone, UniversalText, id_key
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -17,25 +14,25 @@ class OperaLog(DataClassBase):
|
|||||||
__tablename__ = 'sys_opera_log'
|
__tablename__ = 'sys_opera_log'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
trace_id: Mapped[str] = mapped_column(String(32), comment='请求跟踪 ID')
|
trace_id: Mapped[str] = mapped_column(sa.String(32), comment='请求跟踪 ID')
|
||||||
username: Mapped[str | None] = mapped_column(String(20), comment='用户名')
|
username: Mapped[str | None] = mapped_column(sa.String(64), comment='用户名')
|
||||||
method: Mapped[str] = mapped_column(String(20), comment='请求类型')
|
method: Mapped[str] = mapped_column(sa.String(32), comment='请求类型')
|
||||||
title: Mapped[str] = mapped_column(String(255), comment='操作模块')
|
title: Mapped[str] = mapped_column(sa.String(256), comment='操作模块')
|
||||||
path: Mapped[str] = mapped_column(String(500), comment='请求路径')
|
path: Mapped[str] = mapped_column(sa.String(512), comment='请求路径')
|
||||||
ip: Mapped[str] = mapped_column(String(50), comment='IP地址')
|
ip: Mapped[str] = mapped_column(sa.String(64), comment='IP地址')
|
||||||
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
country: Mapped[str | None] = mapped_column(sa.String(64), comment='国家')
|
||||||
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
region: Mapped[str | None] = mapped_column(sa.String(64), comment='地区')
|
||||||
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
city: Mapped[str | None] = mapped_column(sa.String(64), comment='城市')
|
||||||
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
|
user_agent: Mapped[str | None] = mapped_column(sa.String(512), comment='请求头')
|
||||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
os: Mapped[str | None] = mapped_column(sa.String(64), comment='操作系统')
|
||||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
browser: Mapped[str | None] = mapped_column(sa.String(64), comment='浏览器')
|
||||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
device: Mapped[str | None] = mapped_column(sa.String(64), comment='设备')
|
||||||
args: Mapped[str | None] = mapped_column(JSON(), comment='请求参数')
|
args: Mapped[str | None] = mapped_column(sa.JSON(), comment='请求参数')
|
||||||
status: Mapped[int] = mapped_column(comment='操作状态(0异常 1正常)')
|
status: Mapped[int] = mapped_column(comment='操作状态(0异常 1正常)')
|
||||||
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
|
code: Mapped[str] = mapped_column(sa.String(32), insert_default='200', comment='操作状态码')
|
||||||
msg: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
msg: Mapped[str | None] = mapped_column(UniversalText, comment='提示消息')
|
||||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms)')
|
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(
|
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,19 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
import sqlalchemy as sa
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from sqlalchemy import String
|
from backend.common.model import Base, UniversalText, id_key
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
|
|
||||||
from backend.common.model import Base, id_key
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import DataScope, Menu, User
|
|
||||||
|
|
||||||
|
|
||||||
class Role(Base):
|
class Role(Base):
|
||||||
@@ -22,17 +11,7 @@ class Role(Base):
|
|||||||
__tablename__ = 'sys_role'
|
__tablename__ = 'sys_role'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
name: Mapped[str] = mapped_column(sa.String(32), unique=True, comment='角色名称')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
||||||
remark: Mapped[str | None] = mapped_column(
|
is_filter_scopes: Mapped[bool] = mapped_column(default=True, comment='过滤数据权限(0否 1是)')
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
remark: Mapped[str | None] = mapped_column(UniversalText, default=None, comment='备注')
|
||||||
)
|
|
||||||
|
|
||||||
# 角色用户多对多
|
|
||||||
users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles')
|
|
||||||
|
|
||||||
# 角色菜单多对多
|
|
||||||
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
|
|
||||||
|
|
||||||
# 角色数据范围多对多
|
|
||||||
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='roles')
|
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sqlalchemy import VARBINARY, Boolean, DateTime, ForeignKey, String
|
import sqlalchemy as sa
|
||||||
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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
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.database.db import uuid4_str
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import Dept, Role
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
"""用户表"""
|
"""用户表"""
|
||||||
@@ -24,36 +15,25 @@ class User(Base):
|
|||||||
__tablename__ = 'sys_user'
|
__tablename__ = 'sys_user'
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
uuid: Mapped[str] = mapped_column(sa.String(64), init=False, default_factory=uuid4_str, unique=True)
|
||||||
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
username: Mapped[str] = mapped_column(sa.String(64), unique=True, index=True, comment='用户名')
|
||||||
nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称')
|
nickname: Mapped[str] = mapped_column(sa.String(64), comment='昵称')
|
||||||
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
password: Mapped[str | None] = mapped_column(sa.String(256), comment='密码')
|
||||||
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
salt: Mapped[bytes | None] = mapped_column(sa.LargeBinary(255), comment='加密盐')
|
||||||
email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(sa.String(256), default=None, unique=True, index=True, comment='邮箱')
|
||||||
is_superuser: Mapped[bool] = mapped_column(
|
phone: Mapped[str | None] = mapped_column(sa.String(11), default=None, comment='手机号')
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
avatar: Mapped[str | None] = mapped_column(sa.String(256), default=None, comment='头像')
|
||||||
)
|
|
||||||
is_staff: Mapped[bool] = mapped_column(
|
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
|
||||||
)
|
|
||||||
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||||
is_multi_login: Mapped[bool] = mapped_column(
|
is_superuser: Mapped[bool] = mapped_column(default=False, comment='超级权限(0否 1是)')
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
is_staff: Mapped[bool] = mapped_column(default=False, comment='后台管理登陆(0否 1是)')
|
||||||
)
|
is_multi_login: Mapped[bool] = mapped_column(default=False, comment='是否重复登陆(0否 1是)')
|
||||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
join_time: Mapped[datetime] = mapped_column(TimeZone, init=False, default_factory=timezone.now, comment='注册时间')
|
||||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
|
||||||
join_time: Mapped[datetime] = mapped_column(
|
|
||||||
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='注册时间'
|
|
||||||
)
|
|
||||||
last_login_time: Mapped[datetime | None] = mapped_column(
|
last_login_time: Mapped[datetime | None] = mapped_column(
|
||||||
DateTime(timezone=True), init=False, onupdate=timezone.now, comment='上次登录'
|
TimeZone, init=False, onupdate=timezone.now, comment='上次登录时间'
|
||||||
|
)
|
||||||
|
last_password_changed_time: Mapped[datetime | None] = mapped_column(
|
||||||
|
TimeZone, init=False, default_factory=timezone.now, comment='上次密码变更时间'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 部门用户一对多
|
# 逻辑外键
|
||||||
dept_id: Mapped[int | None] = mapped_column(
|
dept_id: Mapped[int | None] = mapped_column(sa.BigInteger, 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')
|
|
||||||
|
|
||||||
# 用户角色多对多
|
|
||||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from backend.common.model import DataClassBase, TimeZone, id_key
|
||||||
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordHistory(DataClassBase):
|
||||||
|
"""用户密码历史记录表"""
|
||||||
|
|
||||||
|
__tablename__ = 'sys_user_password_history'
|
||||||
|
|
||||||
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
|
user_id: Mapped[int] = mapped_column(sa.BigInteger, index=True, comment='用户 ID')
|
||||||
|
password: Mapped[str] = mapped_column(sa.String(256), comment='历史密码')
|
||||||
|
created_time: Mapped[datetime] = mapped_column(
|
||||||
|
TimeZone,
|
||||||
|
init=False,
|
||||||
|
default_factory=timezone.now,
|
||||||
|
comment='创建时间',
|
||||||
|
)
|
||||||
@@ -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 pydantic import Field
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
@@ -8,5 +6,7 @@ from backend.common.schema import SchemaBase
|
|||||||
class GetCaptchaDetail(SchemaBase):
|
class GetCaptchaDetail(SchemaBase):
|
||||||
"""验证码详情"""
|
"""验证码详情"""
|
||||||
|
|
||||||
image_type: str = Field(description='图片类型')
|
is_enabled: bool = Field(description='是否启用')
|
||||||
|
expire_seconds: int = Field(description='过期秒数')
|
||||||
|
uuid: str = Field(description='图片唯一标识')
|
||||||
image: str = Field(description='图片内容')
|
image: str = Field(description='图片内容')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
@@ -14,8 +12,8 @@ class DataRuleSchemaBase(SchemaBase):
|
|||||||
name: str = Field(description='规则名称')
|
name: str = Field(description='规则名称')
|
||||||
model: str = Field(description='模型名称')
|
model: str = Field(description='模型名称')
|
||||||
column: str = Field(description='字段名称')
|
column: str = Field(description='字段名称')
|
||||||
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.AND, description='操作符(AND/OR)')
|
operator: RoleDataRuleOperatorType = Field(description='操作符(AND/OR)')
|
||||||
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq, description='表达式类型')
|
expression: RoleDataRuleExpressionType = Field(description='表达式类型')
|
||||||
value: str = Field(description='规则值')
|
value: str = Field(description='规则值')
|
||||||
|
|
||||||
|
|
||||||
@@ -27,10 +25,16 @@ class UpdateDataRuleParam(DataRuleSchemaBase):
|
|||||||
"""更新数据规则参数"""
|
"""更新数据规则参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDataRuleParam(SchemaBase):
|
||||||
|
"""删除数据规则参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='规则 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetDataRuleDetail(DataRuleSchemaBase):
|
class GetDataRuleDetail(DataRuleSchemaBase):
|
||||||
"""数据规则详情"""
|
"""数据规则详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True, frozen=True)
|
||||||
|
|
||||||
id: int = Field(description='规则 ID')
|
id: int = Field(description='规则 ID')
|
||||||
created_time: datetime = Field(description='创建时间')
|
created_time: datetime = Field(description='创建时间')
|
||||||
@@ -41,4 +45,4 @@ class GetDataRuleColumnDetail(SchemaBase):
|
|||||||
"""数据规则可用模型字段详情"""
|
"""数据规则可用模型字段详情"""
|
||||||
|
|
||||||
key: str = Field(description='字段名')
|
key: str = Field(description='字段名')
|
||||||
comment: str = Field(description='字段评论')
|
comment: str | None = Field(description='字段评论')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
@@ -13,7 +11,7 @@ class DataScopeBase(SchemaBase):
|
|||||||
"""数据范围基础模型"""
|
"""数据范围基础模型"""
|
||||||
|
|
||||||
name: str = Field(description='名称')
|
name: str = Field(description='名称')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(description='状态')
|
||||||
|
|
||||||
|
|
||||||
class CreateDataScopeParam(DataScopeBase):
|
class CreateDataScopeParam(DataScopeBase):
|
||||||
@@ -24,12 +22,25 @@ class UpdateDataScopeParam(DataScopeBase):
|
|||||||
"""更新数据范围参数"""
|
"""更新数据范围参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDataScopeRuleParam(SchemaBase):
|
||||||
|
"""创建数据范围规则参数"""
|
||||||
|
|
||||||
|
data_scope_id: int = Field(description='数据范围 ID')
|
||||||
|
data_rule_id: int = Field(description='数据规则 ID')
|
||||||
|
|
||||||
|
|
||||||
class UpdateDataScopeRuleParam(SchemaBase):
|
class UpdateDataScopeRuleParam(SchemaBase):
|
||||||
"""更新数据范围规则参数"""
|
"""更新数据范围规则参数"""
|
||||||
|
|
||||||
rules: list[int] = Field(description='数据规则 ID 列表')
|
rules: list[int] = Field(description='数据规则 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDataScopeParam(SchemaBase):
|
||||||
|
"""删除数据范围参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='数据范围 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetDataScopeDetail(DataScopeBase):
|
class GetDataScopeDetail(DataScopeBase):
|
||||||
"""数据范围详情"""
|
"""数据范围详情"""
|
||||||
|
|
||||||
@@ -43,4 +54,4 @@ class GetDataScopeDetail(DataScopeBase):
|
|||||||
class GetDataScopeWithRelationDetail(GetDataScopeDetail):
|
class GetDataScopeWithRelationDetail(GetDataScopeDetail):
|
||||||
"""数据范围关联详情"""
|
"""数据范围关联详情"""
|
||||||
|
|
||||||
rules: list[GetDataRuleDetail] = Field([], description='数据规则列表')
|
rules: list[GetDataRuleDetail | None] = Field([], description='数据规则列表')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
@@ -17,7 +15,7 @@ class DeptSchemaBase(SchemaBase):
|
|||||||
leader: str | None = Field(None, description='负责人')
|
leader: str | None = Field(None, description='负责人')
|
||||||
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
|
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
|
||||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(description='状态')
|
||||||
|
|
||||||
|
|
||||||
class CreateDeptParam(DeptSchemaBase):
|
class CreateDeptParam(DeptSchemaBase):
|
||||||
@@ -37,3 +35,9 @@ class GetDeptDetail(DeptSchemaBase):
|
|||||||
del_flag: bool = Field(description='是否删除')
|
del_flag: bool = Field(description='是否删除')
|
||||||
created_time: datetime = Field(description='创建时间')
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = Field(None, 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 datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
@@ -17,7 +15,7 @@ class LoginLogSchemaBase(SchemaBase):
|
|||||||
country: str | None = Field(None, description='国家')
|
country: str | None = Field(None, description='国家')
|
||||||
region: str | None = Field(None, description='地区')
|
region: str | None = Field(None, description='地区')
|
||||||
city: str | None = Field(None, description='城市')
|
city: str | None = Field(None, description='城市')
|
||||||
user_agent: str = Field(description='用户代理')
|
user_agent: str | None = Field(description='用户代理')
|
||||||
browser: str | None = Field(None, description='浏览器')
|
browser: str | None = Field(None, description='浏览器')
|
||||||
os: str | None = Field(None, description='操作系统')
|
os: str | None = Field(None, description='操作系统')
|
||||||
device: str | None = Field(None, description='设备')
|
device: str | None = Field(None, description='设备')
|
||||||
@@ -33,6 +31,12 @@ class UpdateLoginLogParam(LoginLogSchemaBase):
|
|||||||
"""更新登录日志参数"""
|
"""更新登录日志参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteLoginLogParam(SchemaBase):
|
||||||
|
"""删除登录日志参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='登录日志 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetLoginLogDetail(LoginLogSchemaBase):
|
class GetLoginLogDetail(LoginLogSchemaBase):
|
||||||
"""登录日志详情"""
|
"""登录日志详情"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
@@ -17,12 +15,12 @@ class MenuSchemaBase(SchemaBase):
|
|||||||
parent_id: int | None = Field(None, description='菜单父级 ID')
|
parent_id: int | None = Field(None, description='菜单父级 ID')
|
||||||
sort: int = Field(0, ge=0, description='排序')
|
sort: int = Field(0, ge=0, description='排序')
|
||||||
icon: str | None = Field(None, description='图标')
|
icon: str | None = Field(None, description='图标')
|
||||||
type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
type: MenuType = Field(description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||||
component: str | None = Field(None, description='组件路径')
|
component: str | None = Field(None, description='组件路径')
|
||||||
perms: str | None = Field(None, description='权限标识')
|
perms: str | None = Field(None, description='权限标识')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(description='状态')
|
||||||
display: StatusType = Field(StatusType.enable, description='是否显示')
|
display: StatusType = Field(description='是否显示')
|
||||||
cache: StatusType = Field(StatusType.enable, description='是否缓存')
|
cache: StatusType = Field(description='是否缓存')
|
||||||
link: str | None = Field(None, description='外链地址')
|
link: str | None = Field(None, description='外链地址')
|
||||||
remark: str | None = Field(None, description='备注')
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
@@ -43,3 +41,9 @@ class GetMenuDetail(MenuSchemaBase):
|
|||||||
id: int = Field(description='菜单 ID')
|
id: int = Field(description='菜单 ID')
|
||||||
created_time: datetime = Field(description='创建时间')
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = Field(None, 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 datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -21,12 +19,12 @@ class OperaLogSchemaBase(SchemaBase):
|
|||||||
country: str | None = Field(None, description='国家')
|
country: str | None = Field(None, description='国家')
|
||||||
region: str | None = Field(None, description='地区')
|
region: str | None = Field(None, description='地区')
|
||||||
city: str | None = Field(None, description='城市')
|
city: str | None = Field(None, description='城市')
|
||||||
user_agent: str = Field(description='用户代理')
|
user_agent: str | None = Field(description='用户代理')
|
||||||
os: str | None = Field(None, description='操作系统')
|
os: str | None = Field(None, description='操作系统')
|
||||||
browser: str | None = Field(None, description='浏览器')
|
browser: str | None = Field(None, description='浏览器')
|
||||||
device: str | None = Field(None, description='设备')
|
device: str | None = Field(None, description='设备')
|
||||||
args: dict[str, Any] | None = Field(None, description='请求参数')
|
args: dict[str, Any] | None = Field(None, description='请求参数')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(description='状态')
|
||||||
code: str = Field(description='状态码')
|
code: str = Field(description='状态码')
|
||||||
msg: str | None = Field(None, description='消息')
|
msg: str | None = Field(None, description='消息')
|
||||||
cost_time: float = Field(description='耗时')
|
cost_time: float = Field(description='耗时')
|
||||||
@@ -41,6 +39,12 @@ class UpdateOperaLogParam(OperaLogSchemaBase):
|
|||||||
"""更新操作日志参数"""
|
"""更新操作日志参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteOperaLogParam(SchemaBase):
|
||||||
|
"""删除操作日志参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='操作日志 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetOperaLogDetail(OperaLogSchemaBase):
|
class GetOperaLogDetail(OperaLogSchemaBase):
|
||||||
"""操作日志详情"""
|
"""操作日志详情"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
from backend.app.admin.schema.data_scope import GetDataScopeDetail
|
from backend.app.admin.schema.data_scope import GetDataScopeWithRelationDetail
|
||||||
from backend.app.admin.schema.menu import GetMenuDetail
|
from backend.app.admin.schema.menu import GetMenuDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
@@ -14,7 +12,8 @@ class RoleSchemaBase(SchemaBase):
|
|||||||
"""角色基础模型"""
|
"""角色基础模型"""
|
||||||
|
|
||||||
name: str = Field(description='角色名称')
|
name: str = Field(description='角色名称')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(description='状态')
|
||||||
|
is_filter_scopes: bool = Field(True, description='过滤数据权限')
|
||||||
remark: str | None = Field(None, description='备注')
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
@@ -26,12 +25,32 @@ class UpdateRoleParam(RoleSchemaBase):
|
|||||||
"""更新角色参数"""
|
"""更新角色参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRoleParam(SchemaBase):
|
||||||
|
"""删除角色参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRoleMenuParam(SchemaBase):
|
||||||
|
"""创建角色菜单参数"""
|
||||||
|
|
||||||
|
role_id: int = Field(description='角色 ID')
|
||||||
|
menu_id: int = Field(description='菜单 ID')
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleMenuParam(SchemaBase):
|
class UpdateRoleMenuParam(SchemaBase):
|
||||||
"""更新角色菜单参数"""
|
"""更新角色菜单参数"""
|
||||||
|
|
||||||
menus: list[int] = Field(description='菜单 ID 列表')
|
menus: list[int] = Field(description='菜单 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRoleScopeParam(SchemaBase):
|
||||||
|
"""创建角色数据范围参数"""
|
||||||
|
|
||||||
|
role_id: int = Field(description='角色 ID')
|
||||||
|
data_scope_id: int = Field(description='数据范围 ID')
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleScopeParam(SchemaBase):
|
class UpdateRoleScopeParam(SchemaBase):
|
||||||
"""更新角色数据范围参数"""
|
"""更新角色数据范围参数"""
|
||||||
|
|
||||||
@@ -52,4 +71,4 @@ class GetRoleWithRelationDetail(GetRoleDetail):
|
|||||||
"""角色关联详情"""
|
"""角色关联详情"""
|
||||||
|
|
||||||
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
|
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
|
||||||
scopes: list[GetDataScopeDetail | None] = Field([], description='数据范围列表')
|
scopes: list[GetDataScopeWithRelationDetail | None] = Field([], description='数据范围列表')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
@@ -32,6 +30,7 @@ class GetNewToken(AccessTokenBase):
|
|||||||
class GetLoginToken(AccessTokenBase):
|
class GetLoginToken(AccessTokenBase):
|
||||||
"""获取登录令牌"""
|
"""获取登录令牌"""
|
||||||
|
|
||||||
|
password_expire_days_remaining: int | None = Field(None, description='密码过期剩余天数')
|
||||||
user: GetUserInfoDetail = Field(description='用户信息')
|
user: GetUserInfoDetail = Field(description='用户信息')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,53 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
from pydantic import ConfigDict, Field, HttpUrl, PlainSerializer, model_validator
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from backend.app.admin.schema.dept import GetDeptDetail
|
from backend.app.admin.schema.dept import GetDeptDetail
|
||||||
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.schema import CustomPhoneNumber, SchemaBase
|
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase, ser_string
|
||||||
|
|
||||||
|
|
||||||
class AuthSchemaBase(SchemaBase):
|
class AuthSchemaBase(SchemaBase):
|
||||||
"""用户认证基础模型"""
|
"""用户认证基础模型"""
|
||||||
|
|
||||||
username: str = Field(description='用户名')
|
username: str = Field(description='用户名')
|
||||||
password: str | None = Field(description='密码')
|
password: str = Field(description='密码')
|
||||||
|
|
||||||
|
|
||||||
class AuthLoginParam(AuthSchemaBase):
|
class AuthLoginParam(AuthSchemaBase):
|
||||||
"""用户登录参数"""
|
"""用户登录参数"""
|
||||||
|
|
||||||
captcha: str = Field(description='验证码')
|
uuid: str | None = Field(None, description='验证码 UUID')
|
||||||
|
captcha: str | None = Field(None, description='验证码')
|
||||||
|
|
||||||
class RegisterUserParam(AuthSchemaBase):
|
|
||||||
"""用户注册参数"""
|
|
||||||
|
|
||||||
nickname: str | None = Field(None, description='昵称')
|
|
||||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
|
||||||
|
|
||||||
|
|
||||||
class AddUserParam(AuthSchemaBase):
|
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')
|
dept_id: int = Field(description='部门 ID')
|
||||||
roles: list[int] = Field(description='角色 ID 列表')
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class AddUserRoleParam(SchemaBase):
|
||||||
|
"""添加用户角色"""
|
||||||
|
|
||||||
|
user_id: int = Field(description='用户 ID')
|
||||||
|
role_id: int = Field(description='角色 ID')
|
||||||
|
|
||||||
|
|
||||||
|
class AddOAuth2UserParam(AuthSchemaBase):
|
||||||
|
"""添加 OAuth2 用户参数"""
|
||||||
|
|
||||||
|
password: str | None = Field(None, description='密码')
|
||||||
nickname: str | None = Field(None, description='昵称')
|
nickname: str | None = Field(None, description='昵称')
|
||||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
avatar: Annotated[HttpUrl, PlainSerializer(ser_string)] | None = Field(None, description='头像地址')
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordParam(SchemaBase):
|
class ResetPasswordParam(SchemaBase):
|
||||||
@@ -55,8 +64,8 @@ class UserInfoSchemaBase(SchemaBase):
|
|||||||
dept_id: int | None = Field(None, description='部门 ID')
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
username: str = Field(description='用户名')
|
username: str = Field(description='用户名')
|
||||||
nickname: str = Field(description='昵称')
|
nickname: str = Field(description='昵称')
|
||||||
avatar: HttpUrl | None = Field(None, description='头像')
|
avatar: Annotated[HttpUrl, PlainSerializer(ser_string)] | None = Field(None, description='头像地址')
|
||||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||||
|
|
||||||
|
|
||||||
@@ -74,8 +83,7 @@ class GetUserInfoDetail(UserInfoSchemaBase):
|
|||||||
dept_id: int | None = Field(None, description='部门 ID')
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
id: int = Field(description='用户 ID')
|
id: int = Field(description='用户 ID')
|
||||||
uuid: str = Field(description='用户 UUID')
|
uuid: str = Field(description='用户 UUID')
|
||||||
avatar: str | None = Field(None, description='头像')
|
status: StatusType = Field(description='状态')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
|
||||||
is_superuser: bool = Field(description='是否超级管理员')
|
is_superuser: bool = Field(description='是否超级管理员')
|
||||||
is_staff: bool = Field(description='是否管理员')
|
is_staff: bool = Field(description='是否管理员')
|
||||||
is_multi_login: bool = Field(description='是否允许多端登录')
|
is_multi_login: bool = Field(description='是否允许多端登录')
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordHistoryBase(SchemaBase):
|
||||||
|
"""用户历史密码记录基础模型"""
|
||||||
|
|
||||||
|
user_id: int = Field(description='用户 ID')
|
||||||
|
password: str = Field(description='历史密码')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateUserPasswordHistoryParam(UserPasswordHistoryBase):
|
||||||
|
"""创建用户历史密码记录"""
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
from fastapi.security import HTTPBasicCredentials
|
from fastapi.security import HTTPBasicCredentials
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from starlette.background import BackgroundTask, BackgroundTasks
|
from starlette.background import BackgroundTask, BackgroundTasks
|
||||||
|
|
||||||
|
from backend.app.admin.crud.crud_menu import menu_dao
|
||||||
from backend.app.admin.crud.crud_user import user_dao
|
from backend.app.admin.crud.crud_user import user_dao
|
||||||
from backend.app.admin.model import User
|
from backend.app.admin.model import User
|
||||||
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
|
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
|
||||||
from backend.app.admin.schema.user import AuthLoginParam
|
from backend.app.admin.schema.user import AuthLoginParam
|
||||||
from backend.app.admin.service.login_log_service import login_log_service
|
from backend.app.admin.service.login_log_service import login_log_service
|
||||||
|
from backend.app.admin.service.user_password_history_service import password_security_service
|
||||||
|
from backend.app.admin.utils.password_security import password_verify
|
||||||
|
from backend.common.context import ctx
|
||||||
from backend.common.enums import LoginLogStatusType
|
from backend.common.enums import LoginLogStatusType
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
|
from backend.common.i18n import t
|
||||||
from backend.common.log import log
|
from backend.common.log import log
|
||||||
from backend.common.response.response_code import CustomErrorCode
|
from backend.common.response.response_code import CustomErrorCode
|
||||||
from backend.common.security.jwt import (
|
from backend.common.security.jwt import (
|
||||||
@@ -20,11 +23,11 @@ from backend.common.security.jwt import (
|
|||||||
create_refresh_token,
|
create_refresh_token,
|
||||||
get_token,
|
get_token,
|
||||||
jwt_decode,
|
jwt_decode,
|
||||||
password_verify,
|
|
||||||
)
|
)
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session, uuid4_str
|
from backend.database.db import uuid4_str
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
|
from backend.utils.dynamic_config import load_login_config
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ class AuthService:
|
|||||||
"""认证服务类"""
|
"""认证服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def user_verify(db: AsyncSession, username: str, password: str | None) -> User:
|
async def user_verify(db: AsyncSession, username: str, password: str) -> tuple[User, int | None]:
|
||||||
"""
|
"""
|
||||||
验证用户名和密码
|
验证用户名和密码
|
||||||
|
|
||||||
@@ -45,162 +48,198 @@ class AuthService:
|
|||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户名或密码有误')
|
raise errors.NotFoundError(msg='用户名或密码有误')
|
||||||
|
|
||||||
if user.password is None:
|
await password_security_service.check_status(user.id, user.status)
|
||||||
|
|
||||||
|
if user.password is None or not password_verify(password, user.password):
|
||||||
|
await password_security_service.handle_login_failure(db, user.id)
|
||||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||||
else:
|
|
||||||
if not password_verify(password, user.password):
|
|
||||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
|
||||||
|
|
||||||
if not user.status:
|
days_remaining = await password_security_service.check_password_expiry_status(
|
||||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
db, user.last_password_changed_time
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
await password_security_service.handle_login_success(user.id)
|
||||||
|
|
||||||
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
return user, days_remaining
|
||||||
|
|
||||||
|
async def swagger_login(self, *, db: AsyncSession, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
||||||
"""
|
"""
|
||||||
Swagger 文档登录
|
Swagger 文档登录
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 登录凭证
|
:param obj: 登录凭证
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
user, _ = await self.user_verify(db, obj.username, obj.password)
|
||||||
user = await self.user_verify(db, obj.username, obj.password)
|
await user_dao.update_login_time(db, obj.username)
|
||||||
await user_dao.update_login_time(db, obj.username)
|
access_token_data = await create_access_token(
|
||||||
a_token = await create_access_token(
|
user.id,
|
||||||
str(user.id),
|
multi_login=user.is_multi_login,
|
||||||
user.is_multi_login,
|
# extra info
|
||||||
# extra info
|
swagger=True,
|
||||||
swagger=True,
|
)
|
||||||
)
|
return access_token_data.access_token, user
|
||||||
return a_token.access_token, user
|
|
||||||
|
|
||||||
async def login(
|
async def login(
|
||||||
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
|
self,
|
||||||
|
*,
|
||||||
|
db: AsyncSession,
|
||||||
|
response: Response,
|
||||||
|
obj: AuthLoginParam,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
) -> GetLoginToken:
|
) -> GetLoginToken:
|
||||||
"""
|
"""
|
||||||
用户登录
|
用户登录
|
||||||
|
|
||||||
:param request: 请求对象
|
:param db: 数据库会话
|
||||||
:param response: 响应对象
|
:param response: 响应对象
|
||||||
:param obj: 登录参数
|
:param obj: 登录参数
|
||||||
:param background_tasks: 后台任务
|
:param background_tasks: 后台任务
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
user = None
|
||||||
user = None
|
try:
|
||||||
try:
|
user, days_remaining = await self.user_verify(db, obj.username, obj.password)
|
||||||
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}')
|
await load_login_config(db)
|
||||||
|
if settings.LOGIN_CAPTCHA_ENABLED:
|
||||||
|
if not obj.uuid or not obj.captcha:
|
||||||
|
raise errors.RequestError(msg=t('error.captcha.invalid'))
|
||||||
|
captcha_code = await redis_client.get(f'{settings.LOGIN_CAPTCHA_REDIS_PREFIX}:{obj.uuid}')
|
||||||
if not captcha_code:
|
if not captcha_code:
|
||||||
raise errors.ForbiddenError(msg='验证码失效,请重新获取')
|
raise errors.RequestError(msg=t('error.captcha.expired'))
|
||||||
if captcha_code.lower() != obj.captcha.lower():
|
if captcha_code.lower() != obj.captcha.lower():
|
||||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
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.LOGIN_CAPTCHA_REDIS_PREFIX}:{obj.uuid}')
|
||||||
await user_dao.update_login_time(db, obj.username)
|
|
||||||
await db.refresh(user)
|
await user_dao.update_login_time(db, obj.username)
|
||||||
a_token = await create_access_token(
|
await db.refresh(user)
|
||||||
str(user.id),
|
access_token_data = await create_access_token(
|
||||||
user.is_multi_login,
|
user.id,
|
||||||
# extra info
|
multi_login=user.is_multi_login,
|
||||||
username=user.username,
|
# extra info
|
||||||
nickname=user.nickname,
|
username=user.username,
|
||||||
last_login_time=timezone.t_str(user.last_login_time),
|
nickname=user.nickname,
|
||||||
ip=request.state.ip,
|
last_login_time=timezone.to_str(user.last_login_time),
|
||||||
os=request.state.os,
|
ip=ctx.ip,
|
||||||
browser=request.state.browser,
|
os=ctx.os,
|
||||||
device=request.state.device,
|
browser=ctx.browser,
|
||||||
)
|
device=ctx.device,
|
||||||
r_token = await create_refresh_token(str(user.id), user.is_multi_login)
|
)
|
||||||
response.set_cookie(
|
refresh_token_data = await create_refresh_token(
|
||||||
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
access_token_data.session_uuid,
|
||||||
value=r_token.refresh_token,
|
user.id,
|
||||||
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
multi_login=user.is_multi_login,
|
||||||
expires=timezone.f_utc(r_token.refresh_token_expire_time),
|
)
|
||||||
httponly=True,
|
response.set_cookie(
|
||||||
)
|
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
||||||
except errors.NotFoundError as e:
|
value=refresh_token_data.refresh_token,
|
||||||
log.error('登陆错误: 用户名不存在')
|
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
raise errors.NotFoundError(msg=e.msg)
|
expires=timezone.to_utc(refresh_token_data.refresh_token_expire_time),
|
||||||
except (errors.ForbiddenError, errors.CustomError) as e:
|
httponly=True,
|
||||||
if not user:
|
)
|
||||||
log.error('登陆错误: 用户密码有误')
|
except errors.NotFoundError as e:
|
||||||
task = BackgroundTask(
|
log.error('登陆错误: 用户名不存在')
|
||||||
login_log_service.create,
|
raise errors.NotFoundError(msg=e.msg)
|
||||||
**dict(
|
except (errors.RequestError, errors.CustomError) as e:
|
||||||
db=db,
|
if not user:
|
||||||
request=request,
|
log.error('登陆错误: 用户密码有误')
|
||||||
user_uuid=user.uuid if user else uuid4_str(),
|
task = BackgroundTask(
|
||||||
username=obj.username,
|
login_log_service.create,
|
||||||
login_time=timezone.now(),
|
user_uuid=user.uuid if user else uuid4_str(),
|
||||||
status=LoginLogStatusType.fail.value,
|
username=obj.username,
|
||||||
msg=e.msg,
|
login_time=timezone.now(),
|
||||||
),
|
status=LoginLogStatusType.fail.value,
|
||||||
)
|
msg=e.msg,
|
||||||
raise errors.RequestError(msg=e.msg, background=task)
|
)
|
||||||
except Exception as e:
|
raise errors.RequestError(code=e.code, msg=e.msg, background=task)
|
||||||
log.error(f'登陆错误: {e}')
|
except Exception as e:
|
||||||
raise e
|
log.error(f'登陆错误: {e}')
|
||||||
else:
|
raise
|
||||||
background_tasks.add_task(
|
else:
|
||||||
login_log_service.create,
|
background_tasks.add_task(
|
||||||
**dict(
|
login_log_service.create,
|
||||||
db=db,
|
user_uuid=user.uuid,
|
||||||
request=request,
|
username=obj.username,
|
||||||
user_uuid=user.uuid,
|
login_time=timezone.now(),
|
||||||
username=obj.username,
|
status=LoginLogStatusType.success.value,
|
||||||
login_time=timezone.now(),
|
msg=t('success.login.success'),
|
||||||
status=LoginLogStatusType.success.value,
|
)
|
||||||
msg='登录成功',
|
data = GetLoginToken(
|
||||||
),
|
access_token=access_token_data.access_token,
|
||||||
)
|
access_token_expire_time=access_token_data.access_token_expire_time,
|
||||||
data = GetLoginToken(
|
session_uuid=access_token_data.session_uuid,
|
||||||
access_token=a_token.access_token,
|
password_expire_days_remaining=days_remaining,
|
||||||
access_token_expire_time=a_token.access_token_expire_time,
|
user=user, # type: ignore
|
||||||
session_uuid=a_token.session_uuid,
|
)
|
||||||
user=user, # type: ignore
|
return data
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def new_token(*, request: Request) -> GetNewToken:
|
async def get_codes(*, db: AsyncSession, request: Request) -> list[str]:
|
||||||
"""
|
"""
|
||||||
获取新的访问令牌
|
获取用户权限码
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
codes = set()
|
||||||
|
if request.user.is_superuser:
|
||||||
|
menus = await menu_dao.get_all(db, None, None)
|
||||||
|
for menu in menus:
|
||||||
|
if menu.perms:
|
||||||
|
codes.add(*menu.perms.split(','))
|
||||||
|
else:
|
||||||
|
roles = request.user.roles
|
||||||
|
if roles:
|
||||||
|
for role in roles:
|
||||||
|
for menu in role.menus:
|
||||||
|
if menu.perms:
|
||||||
|
codes.add(*menu.perms.split(','))
|
||||||
|
|
||||||
|
return list(codes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def refresh_token(*, db: AsyncSession, request: Request) -> GetNewToken:
|
||||||
|
"""
|
||||||
|
刷新令牌
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param request: FastAPI 请求对象
|
:param request: FastAPI 请求对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
if not refresh_token:
|
if not refresh_token:
|
||||||
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
|
raise errors.RequestError(msg='Refresh Token 已过期,请重新登录')
|
||||||
try:
|
token_payload = jwt_decode(refresh_token)
|
||||||
user_id = jwt_decode(refresh_token).id
|
|
||||||
except Exception:
|
user = await user_dao.get(db, token_payload.id)
|
||||||
raise errors.TokenError(msg='Refresh Token 无效')
|
if not user:
|
||||||
async with async_db_session() as db:
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
user = await user_dao.get(db, user_id)
|
if not user.status:
|
||||||
if not user:
|
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||||
raise errors.NotFoundError(msg='用户名或密码有误')
|
if not user.is_multi_login and await redis_client.get_prefix(f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
|
||||||
elif not user.status:
|
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
|
||||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
new_token = await create_new_token(
|
||||||
new_token = await create_new_token(
|
refresh_token,
|
||||||
user_id=str(user.id),
|
token_payload.session_uuid,
|
||||||
refresh_token=refresh_token,
|
user.id,
|
||||||
multi_login=user.is_multi_login,
|
multi_login=user.is_multi_login,
|
||||||
# extra info
|
# extra info
|
||||||
username=user.username,
|
username=user.username,
|
||||||
nickname=user.nickname,
|
nickname=user.nickname,
|
||||||
last_login_time=timezone.t_str(user.last_login_time),
|
last_login_time=timezone.to_str(user.last_login_time),
|
||||||
ip=request.state.ip,
|
ip=ctx.ip,
|
||||||
os=request.state.os,
|
os=ctx.os,
|
||||||
browser=request.state.browser,
|
browser=ctx.browser,
|
||||||
device_type=request.state.device,
|
device_type=ctx.device,
|
||||||
)
|
)
|
||||||
data = GetNewToken(
|
data = GetNewToken(
|
||||||
access_token=new_token.new_access_token,
|
access_token=new_token.new_access_token,
|
||||||
access_token_expire_time=new_token.new_access_token_expire_time,
|
access_token_expire_time=new_token.new_access_token_expire_time,
|
||||||
session_uuid=new_token.session_uuid,
|
session_uuid=new_token.session_uuid,
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def logout(*, request: Request, response: Response) -> None:
|
async def logout(*, request: Request, response: Response) -> None:
|
||||||
@@ -215,24 +254,17 @@ class AuthService:
|
|||||||
token = get_token(request)
|
token = get_token(request)
|
||||||
token_payload = jwt_decode(token)
|
token_payload = jwt_decode(token)
|
||||||
user_id = token_payload.id
|
user_id = token_payload.id
|
||||||
|
session_uuid = token_payload.session_uuid
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
except errors.TokenError:
|
except errors.TokenError:
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
|
||||||
# 清理缓存
|
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||||
if request.user.is_multi_login:
|
await redis_client.delete(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
|
if refresh_token:
|
||||||
if refresh_token:
|
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
|
||||||
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
|
|
||||||
else:
|
|
||||||
key_prefix = [
|
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:',
|
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:',
|
|
||||||
]
|
|
||||||
for prefix in key_prefix:
|
|
||||||
await redis_client.delete_prefix(prefix)
|
|
||||||
|
|
||||||
|
|
||||||
auth_service: AuthService = AuthService()
|
auth_service: AuthService = AuthService()
|
||||||
|
|||||||
@@ -1,39 +1,47 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Table
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_data_rule import data_rule_dao
|
from backend.app.admin.crud.crud_data_rule import data_rule_dao
|
||||||
from backend.app.admin.model import DataRule
|
from backend.app.admin.model import DataRule
|
||||||
from backend.app.admin.schema.data_rule import CreateDataRuleParam, GetDataRuleColumnDetail, UpdateDataRuleParam
|
from backend.app.admin.schema.data_rule import (
|
||||||
|
CreateDataRuleParam,
|
||||||
|
DeleteDataRuleParam,
|
||||||
|
GetDataRuleColumnDetail,
|
||||||
|
UpdateDataRuleParam,
|
||||||
|
)
|
||||||
|
from backend.app.admin.utils.cache import user_cache_manager
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
|
from backend.common.pagination import paging_data
|
||||||
|
from backend.common.security.permission import get_data_permission_models
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.utils.import_parse import dynamic_import_data_model
|
|
||||||
|
|
||||||
|
|
||||||
class DataRuleService:
|
class DataRuleService:
|
||||||
"""数据规则服务类"""
|
"""数据规则服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> DataRule:
|
async def get(*, db: AsyncSession, pk: int) -> DataRule:
|
||||||
"""
|
"""
|
||||||
获取数据规则详情
|
获取数据规则详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 规则 ID
|
:param pk: 规则 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
data_rule = await data_rule_dao.get(db, pk)
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
if not data_rule:
|
if not data_rule:
|
||||||
raise errors.NotFoundError(msg='数据规则不存在')
|
raise errors.NotFoundError(msg='数据规则不存在')
|
||||||
return data_rule
|
return data_rule
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_models() -> list[str]:
|
async def get_models() -> list[str]:
|
||||||
"""获取所有数据规则可用模型"""
|
"""获取所有数据规则可用模型"""
|
||||||
return list(settings.DATA_PERMISSION_MODELS.keys())
|
model_exclude = ['DataScope', 'DataRule', 'sys_role_data_scope', 'sys_data_scope_rule']
|
||||||
|
return [m for m in list(get_data_permission_models().keys()) if m not in model_exclude]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
|
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
|
||||||
@@ -43,78 +51,88 @@ class DataRuleService:
|
|||||||
:param model: 模型名称
|
:param model: 模型名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if model not in settings.DATA_PERMISSION_MODELS:
|
available_models = get_data_permission_models()
|
||||||
|
if model not in available_models:
|
||||||
raise errors.NotFoundError(msg='数据规则可用模型不存在')
|
raise errors.NotFoundError(msg='数据规则可用模型不存在')
|
||||||
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
|
model_ins = available_models[model]
|
||||||
|
|
||||||
|
table = model_ins if isinstance(model_ins, Table) else model_ins.__table__
|
||||||
model_columns = [
|
model_columns = [
|
||||||
GetDataRuleColumnDetail(key=column.key, comment=column.comment)
|
GetDataRuleColumnDetail(key=column.key, comment=column.comment)
|
||||||
for column in model_ins.__table__.columns
|
for column in table.columns
|
||||||
if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
|
if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
|
||||||
]
|
]
|
||||||
return model_columns
|
return model_columns
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str | None) -> Select:
|
async def get_list(*, db: AsyncSession, name: str | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取数据规则列表查询条件
|
获取数据规则列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param name: 规则名称
|
:param name: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await data_rule_dao.get_list(name=name)
|
data_rule_select = await data_rule_dao.get_select(name=name)
|
||||||
|
return await paging_data(db, data_rule_select)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> Sequence[DataRule]:
|
async def get_all(*, db: AsyncSession) -> Sequence[DataRule]:
|
||||||
"""获取所有数据规则"""
|
"""
|
||||||
async with async_db_session() as db:
|
获取所有数据规则
|
||||||
data_rules = await data_rule_dao.get_all(db)
|
|
||||||
return data_rules
|
:param db: 数据库会话
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_rules = await data_rule_dao.get_all(db)
|
||||||
|
return data_rules
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDataRuleParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateDataRuleParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建数据规则
|
创建数据规则
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 规则创建参数
|
:param obj: 规则创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
data_rule = await data_rule_dao.get_by_name(db, obj.name)
|
||||||
data_rule = await data_rule_dao.get_by_name(db, obj.name)
|
if data_rule:
|
||||||
if data_rule:
|
raise errors.ConflictError(msg='数据规则已存在')
|
||||||
raise errors.ForbiddenError(msg='数据规则已存在')
|
await data_rule_dao.create(db, obj)
|
||||||
await data_rule_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据规则
|
更新数据规则
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 规则 ID
|
:param pk: 规则 ID
|
||||||
:param obj: 规则更新参数
|
:param obj: 规则更新参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
data_rule = await data_rule_dao.get(db, pk)
|
if not data_rule:
|
||||||
if not data_rule:
|
raise errors.NotFoundError(msg='数据规则不存在')
|
||||||
raise errors.NotFoundError(msg='数据规则不存在')
|
if data_rule.name != obj.name and await data_rule_dao.get_by_name(db, obj.name):
|
||||||
if data_rule.name != obj.name:
|
raise errors.ConflictError(msg='数据规则已存在')
|
||||||
if await data_rule_dao.get_by_name(db, obj.name):
|
count = await data_rule_dao.update(db, pk, obj)
|
||||||
raise errors.ForbiddenError(msg='数据规则已存在')
|
await user_cache_manager.clear_by_data_rule_id(db, [pk])
|
||||||
count = await data_rule_dao.update(db, pk, obj)
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, db: AsyncSession, obj: DeleteDataRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
删除数据规则
|
批量删除数据规则
|
||||||
|
|
||||||
:param pk: 规则 ID 列表
|
:param db: 数据库会话
|
||||||
|
:param obj: 规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
count = await data_rule_dao.delete(db, obj.pks)
|
||||||
count = await data_rule_dao.delete(db, pk)
|
await user_cache_manager.clear_by_data_rule_id(db, obj.pks)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
data_rule_service: DataRuleService = DataRuleService()
|
data_rule_service: DataRuleService = DataRuleService()
|
||||||
|
|||||||
@@ -1,124 +1,138 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from sqlalchemy import Select
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
||||||
from backend.app.admin.model import DataScope
|
from backend.app.admin.model import DataScope
|
||||||
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
|
from backend.app.admin.schema.data_scope import (
|
||||||
|
CreateDataScopeParam,
|
||||||
|
DeleteDataScopeParam,
|
||||||
|
UpdateDataScopeParam,
|
||||||
|
UpdateDataScopeRuleParam,
|
||||||
|
)
|
||||||
|
from backend.app.admin.utils.cache import user_cache_manager
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.core.conf import settings
|
from backend.common.pagination import paging_data
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.database.redis import redis_client
|
|
||||||
|
|
||||||
|
|
||||||
class DataScopeService:
|
class DataScopeService:
|
||||||
"""数据范围服务类"""
|
"""数据范围服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> DataScope:
|
async def get(*, db: AsyncSession, pk: int) -> DataScope:
|
||||||
"""
|
"""
|
||||||
获取数据范围详情
|
获取数据范围详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 范围 ID
|
:param pk: 范围 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
data_scope = await data_scope_dao.get(db, pk)
|
data_scope = await data_scope_dao.get(db, pk)
|
||||||
if not data_scope:
|
if not data_scope:
|
||||||
raise errors.NotFoundError(msg='数据范围不存在')
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
return data_scope
|
return data_scope
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_rules(*, pk: int) -> DataScope:
|
async def get_all(*, db: AsyncSession) -> Sequence[DataScope]:
|
||||||
|
"""
|
||||||
|
获取所有数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_scopes = await data_scope_dao.get_all(db)
|
||||||
|
return data_scopes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_rules(*, db: AsyncSession, pk: int) -> DataScope:
|
||||||
"""
|
"""
|
||||||
获取数据范围规则
|
获取数据范围规则
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 范围 ID
|
:param pk: 范围 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
data_scope = await data_scope_dao.get_with_relation(db, pk)
|
data_scope = await data_scope_dao.get_join(db, pk)
|
||||||
if not data_scope:
|
if not data_scope:
|
||||||
raise errors.NotFoundError(msg='数据范围不存在')
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
return data_scope
|
return data_scope
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str | None, status: int | None) -> Select:
|
async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取数据范围列表查询条件
|
获取数据范围列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param name: 范围名称
|
:param name: 范围名称
|
||||||
:param status: 范围状态
|
:param status: 范围状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await data_scope_dao.get_list(name, status)
|
data_scope_select = await data_scope_dao.get_select(name, status)
|
||||||
|
return await paging_data(db, data_scope_select)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDataScopeParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建数据范围
|
创建数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 数据范围参数
|
:param obj: 数据范围参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
data_scope = await data_scope_dao.get_by_name(db, obj.name)
|
||||||
data_scope = await data_scope_dao.get_by_name(db, obj.name)
|
if data_scope:
|
||||||
if data_scope:
|
raise errors.ConflictError(msg='数据范围已存在')
|
||||||
raise errors.ForbiddenError(msg='数据范围已存在')
|
await data_scope_dao.create(db, obj)
|
||||||
await data_scope_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDataScopeParam) -> int:
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据范围
|
更新数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 范围 ID
|
:param pk: 范围 ID
|
||||||
:param obj: 数据范围更新参数
|
:param obj: 数据范围更新参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
data_scope = await data_scope_dao.get(db, pk)
|
||||||
data_scope = await data_scope_dao.get(db, pk)
|
if not data_scope:
|
||||||
if not data_scope:
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
raise errors.NotFoundError(msg='数据范围不存在')
|
if data_scope.name != obj.name and await data_scope_dao.get_by_name(db, obj.name):
|
||||||
if data_scope.name != obj.name:
|
raise errors.ConflictError(msg='数据范围已存在')
|
||||||
if await data_scope_dao.get_by_name(db, obj.name):
|
count = await data_scope_dao.update(db, pk, obj)
|
||||||
raise errors.ForbiddenError(msg='数据范围已存在')
|
await user_cache_manager.clear_by_data_scope_id(db, [pk])
|
||||||
count = await data_scope_dao.update(db, pk, obj)
|
return count
|
||||||
for role in await data_scope.awaitable_attrs.roles:
|
|
||||||
for user in await role.awaitable_attrs.users:
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_data_scope_rule(*, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
|
async def update_data_scope_rule(*, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据范围规则
|
更新数据范围规则
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 范围 ID
|
:param pk: 范围 ID
|
||||||
:param rule_ids: 规则 ID 列表
|
:param rule_ids: 规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
count = await data_scope_dao.update_rules(db, pk, rule_ids)
|
||||||
count = await data_scope_dao.update_rules(db, pk, rule_ids)
|
await user_cache_manager.clear_by_data_scope_id(db, [pk])
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, db: AsyncSession, obj: DeleteDataScopeParam) -> int:
|
||||||
"""
|
"""
|
||||||
删除数据范围
|
批量删除数据范围
|
||||||
|
|
||||||
:param pk: 范围 ID 列表
|
:param db: 数据库会话
|
||||||
|
:param obj: 范围 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
count = await data_scope_dao.delete(db, obj.pks)
|
||||||
count = await data_scope_dao.delete(db, pk)
|
await user_cache_manager.clear_by_data_scope_id(db, obj.pks)
|
||||||
for _pk in pk:
|
return count
|
||||||
data_rule = await data_scope_dao.get(db, _pk)
|
|
||||||
if data_rule:
|
|
||||||
for role in await data_rule.awaitable_attrs.roles:
|
|
||||||
for user in await role.awaitable_attrs.users:
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
data_scope_service: DataScopeService = DataScopeService()
|
data_scope_service: DataScopeService = DataScopeService()
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Request
|
from sqlalchemy import ColumnElement
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_dept import dept_dao
|
from backend.app.admin.crud.crud_dept import dept_dao
|
||||||
from backend.app.admin.model import Dept
|
from backend.app.admin.model import Dept
|
||||||
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
from backend.utils.build_tree import get_tree_data
|
from backend.utils.build_tree import get_tree_data
|
||||||
|
|
||||||
@@ -18,100 +16,108 @@ class DeptService:
|
|||||||
"""部门服务类"""
|
"""部门服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> Dept:
|
async def get(*, db: AsyncSession, pk: int) -> Dept:
|
||||||
"""
|
"""
|
||||||
获取部门详情
|
获取部门详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 部门 ID
|
:param pk: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
dept = await dept_dao.get(db, pk)
|
dept = await dept_dao.get(db, pk)
|
||||||
if not dept:
|
if not dept:
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
return dept
|
return dept
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_dept_tree(
|
async def get_tree(
|
||||||
*, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
|
*,
|
||||||
|
db: AsyncSession,
|
||||||
|
data_filter: ColumnElement[bool],
|
||||||
|
name: str | None,
|
||||||
|
leader: str | None,
|
||||||
|
phone: str | None,
|
||||||
|
status: int | None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取部门树形结构
|
获取部门树形结构
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param db: 数据库会话
|
||||||
|
:param data_filter: 请求用户
|
||||||
:param name: 部门名称
|
:param name: 部门名称
|
||||||
:param leader: 部门负责人
|
:param leader: 部门负责人
|
||||||
:param phone: 联系电话
|
:param phone: 联系电话
|
||||||
:param status: 状态
|
:param status: 状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
dept_select = await dept_dao.get_all(db, data_filter, name, leader, phone, status)
|
||||||
dept_select = await dept_dao.get_all(request, db, name, leader, phone, status)
|
tree_data = get_tree_data(dept_select)
|
||||||
tree_data = get_tree_data(dept_select)
|
return tree_data
|
||||||
return tree_data
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDeptParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateDeptParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建部门
|
创建部门
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 部门创建参数
|
:param obj: 部门创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
dept = await dept_dao.get_by_name(db, obj.name)
|
||||||
dept = await dept_dao.get_by_name(db, obj.name)
|
if dept:
|
||||||
if dept:
|
raise errors.ConflictError(msg='部门名称已存在')
|
||||||
raise errors.ForbiddenError(msg='部门名称已存在')
|
if obj.parent_id is not None:
|
||||||
if obj.parent_id:
|
parent_dept = await dept_dao.get(db, obj.parent_id)
|
||||||
parent_dept = await dept_dao.get(db, obj.parent_id)
|
if not parent_dept:
|
||||||
if not parent_dept:
|
raise errors.NotFoundError(msg='父级部门不存在')
|
||||||
raise errors.NotFoundError(msg='父级部门不存在')
|
await dept_dao.create(db, obj)
|
||||||
await dept_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDeptParam) -> int:
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateDeptParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新部门
|
更新部门
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 部门 ID
|
:param pk: 部门 ID
|
||||||
:param obj: 部门更新参数
|
:param obj: 部门更新参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
dept = await dept_dao.get(db, pk)
|
||||||
dept = await dept_dao.get(db, pk)
|
if not dept:
|
||||||
if not dept:
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
if dept.name != obj.name and await dept_dao.get_by_name(db, obj.name):
|
||||||
if dept.name != obj.name:
|
raise errors.ConflictError(msg='部门名称已存在')
|
||||||
if await dept_dao.get_by_name(db, obj.name):
|
if obj.parent_id:
|
||||||
raise errors.ForbiddenError(msg='部门名称已存在')
|
parent_dept = await dept_dao.get(db, obj.parent_id)
|
||||||
if obj.parent_id:
|
if not parent_dept:
|
||||||
parent_dept = await dept_dao.get(db, obj.parent_id)
|
raise errors.NotFoundError(msg='父级部门不存在')
|
||||||
if not parent_dept:
|
if obj.parent_id == dept.id:
|
||||||
raise errors.NotFoundError(msg='父级部门不存在')
|
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
||||||
if obj.parent_id == dept.id:
|
count = await dept_dao.update(db, pk, obj)
|
||||||
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
return count
|
||||||
count = await dept_dao.update(db, pk, obj)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: int) -> int:
|
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除部门
|
删除部门
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 部门 ID
|
:param pk: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
dept = await dept_dao.get_join(db, pk)
|
||||||
dept = await dept_dao.get_with_relation(db, pk)
|
if not dept:
|
||||||
if dept.users:
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
|
if dept.users:
|
||||||
children = await dept_dao.get_children(db, pk)
|
raise errors.ConflictError(msg='部门下存在用户,无法删除')
|
||||||
if children:
|
children = await dept_dao.get_children(db, pk)
|
||||||
raise errors.ForbiddenError(msg='部门下存在子部门,无法删除')
|
if children:
|
||||||
count = await dept_dao.delete(db, pk)
|
raise errors.ConflictError(msg='部门下存在子部门,无法删除')
|
||||||
for user in dept.users:
|
count = await dept_dao.delete(db, pk)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
for user in dept.users:
|
||||||
return count
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
dept_service: DeptService = DeptService()
|
dept_service: DeptService = DeptService()
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Request
|
|
||||||
from sqlalchemy import Select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_login_log import login_log_dao
|
from backend.app.admin.crud.crud_login_log import login_log_dao
|
||||||
from backend.app.admin.schema.login_log import CreateLoginLogParam
|
from backend.app.admin.schema.login_log import CreateLoginLogParam, DeleteLoginLogParam
|
||||||
|
from backend.common.context import ctx
|
||||||
from backend.common.log import log
|
from backend.common.log import log
|
||||||
|
from backend.common.pagination import paging_data
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
|
|
||||||
|
|
||||||
@@ -16,22 +15,22 @@ class LoginLogService:
|
|||||||
"""登录日志服务类"""
|
"""登录日志服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
|
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取登录日志列表查询条件
|
获取登录日志列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:param status: 状态
|
:param status: 状态
|
||||||
:param ip: IP 地址
|
:param ip: IP 地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await login_log_dao.get_list(username=username, status=status, ip=ip)
|
log_select = await login_log_dao.get_select(username=username, status=status, ip=ip)
|
||||||
|
return await paging_data(db, log_select)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(
|
async def create(
|
||||||
*,
|
*,
|
||||||
db: AsyncSession,
|
|
||||||
request: Request,
|
|
||||||
user_uuid: str,
|
user_uuid: str,
|
||||||
username: str,
|
username: str,
|
||||||
login_time: datetime,
|
login_time: datetime,
|
||||||
@@ -41,8 +40,6 @@ class LoginLogService:
|
|||||||
"""
|
"""
|
||||||
创建登录日志
|
创建登录日志
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param user_uuid: 用户 UUID
|
:param user_uuid: 用户 UUID
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:param login_time: 登录时间
|
:param login_time: 登录时间
|
||||||
@@ -55,39 +52,39 @@ class LoginLogService:
|
|||||||
user_uuid=user_uuid,
|
user_uuid=user_uuid,
|
||||||
username=username,
|
username=username,
|
||||||
status=status,
|
status=status,
|
||||||
ip=request.state.ip,
|
ip=ctx.ip,
|
||||||
country=request.state.country,
|
country=ctx.country,
|
||||||
region=request.state.region,
|
region=ctx.region,
|
||||||
city=request.state.city,
|
city=ctx.city,
|
||||||
user_agent=request.state.user_agent,
|
user_agent=ctx.user_agent,
|
||||||
browser=request.state.browser,
|
browser=ctx.browser,
|
||||||
os=request.state.os,
|
os=ctx.os,
|
||||||
device=request.state.device,
|
device=ctx.device,
|
||||||
msg=msg,
|
msg=msg,
|
||||||
login_time=login_time,
|
login_time=login_time,
|
||||||
)
|
)
|
||||||
await login_log_dao.create(db, obj)
|
# 为后台任务创建独立数据库会话
|
||||||
|
async with async_db_session.begin() as db:
|
||||||
|
await login_log_dao.create(db, obj)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f'登录日志创建失败: {e}')
|
log.error(f'登录日志创建失败: {e}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, db: AsyncSession, obj: DeleteLoginLogParam) -> int:
|
||||||
"""
|
"""
|
||||||
删除登录日志
|
批量删除登录日志
|
||||||
|
|
||||||
:param pk: 日志 ID 列表
|
:param db: 数据库会话
|
||||||
|
:param obj: 日志 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
count = await login_log_dao.delete(db, obj.pks)
|
||||||
count = await login_log_dao.delete(db, pk)
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete_all() -> int:
|
async def delete_all(*, db: AsyncSession) -> None:
|
||||||
"""清空所有登录日志"""
|
"""清空所有登录日志"""
|
||||||
async with async_db_session.begin() as db:
|
await login_log_dao.delete_all(db)
|
||||||
count = await login_log_dao.delete_all(db)
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
login_log_service: LoginLogService = LoginLogService()
|
login_log_service: LoginLogService = LoginLogService()
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_menu import menu_dao
|
from backend.app.admin.crud.crud_menu import menu_dao
|
||||||
from backend.app.admin.model import Menu
|
from backend.app.admin.model import Menu
|
||||||
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
||||||
|
from backend.app.admin.utils.cache import user_cache_manager
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.core.conf import settings
|
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.database.redis import redis_client
|
|
||||||
from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
|
from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
|
||||||
|
|
||||||
|
|
||||||
@@ -18,124 +15,122 @@ class MenuService:
|
|||||||
"""菜单服务类"""
|
"""菜单服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> Menu:
|
async def get(*, db: AsyncSession, pk: int) -> Menu:
|
||||||
"""
|
"""
|
||||||
获取菜单详情
|
获取菜单详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 菜单 ID
|
:param pk: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
menu = await menu_dao.get(db, menu_id=pk)
|
menu = await menu_dao.get(db, menu_id=pk)
|
||||||
if not menu:
|
if not menu:
|
||||||
raise errors.NotFoundError(msg='菜单不存在')
|
raise errors.NotFoundError(msg='菜单不存在')
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_menu_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
|
async def get_tree(*, db: AsyncSession, title: str | None, status: int | None) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取菜单树形结构
|
获取菜单树形结构
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param title: 菜单标题
|
:param title: 菜单标题
|
||||||
:param status: 状态
|
:param status: 状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
menu_data = await menu_dao.get_all(db, title=title, status=status)
|
menu_data = await menu_dao.get_all(db, title=title, status=status)
|
||||||
menu_tree = get_tree_data(menu_data)
|
menu_tree = get_tree_data(menu_data)
|
||||||
return menu_tree
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_sidebar(*, request: Request) -> list[dict[str, Any] | None]:
|
async def get_sidebar(*, db: AsyncSession, request: Request) -> list[dict[str, Any] | None]:
|
||||||
"""
|
"""
|
||||||
获取用户的菜单侧边栏
|
获取用户的菜单侧边栏
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param request: FastAPI 请求对象
|
:param request: FastAPI 请求对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
menu_data = None
|
||||||
|
if request.user.is_superuser:
|
||||||
|
menu_data = await menu_dao.get_sidebar(db, None)
|
||||||
|
else:
|
||||||
roles = request.user.roles
|
roles = request.user.roles
|
||||||
menu_tree = []
|
menu_ids = set()
|
||||||
if roles:
|
if roles:
|
||||||
unique_menus = {}
|
|
||||||
for role in roles:
|
for role in roles:
|
||||||
for menu in role.menus:
|
menu_ids.update(menu.id for menu in role.menus)
|
||||||
unique_menus[menu.id] = menu
|
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
|
||||||
all_ids = set(unique_menus.keys())
|
|
||||||
valid_menu_ids = [
|
if menu_data:
|
||||||
menu_id
|
return get_vben5_tree_data(menu_data)
|
||||||
for menu_id, menu in unique_menus.items()
|
|
||||||
if menu.parent_id is None or menu.parent_id in all_ids
|
return []
|
||||||
]
|
|
||||||
menu_data = await menu_dao.get_sidebar(db, request.user.is_superuser, valid_menu_ids)
|
|
||||||
menu_tree = get_vben5_tree_data(menu_data)
|
|
||||||
return menu_tree
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateMenuParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建菜单
|
创建菜单
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 菜单创建参数
|
:param obj: 菜单创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
title = await menu_dao.get_by_title(db, obj.title)
|
title = await menu_dao.get_by_title(db, obj.title)
|
||||||
if title:
|
if title:
|
||||||
raise errors.ForbiddenError(msg='菜单标题已存在')
|
raise errors.ConflictError(msg='菜单标题已存在')
|
||||||
if obj.parent_id:
|
if obj.parent_id:
|
||||||
parent_menu = await menu_dao.get(db, obj.parent_id)
|
parent_menu = await menu_dao.get(db, obj.parent_id)
|
||||||
if not parent_menu:
|
if not parent_menu:
|
||||||
raise errors.NotFoundError(msg='父级菜单不存在')
|
raise errors.NotFoundError(msg='父级菜单不存在')
|
||||||
await menu_dao.create(db, obj)
|
await menu_dao.create(db, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateMenuParam) -> int:
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新菜单
|
更新菜单
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 菜单 ID
|
:param pk: 菜单 ID
|
||||||
:param obj: 菜单更新参数
|
:param obj: 菜单更新参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
menu = await menu_dao.get(db, pk)
|
menu = await menu_dao.get(db, pk)
|
||||||
if not menu:
|
if not menu:
|
||||||
raise errors.NotFoundError(msg='菜单不存在')
|
raise errors.NotFoundError(msg='菜单不存在')
|
||||||
if menu.title != obj.title:
|
if menu.title != obj.title and await menu_dao.get_by_title(db, obj.title):
|
||||||
if await menu_dao.get_by_title(db, obj.title):
|
raise errors.ConflictError(msg='菜单标题已存在')
|
||||||
raise errors.ForbiddenError(msg='菜单标题已存在')
|
if obj.parent_id:
|
||||||
if obj.parent_id:
|
parent_menu = await menu_dao.get(db, obj.parent_id)
|
||||||
parent_menu = await menu_dao.get(db, obj.parent_id)
|
if not parent_menu:
|
||||||
if not parent_menu:
|
raise errors.NotFoundError(msg='父级菜单不存在')
|
||||||
raise errors.NotFoundError(msg='父级菜单不存在')
|
if obj.parent_id == menu.id:
|
||||||
if obj.parent_id == menu.id:
|
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
||||||
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
count = await menu_dao.update(db, pk, obj)
|
||||||
count = await menu_dao.update(db, pk, obj)
|
await user_cache_manager.clear_by_menu_id(db, [pk])
|
||||||
for role in await menu.awaitable_attrs.roles:
|
return count
|
||||||
for user in await role.awaitable_attrs.users:
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: int) -> int:
|
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除菜单
|
删除菜单
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 菜单 ID
|
:param pk: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
children = await menu_dao.get_children(db, pk)
|
children = await menu_dao.get_children(db, pk)
|
||||||
if children:
|
if children:
|
||||||
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
|
raise errors.ConflictError(msg='菜单下存在子菜单,无法删除')
|
||||||
menu = await menu_dao.get(db, pk)
|
count = await menu_dao.delete(db, pk)
|
||||||
count = await menu_dao.delete(db, pk)
|
if count:
|
||||||
if menu:
|
await user_cache_manager.clear_by_menu_id(db, [pk])
|
||||||
for role in await menu.awaitable_attrs.roles:
|
return count
|
||||||
for user in await role.awaitable_attrs.users:
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
menu_service: MenuService = MenuService()
|
menu_service: MenuService = MenuService()
|
||||||
|
|||||||
@@ -1,56 +1,72 @@
|
|||||||
#!/usr/bin/env python3
|
from typing import Any
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_opera_log import opera_log_dao
|
from backend.app.admin.crud.crud_opera_log import opera_log_dao
|
||||||
from backend.app.admin.schema.opera_log import CreateOperaLogParam
|
from backend.app.admin.schema.opera_log import CreateOperaLogParam, DeleteOperaLogParam
|
||||||
from backend.database.db import async_db_session
|
from backend.common.pagination import paging_data
|
||||||
|
|
||||||
|
|
||||||
class OperaLogService:
|
class OperaLogService:
|
||||||
"""操作日志服务类"""
|
"""操作日志服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
|
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取操作日志列表查询条件
|
获取操作日志列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:param status: 状态
|
:param status: 状态
|
||||||
:param ip: IP 地址
|
:param ip: IP 地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
|
log_select = await opera_log_dao.get_select(username=username, status=status, ip=ip)
|
||||||
|
return await paging_data(db, log_select)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateOperaLogParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建操作日志
|
创建操作日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 操作日志创建参数
|
:param obj: 操作日志创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
await opera_log_dao.create(db, obj)
|
||||||
await opera_log_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def bulk_create(*, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
|
||||||
"""
|
"""
|
||||||
删除操作日志
|
批量创建操作日志
|
||||||
|
|
||||||
:param pk: 日志 ID 列表
|
:param db: 数据库会话
|
||||||
|
:param objs: 操作日志创建参数列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
await opera_log_dao.bulk_create(db, objs)
|
||||||
count = await opera_log_dao.delete(db, pk)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete_all() -> int:
|
async def delete(*, db: AsyncSession, obj: DeleteOperaLogParam) -> int:
|
||||||
"""清空所有操作日志"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
批量删除操作日志
|
||||||
count = await opera_log_dao.delete_all(db)
|
|
||||||
return count
|
:param db: 数据库会话
|
||||||
|
:param obj: 日志 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
count = await opera_log_dao.delete(db, obj.pks)
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_all(*, db: AsyncSession) -> None:
|
||||||
|
"""
|
||||||
|
清空所有操作日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await opera_log_dao.delete_all(db)
|
||||||
|
|
||||||
|
|
||||||
opera_log_service: OperaLogService = OperaLogService()
|
opera_log_service: OperaLogService = OperaLogService()
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -8,17 +6,17 @@ import zipfile
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dulwich import porcelain
|
import anyio
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import PluginType, StatusType
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.common.log import log
|
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.core.path_conf import PLUGIN_DIR
|
from backend.core.path_conf import PLUGIN_DIR
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
from backend.plugin.tools import install_requirements_async, uninstall_requirements_async
|
from backend.plugin.tools import uninstall_requirements_async
|
||||||
from backend.utils.re_verify import is_git_url
|
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -28,120 +26,68 @@ class PluginService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> list[dict[str, Any]]:
|
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 = [key async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*')]
|
||||||
keys.append(key)
|
|
||||||
|
|
||||||
for info in await redis_client.mget(*keys):
|
result = [json.loads(info) for info in await redis_client.mget(*keys)]
|
||||||
result.append(json.loads(info))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def changed() -> str | None:
|
async def changed() -> str | None:
|
||||||
"""插件状态是否变更"""
|
"""检查插件是否发生变更"""
|
||||||
return await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
return await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def install_zip(*, file: UploadFile) -> None:
|
async def install(*, type: PluginType, file: UploadFile | None = None, repo_url: str | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
通过 zip 压缩包安装插件
|
安装插件
|
||||||
|
|
||||||
|
:param type: 插件类型
|
||||||
:param file: 插件 zip 压缩包
|
:param file: 插件 zip 压缩包
|
||||||
|
:param repo_url: git 仓库地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
contents = await file.read()
|
if settings.ENVIRONMENT != 'dev':
|
||||||
file_bytes = io.BytesIO(contents)
|
raise errors.RequestError(msg='禁止在非开发环境下安装插件')
|
||||||
if not zipfile.is_zipfile(file_bytes):
|
if type == PluginType.zip:
|
||||||
raise errors.ForbiddenError(msg='插件压缩包格式非法')
|
if not file:
|
||||||
with zipfile.ZipFile(file_bytes) as zf:
|
raise errors.RequestError(msg='ZIP 压缩包不能为空')
|
||||||
# 校验压缩包
|
return await install_zip_plugin(file)
|
||||||
plugin_namelist = zf.namelist()
|
if not repo_url:
|
||||||
plugin_name = plugin_namelist[0].split('/')[0]
|
raise errors.RequestError(msg='Git 仓库地址不能为空')
|
||||||
if not plugin_namelist or plugin_name not in file.filename:
|
return await install_git_plugin(repo_url)
|
||||||
raise errors.ForbiddenError(msg='插件压缩包内容非法')
|
|
||||||
if (
|
|
||||||
len(plugin_namelist) <= 3
|
|
||||||
or f'{plugin_name}/plugin.toml' not in plugin_namelist
|
|
||||||
or f'{plugin_name}/README.md' not in plugin_namelist
|
|
||||||
):
|
|
||||||
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
|
|
||||||
|
|
||||||
# 插件是否可安装
|
|
||||||
full_plugin_path = os.path.join(PLUGIN_DIR, plugin_name)
|
|
||||||
if os.path.exists(full_plugin_path):
|
|
||||||
raise errors.ForbiddenError(msg='此插件已安装')
|
|
||||||
else:
|
|
||||||
os.makedirs(full_plugin_path, exist_ok=True)
|
|
||||||
|
|
||||||
# 解压(安装)
|
|
||||||
members = []
|
|
||||||
for member in zf.infolist():
|
|
||||||
if member.filename.startswith(plugin_name):
|
|
||||||
new_filename = member.filename.replace(plugin_name, '')
|
|
||||||
if new_filename:
|
|
||||||
member.filename = new_filename
|
|
||||||
members.append(member)
|
|
||||||
zf.extractall(os.path.join(PLUGIN_DIR, plugin_name), members)
|
|
||||||
|
|
||||||
await install_requirements_async(plugin_name)
|
|
||||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def install_git(*, repo_url: str):
|
async def uninstall(*, plugin: str) -> None:
|
||||||
"""
|
|
||||||
通过 git 安装插件
|
|
||||||
|
|
||||||
:param repo_url: git 存储库的 URL
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
match = is_git_url(repo_url)
|
|
||||||
if not match:
|
|
||||||
raise errors.ForbiddenError(msg='Git 仓库地址格式非法')
|
|
||||||
repo_name = match.group('repo')
|
|
||||||
plugins = await redis_client.lrange(settings.PLUGIN_REDIS_PREFIX, 0, -1)
|
|
||||||
if repo_name in plugins:
|
|
||||||
raise errors.ForbiddenError(msg=f'{repo_name} 插件已安装')
|
|
||||||
try:
|
|
||||||
porcelain.clone(repo_url, os.path.join(PLUGIN_DIR, repo_name), checkout=True)
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f'插件安装失败: {e}')
|
|
||||||
raise errors.ServerError(msg='插件安装失败,请稍后重试') from e
|
|
||||||
else:
|
|
||||||
await install_requirements_async(repo_name)
|
|
||||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def uninstall(*, plugin: str):
|
|
||||||
"""
|
"""
|
||||||
卸载插件
|
卸载插件
|
||||||
|
|
||||||
:param plugin: 插件名称
|
:param plugin: 插件名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
if settings.ENVIRONMENT != 'dev':
|
||||||
if not os.path.exists(plugin_dir):
|
raise errors.RequestError(msg='禁止在非开发环境下卸载插件')
|
||||||
raise errors.ForbiddenError(msg='插件不存在')
|
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
|
||||||
|
if not await plugin_dir.exists():
|
||||||
|
raise errors.NotFoundError(msg='插件不存在')
|
||||||
await uninstall_requirements_async(plugin)
|
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)
|
shutil.move(plugin_dir, bacup_dir)
|
||||||
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
|
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||||
await redis_client.hdel(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin)
|
|
||||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_status(*, plugin: str):
|
async def update_status(*, plugin: str) -> None:
|
||||||
"""
|
"""
|
||||||
更新插件状态
|
更新插件状态
|
||||||
|
|
||||||
:param plugin: 插件名称
|
:param plugin: 插件名称
|
||||||
:return:
|
: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:
|
if not plugin_info:
|
||||||
raise errors.ForbiddenError(msg='插件不存在')
|
raise errors.NotFoundError(msg='插件不存在')
|
||||||
plugin_info = json.loads(plugin_info)
|
plugin_info = json.loads(plugin_info)
|
||||||
|
|
||||||
# 更新持久缓存状态
|
# 更新持久缓存状态
|
||||||
@@ -151,10 +97,7 @@ class PluginService:
|
|||||||
else str(StatusType.disable.value)
|
else str(StatusType.disable.value)
|
||||||
)
|
)
|
||||||
plugin_info['plugin']['enable'] = new_status
|
plugin_info['plugin']['enable'] = new_status
|
||||||
await redis_client.set(
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(plugin_info, ensure_ascii=False))
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def build(*, plugin: str) -> io.BytesIO:
|
async def build(*, plugin: str) -> io.BytesIO:
|
||||||
@@ -164,9 +107,9 @@ class PluginService:
|
|||||||
:param plugin: 插件名称
|
:param plugin: 插件名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
|
||||||
if not os.path.exists(plugin_dir):
|
if not await plugin_dir.exists():
|
||||||
raise errors.ForbiddenError(msg='插件不存在')
|
raise errors.NotFoundError(msg='插件不存在')
|
||||||
|
|
||||||
bio = io.BytesIO()
|
bio = io.BytesIO()
|
||||||
with zipfile.ZipFile(bio, 'w') as zf:
|
with zipfile.ZipFile(bio, 'w') as zf:
|
||||||
@@ -174,7 +117,7 @@ class PluginService:
|
|||||||
dirs[:] = [d for d in dirs if d != '__pycache__']
|
dirs[:] = [d for d in dirs if d != '__pycache__']
|
||||||
for file in files:
|
for file in files:
|
||||||
file_path = os.path.join(root, file)
|
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))
|
zf.write(file_path, os.path.join(plugin, arcname))
|
||||||
|
|
||||||
bio.seek(0)
|
bio.seek(0)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
from typing import Any
|
||||||
from typing import Any, Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
||||||
from backend.app.admin.crud.crud_menu import menu_dao
|
from backend.app.admin.crud.crud_menu import menu_dao
|
||||||
@@ -10,14 +9,14 @@ from backend.app.admin.crud.crud_role import role_dao
|
|||||||
from backend.app.admin.model import Role
|
from backend.app.admin.model import Role
|
||||||
from backend.app.admin.schema.role import (
|
from backend.app.admin.schema.role import (
|
||||||
CreateRoleParam,
|
CreateRoleParam,
|
||||||
|
DeleteRoleParam,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
UpdateRoleParam,
|
UpdateRoleParam,
|
||||||
UpdateRoleScopeParam,
|
UpdateRoleScopeParam,
|
||||||
)
|
)
|
||||||
|
from backend.app.admin.utils.cache import user_cache_manager
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.core.conf import settings
|
from backend.common.pagination import paging_data
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.database.redis import redis_client
|
|
||||||
from backend.utils.build_tree import get_tree_data
|
from backend.utils.build_tree import get_tree_data
|
||||||
|
|
||||||
|
|
||||||
@@ -25,175 +24,170 @@ class RoleService:
|
|||||||
"""角色服务类"""
|
"""角色服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> Role:
|
async def get(*, db: AsyncSession, pk: int) -> Role:
|
||||||
"""
|
"""
|
||||||
获取角色详情
|
获取角色详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 角色 ID
|
:param pk: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get_join(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
return role
|
return role
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> Sequence[Role]:
|
async def get_all(*, db: AsyncSession) -> Sequence[Role]:
|
||||||
"""获取所有角色"""
|
|
||||||
async with async_db_session() as db:
|
|
||||||
roles = await role_dao.get_all(db)
|
|
||||||
return roles
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_users(*, pk: int) -> Sequence[Role]:
|
|
||||||
"""
|
"""
|
||||||
获取用户的角色列表
|
获取所有角色
|
||||||
|
|
||||||
:param pk: 用户 ID
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
roles = await role_dao.get_users(db, user_id=pk)
|
roles = await role_dao.get_all(db)
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str | None, status: int | None) -> Select:
|
async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取角色列表查询条件
|
获取角色列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param name: 角色名称
|
:param name: 角色名称
|
||||||
:param status: 状态
|
:param status: 状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await role_dao.get_list(name=name, status=status)
|
role_select = await role_dao.get_select(name=name, status=status)
|
||||||
|
return await paging_data(db, role_select)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_menu_tree(*, pk: int) -> list[dict[str, Any] | None]:
|
async def get_menu_tree(*, db: AsyncSession, pk: int) -> list[dict[str, Any] | None]:
|
||||||
"""
|
"""
|
||||||
获取角色的菜单树形结构
|
获取角色的菜单树形结构
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 角色 ID
|
:param pk: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
menu_tree = get_tree_data(role.menus) if role.menus else []
|
menus = await role_dao.get_menus(db, pk)
|
||||||
return menu_tree
|
menu_tree = get_tree_data(menus) if menus else []
|
||||||
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_scopes(*, pk: int) -> list[int]:
|
async def get_scopes(*, db: AsyncSession, pk: int) -> list[int]:
|
||||||
"""
|
"""
|
||||||
获取角色数据范围列表
|
获取角色数据范围列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get_join(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
scope_ids = [scope.id for scope in role.scopes]
|
scope_ids = [scope.id for scope in role.scopes]
|
||||||
return scope_ids
|
return scope_ids
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateRoleParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateRoleParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建角色
|
创建角色
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param obj: 角色创建参数
|
:param obj: 角色创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
role = await role_dao.get_by_name(db, obj.name)
|
role = await role_dao.get_by_name(db, obj.name)
|
||||||
if role:
|
if role:
|
||||||
raise errors.ForbiddenError(msg='角色已存在')
|
raise errors.ConflictError(msg='角色已存在')
|
||||||
await role_dao.create(db, obj)
|
await role_dao.create(db, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateRoleParam) -> int:
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateRoleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色
|
更新角色
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 角色 ID
|
:param pk: 角色 ID
|
||||||
:param obj: 角色更新参数
|
:param obj: 角色更新参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
role = await role_dao.get(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
if role.name != obj.name:
|
if role.name != obj.name and await role_dao.get_by_name(db, obj.name):
|
||||||
role = await role_dao.get_by_name(db, obj.name)
|
raise errors.ConflictError(msg='角色已存在')
|
||||||
if role:
|
count = await role_dao.update(db, pk, obj)
|
||||||
raise errors.ForbiddenError(msg='角色已存在')
|
await user_cache_manager.clear_by_role_id(db, [pk])
|
||||||
count = await role_dao.update(db, pk, obj)
|
return count
|
||||||
for user in await role.awaitable_attrs.users:
|
|
||||||
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_role_menu(*, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
|
async def update_role_menu(*, db: AsyncSession, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色菜单
|
更新角色菜单
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 角色 ID
|
:param pk: 角色 ID
|
||||||
:param menu_ids: 菜单 ID 列表
|
:param menu_ids: 菜单 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
for menu_id in menu_ids.menus:
|
for menu_id in menu_ids.menus:
|
||||||
menu = await menu_dao.get(db, menu_id)
|
menu = await menu_dao.get(db, menu_id)
|
||||||
if not menu:
|
if not menu:
|
||||||
raise errors.NotFoundError(msg='菜单不存在')
|
raise errors.NotFoundError(msg='菜单不存在')
|
||||||
count = await role_dao.update_menus(db, pk, menu_ids)
|
count = await role_dao.update_menus(db, pk, menu_ids)
|
||||||
for user in await role.awaitable_attrs.users:
|
await user_cache_manager.clear_by_role_id(db, [pk])
|
||||||
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_role_scope(*, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
|
async def update_role_scope(*, db: AsyncSession, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色数据范围
|
更新角色数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param pk: 角色 ID
|
:param pk: 角色 ID
|
||||||
:param scope_ids: 权限规则 ID 列表
|
:param scope_ids: 权限规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
role = await role_dao.get(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
for scope_id in scope_ids.scopes:
|
for scope_id in scope_ids.scopes:
|
||||||
scope = await data_scope_dao.get(db, scope_id)
|
scope = await data_scope_dao.get(db, scope_id)
|
||||||
if not scope:
|
if not scope:
|
||||||
raise errors.NotFoundError(msg='数据范围不存在')
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
count = await role_dao.update_scopes(db, pk, scope_ids)
|
count = await role_dao.update_scopes(db, pk, scope_ids)
|
||||||
for user in await role.awaitable_attrs.users:
|
await user_cache_manager.clear_by_role_id(db, [pk])
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, db: AsyncSession, obj: DeleteRoleParam) -> int:
|
||||||
"""
|
"""
|
||||||
删除角色
|
批量删除角色
|
||||||
|
|
||||||
:param pk: 角色 ID 列表
|
:param db: 数据库会话
|
||||||
|
:param obj: 角色 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
count = await role_dao.delete(db, pk)
|
count = await role_dao.delete(db, obj.pks)
|
||||||
for _pk in pk:
|
await user_cache_manager.clear_by_role_id(db, obj.pks)
|
||||||
role = await role_dao.get(db, _pk)
|
return count
|
||||||
if role:
|
|
||||||
for user in await role.awaitable_attrs.users:
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
role_service: RoleService = RoleService()
|
role_service: RoleService = RoleService()
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from backend.app.admin.crud.crud_user_password_history import user_password_history_dao
|
||||||
|
from backend.app.admin.schema.user_password_history import CreateUserPasswordHistoryParam
|
||||||
|
from backend.common.exception import errors
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.database.redis import redis_client
|
||||||
|
from backend.utils.dynamic_config import load_user_security_config
|
||||||
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordHistoryService:
|
||||||
|
"""用户密码历史服务类"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def check_status(user_id: int, user_status: int) -> None:
|
||||||
|
"""
|
||||||
|
检查用户状态
|
||||||
|
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param user_status: 用户状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not user_status:
|
||||||
|
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||||
|
|
||||||
|
locked_until_str = await redis_client.get(f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}')
|
||||||
|
|
||||||
|
if locked_until_str:
|
||||||
|
locked_until = timezone.from_str(locked_until_str)
|
||||||
|
now = timezone.now()
|
||||||
|
if locked_until > now:
|
||||||
|
remaining_minutes = math.ceil((locked_until - now).total_seconds() / 60)
|
||||||
|
raise errors.AuthorizationError(msg=f'账号已被锁定,请在 {remaining_minutes} 分钟后重试')
|
||||||
|
|
||||||
|
await redis_client.delete(f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}')
|
||||||
|
await redis_client.delete(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def handle_login_failure(db: AsyncSession, user_id: int) -> None:
|
||||||
|
"""
|
||||||
|
处理登录失败
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await load_user_security_config(db)
|
||||||
|
|
||||||
|
if settings.USER_LOCK_THRESHOLD == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
failure_count = await redis_client.get(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}')
|
||||||
|
failure_count = int(failure_count) if failure_count else 0
|
||||||
|
failure_count += 1
|
||||||
|
await redis_client.set(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}', str(failure_count))
|
||||||
|
|
||||||
|
if failure_count >= settings.USER_LOCK_THRESHOLD:
|
||||||
|
locked_until = timezone.now() + timedelta(seconds=settings.USER_LOCK_SECONDS)
|
||||||
|
await redis_client.set(f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}', timezone.to_str(locked_until))
|
||||||
|
raise errors.AuthorizationError(msg='登录失败次数过多,账号已被锁定')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def check_password_expiry_status(db: AsyncSession, password_changed_time: datetime) -> int | None:
|
||||||
|
"""
|
||||||
|
检查密码过期状态
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param password_changed_time: 密码修改时间
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await load_user_security_config(db)
|
||||||
|
|
||||||
|
if settings.USER_PASSWORD_EXPIRY_DAYS == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not password_changed_time:
|
||||||
|
raise errors.AuthorizationError(msg='密码已过期,请修改密码后重新登录')
|
||||||
|
|
||||||
|
expiry_time = password_changed_time + timedelta(days=settings.USER_PASSWORD_EXPIRY_DAYS)
|
||||||
|
days_remaining = (expiry_time - timezone.now()).days
|
||||||
|
|
||||||
|
if days_remaining < 0:
|
||||||
|
raise errors.AuthorizationError(msg='密码已过期,请修改密码后重新登录')
|
||||||
|
|
||||||
|
if days_remaining <= settings.USER_PASSWORD_REMINDER_DAYS:
|
||||||
|
return days_remaining
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def handle_login_success(user_id: int) -> None:
|
||||||
|
"""
|
||||||
|
处理登录成功
|
||||||
|
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await redis_client.delete(f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}')
|
||||||
|
await redis_client.delete(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def save_password_history(db: AsyncSession, obj: CreateUserPasswordHistoryParam) -> None:
|
||||||
|
"""
|
||||||
|
保存密码历史记录
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 创建密码历史记录参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await user_password_history_dao.create(db, obj)
|
||||||
|
|
||||||
|
|
||||||
|
password_security_service: UserPasswordHistoryService = UserPasswordHistoryService()
|
||||||
@@ -1,291 +1,325 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy import Select
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_dept import dept_dao
|
from backend.app.admin.crud.crud_dept import dept_dao
|
||||||
from backend.app.admin.crud.crud_role import role_dao
|
from backend.app.admin.crud.crud_role import role_dao
|
||||||
from backend.app.admin.crud.crud_user import user_dao
|
from backend.app.admin.crud.crud_user import user_dao
|
||||||
from backend.app.admin.model import User
|
from backend.app.admin.model import Role, User
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
RegisterUserParam,
|
|
||||||
ResetPasswordParam,
|
ResetPasswordParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
)
|
)
|
||||||
|
from backend.app.admin.schema.user_password_history import CreateUserPasswordHistoryParam
|
||||||
|
from backend.app.admin.service.user_password_history_service import password_security_service
|
||||||
|
from backend.app.admin.utils.password_security import password_verify, validate_new_password
|
||||||
|
from backend.common.context import ctx
|
||||||
|
from backend.common.enums import UserPermissionType
|
||||||
from backend.common.exception import errors
|
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.pagination import paging_data
|
||||||
|
from backend.common.response.response_code import CustomErrorCode
|
||||||
|
from backend.common.security.jwt import get_token, jwt_decode
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
|
from backend.utils.serializers import select_join_serialize
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
"""用户服务类"""
|
"""用户服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def register(*, obj: RegisterUserParam) -> None:
|
async def get_userinfo(*, db: AsyncSession, pk: int | None = None, username: str | None = None) -> User:
|
||||||
"""
|
|
||||||
注册新用户
|
|
||||||
|
|
||||||
:param obj: 用户注册参数
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
if not obj.password:
|
|
||||||
raise errors.ForbiddenError(msg='密码为空')
|
|
||||||
username = await user_dao.get_by_username(db, obj.username)
|
|
||||||
if username:
|
|
||||||
raise errors.ForbiddenError(msg='用户已注册')
|
|
||||||
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(10000, 88888)}'
|
|
||||||
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
|
||||||
if nickname:
|
|
||||||
raise errors.ForbiddenError(msg='昵称已注册')
|
|
||||||
email = await user_dao.check_email(db, obj.email)
|
|
||||||
if email:
|
|
||||||
raise errors.ForbiddenError(msg='邮箱已注册')
|
|
||||||
await user_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def add(*, request: Request, obj: AddUserParam) -> None:
|
|
||||||
"""
|
|
||||||
添加新用户
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param obj: 用户添加参数
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
superuser_verify(request)
|
|
||||||
username = await user_dao.get_by_username(db, obj.username)
|
|
||||||
if username:
|
|
||||||
raise errors.ForbiddenError(msg='用户已注册')
|
|
||||||
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(88888, 99999)}'
|
|
||||||
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
|
||||||
if nickname:
|
|
||||||
raise errors.ForbiddenError(msg='昵称已注册')
|
|
||||||
if not obj.password:
|
|
||||||
raise errors.ForbiddenError(msg='密码为空')
|
|
||||||
email = await user_dao.check_email(db, obj.email)
|
|
||||||
if email:
|
|
||||||
raise errors.ForbiddenError(msg='邮箱已注册')
|
|
||||||
dept = await dept_dao.get(db, obj.dept_id)
|
|
||||||
if not dept:
|
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
|
||||||
for role_id in obj.roles:
|
|
||||||
role = await role_dao.get(db, role_id)
|
|
||||||
if not role:
|
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
|
||||||
await user_dao.add(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def pwd_reset(*, 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, request.user.id)
|
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
if not password_verify(obj.old_password, user.password):
|
|
||||||
raise errors.ForbiddenError(msg='原密码错误')
|
|
||||||
if obj.new_password != obj.confirm_password:
|
|
||||||
raise errors.ForbiddenError(msg='密码输入不一致')
|
|
||||||
new_pwd = get_hash_password(obj.new_password, user.salt)
|
|
||||||
count = await user_dao.reset_password(db, request.user.id, new_pwd)
|
|
||||||
key_prefix = [
|
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}',
|
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}',
|
|
||||||
f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}',
|
|
||||||
]
|
|
||||||
for prefix in key_prefix:
|
|
||||||
await redis_client.delete_prefix(prefix)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_userinfo(*, username: str) -> User:
|
|
||||||
"""
|
"""
|
||||||
获取用户信息
|
获取用户信息
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 用户 ID
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
user = await user_dao.get_join(db, user_id=pk, username=username)
|
||||||
user = await user_dao.get_with_relation(db, username=username)
|
if not user:
|
||||||
if not user:
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
return user
|
||||||
return user
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
|
async def get_roles(*, db: AsyncSession, pk: int) -> Sequence[Role]:
|
||||||
"""
|
"""
|
||||||
更新用户信息
|
获取用户所有角色
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param db: 数据库会话
|
||||||
:param username: 用户名
|
:param pk: 用户 ID
|
||||||
:param obj: 用户更新参数
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
user = await user_dao.get_join(db, user_id=pk)
|
||||||
if request.user.username != username:
|
if not user:
|
||||||
raise errors.ForbiddenError(msg='你只能修改自己的信息')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
user = await user_dao.get_with_relation(db, username=username)
|
return user.roles
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
if user.username != obj.username:
|
|
||||||
_username = await user_dao.get_by_username(db, obj.username)
|
|
||||||
if _username:
|
|
||||||
raise errors.ForbiddenError(msg='用户名已注册')
|
|
||||||
if user.nickname != obj.nickname:
|
|
||||||
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
|
||||||
if nickname:
|
|
||||||
raise errors.ForbiddenError(msg='昵称已注册')
|
|
||||||
if user.email != obj.email:
|
|
||||||
email = await user_dao.check_email(db, obj.email)
|
|
||||||
if email:
|
|
||||||
raise errors.ForbiddenError(msg='邮箱已注册')
|
|
||||||
for role_id in obj.roles:
|
|
||||||
role = await role_dao.get(db, role_id)
|
|
||||||
if not role:
|
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
|
||||||
count = await user_dao.update(db, user, obj)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
|
async def get_list(*, db: AsyncSession, dept: int, username: str, phone: str, status: int) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
获取用户列表查询条件
|
获取用户列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param dept: 部门 ID
|
:param dept: 部门 ID
|
||||||
:param username: 用户名
|
:param username: 用户名
|
||||||
:param phone: 手机号
|
:param phone: 手机号
|
||||||
:param status: 状态
|
:param status: 状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
|
user_select = await user_dao.get_select(dept=dept, username=username, phone=phone, status=status)
|
||||||
|
data = await paging_data(db, user_select)
|
||||||
|
if data['items']:
|
||||||
|
serialized_items = select_join_serialize(data['items'], relationships=['User-m2o-Dept', 'User-m2m-Role'])
|
||||||
|
# 确保返回的是列表,即使只有一个元素
|
||||||
|
data['items'] = [serialized_items] if not isinstance(serialized_items, list) else serialized_items
|
||||||
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_permission(*, request: Request, pk: int) -> int:
|
async def create(*, db: AsyncSession, obj: AddUserParam) -> None:
|
||||||
|
"""
|
||||||
|
创建用户
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 用户添加参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if await user_dao.get_by_username(db, obj.username):
|
||||||
|
raise errors.ConflictError(msg='用户名已注册')
|
||||||
|
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):
|
||||||
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
|
for role_id in obj.roles:
|
||||||
|
if not await role_dao.get(db, role_id):
|
||||||
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
|
await user_dao.add(db, obj)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateUserParam) -> int:
|
||||||
|
"""
|
||||||
|
更新用户信息
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 用户 ID
|
||||||
|
:param obj: 用户更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
user = await user_dao.get_join(db, user_id=pk)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
if obj.username != user.username and await user_dao.get_by_username(db, obj.username):
|
||||||
|
raise errors.ConflictError(msg='用户名已注册')
|
||||||
|
if obj.dept_id and obj.dept_id != user.dept_id and not await dept_dao.get(db, dept_id=obj.dept_id):
|
||||||
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
|
for role_id in obj.roles:
|
||||||
|
if not await role_dao.get(db, role_id):
|
||||||
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
|
count = await user_dao.update(db, user.id, obj)
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_permission(*, db: AsyncSession, request: Request, pk: int, type: UserPermissionType) -> int: # noqa: C901
|
||||||
"""
|
"""
|
||||||
更新用户权限
|
更新用户权限
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:param request: FastAPI 请求对象
|
:param request: FastAPI 请求对象
|
||||||
:param pk: 用户 ID
|
:param pk: 用户 ID
|
||||||
|
:param type: 权限类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
match type:
|
||||||
superuser_verify(request)
|
case UserPermissionType.superuser:
|
||||||
user = await user_dao.get(db, pk)
|
user = await user_dao.get(db, pk)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if pk == request.user.id:
|
if pk == request.user.id:
|
||||||
raise errors.ForbiddenError(msg='非法操作')
|
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||||
super_status = await user_dao.get_super(db, pk)
|
count = await user_dao.set_super(db, pk, is_super=not user.is_superuser)
|
||||||
count = await user_dao.set_super(db, pk, not super_status)
|
case UserPermissionType.staff:
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
user = await user_dao.get(db, pk)
|
||||||
return count
|
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
|
@staticmethod
|
||||||
async def update_staff(*, request: Request, pk: int) -> int:
|
async def reset_password(*, db: AsyncSession, pk: int, password: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户职员状态
|
重置用户密码
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param db: 数据库会话
|
||||||
:param pk: 用户 ID
|
:param pk: 用户 ID
|
||||||
|
:param password: 新密码
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
user = await user_dao.get(db, pk)
|
||||||
superuser_verify(request)
|
if not user:
|
||||||
user = await user_dao.get(db, pk)
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
await validate_new_password(db, user.id, password)
|
||||||
if pk == request.user.id:
|
count = await user_dao.reset_password(db, user.id, password)
|
||||||
raise errors.ForbiddenError(msg='非法操作')
|
|
||||||
staff_status = await user_dao.get_staff(db, pk)
|
history_obj = CreateUserPasswordHistoryParam(user_id=user.id, password=user.password)
|
||||||
count = await user_dao.set_staff(db, pk, not staff_status)
|
await password_security_service.save_password_history(db, history_obj)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
await user_dao.update_password_changed_time(db, user.id)
|
||||||
return count
|
|
||||||
|
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
|
@staticmethod
|
||||||
async def update_status(*, request: Request, pk: int) -> int:
|
async def update_nickname(*, db: AsyncSession, user_id: int, nickname: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户状态
|
更新当前用户昵称
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param db: 数据库会话
|
||||||
:param pk: 用户 ID
|
:param user_id: 用户 ID
|
||||||
|
:param nickname: 用户昵称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
count = await user_dao.update_nickname(db, user_id, nickname)
|
||||||
superuser_verify(request)
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}')
|
||||||
user = await user_dao.get(db, pk)
|
return count
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
if pk == request.user.id:
|
|
||||||
raise errors.ForbiddenError(msg='非法操作')
|
|
||||||
status = await user_dao.get_status(db, pk)
|
|
||||||
count = await user_dao.set_status(db, pk, 0 if status == 1 else 1)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_multi_login(*, request: Request, pk: int) -> int:
|
async def update_avatar(*, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户多端登录状态
|
更新当前用户头像
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param db: 数据库会话
|
||||||
:param pk: 用户 ID
|
:param user_id: 用户 ID
|
||||||
|
:param avatar: 头像地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
count = await user_dao.update_avatar(db, user_id, avatar)
|
||||||
superuser_verify(request)
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}')
|
||||||
user = await user_dao.get(db, pk)
|
return count
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
multi_login = await user_dao.get_multi_login(db, pk) 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
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, username: str) -> int:
|
async def update_email(*, db: AsyncSession, user_id: int, captcha: str, email: str) -> int:
|
||||||
|
"""
|
||||||
|
更新当前用户邮箱
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param captcha: 邮箱验证码
|
||||||
|
:param email: 邮箱
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
|
||||||
|
if not captcha_code:
|
||||||
|
raise errors.RequestError(msg='验证码已失效,请重新获取')
|
||||||
|
if captcha != captcha_code:
|
||||||
|
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||||
|
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
|
||||||
|
count = await user_dao.update_email(db, user_id, email)
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_password(*, db: AsyncSession, user_id: int, obj: ResetPasswordParam) -> int:
|
||||||
|
"""
|
||||||
|
更新当前用户密码
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param obj: 密码重置参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
user = await user_dao.get(db, user_id)
|
||||||
|
|
||||||
|
if user.password and not password_verify(obj.old_password, user.password):
|
||||||
|
raise errors.RequestError(msg='原密码错误')
|
||||||
|
|
||||||
|
if obj.new_password != obj.confirm_password:
|
||||||
|
raise errors.RequestError(msg='两次密码输入不一致')
|
||||||
|
|
||||||
|
await validate_new_password(db, user_id, obj.new_password)
|
||||||
|
count = await user_dao.reset_password(db, user_id, obj.new_password)
|
||||||
|
|
||||||
|
history_obj = CreateUserPasswordHistoryParam(user_id=user.id, password=user.password)
|
||||||
|
await password_security_service.save_password_history(db, history_obj)
|
||||||
|
await user_dao.update_password_changed_time(db, user.id)
|
||||||
|
|
||||||
|
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(prefix)
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除用户
|
删除用户
|
||||||
|
|
||||||
:param username: 用户名
|
:param db: 数据库会话
|
||||||
|
:param pk: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
user = await user_dao.get(db, pk)
|
||||||
user = await user_dao.get_by_username(db, username)
|
if not user:
|
||||||
if not user:
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
count = await user_dao.delete(db, user.id)
|
||||||
count = await user_dao.delete(db, user.id)
|
key_prefix = [
|
||||||
key_prefix = [
|
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
]
|
||||||
]
|
for key in key_prefix:
|
||||||
for key in key_prefix:
|
await redis_client.delete_prefix(key)
|
||||||
await redis_client.delete_prefix(key)
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
user_service: UserService = UserService()
|
user_service: UserService = UserService()
|
||||||
|
|||||||
@@ -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
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Generator
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import AsyncGenerator
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from backend.app.admin.model import data_scope_rule, role_data_scope, role_menu, user_role
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.database.redis import redis_client
|
||||||
|
|
||||||
|
|
||||||
|
class UserCacheManager:
|
||||||
|
"""用户缓存管理"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def clear(user_ids: Sequence[int]) -> None:
|
||||||
|
"""
|
||||||
|
清理用户缓存
|
||||||
|
|
||||||
|
:param user_ids: 用户 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if user_ids:
|
||||||
|
await redis_client.delete(*[f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}' for user_id in user_ids])
|
||||||
|
|
||||||
|
async def clear_by_role_id(self, db: AsyncSession, role_ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
通过角色 ID 清理用户缓存
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param role_ids: 角色 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stmt = select(user_role.c.user_id).where(user_role.c.role_id.in_(role_ids)).distinct()
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user_ids = result.scalars().all()
|
||||||
|
|
||||||
|
await self.clear(user_ids)
|
||||||
|
|
||||||
|
async def clear_by_menu_id(self, db: AsyncSession, menu_ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
通过菜单 ID 清理用户缓存
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param menu_ids: 菜单 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(user_role.c.user_id)
|
||||||
|
.join(role_menu, user_role.c.role_id == role_menu.c.role_id)
|
||||||
|
.where(role_menu.c.menu_id.in_(menu_ids))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user_ids = result.scalars().all()
|
||||||
|
|
||||||
|
await self.clear(user_ids)
|
||||||
|
|
||||||
|
async def clear_by_data_scope_id(self, db: AsyncSession, scope_ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
通过数据范围 ID 清理用户缓存
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param scope_ids: 数据范围 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(user_role.c.user_id)
|
||||||
|
.join(role_data_scope, user_role.c.role_id == role_data_scope.c.role_id)
|
||||||
|
.where(role_data_scope.c.data_scope_id.in_(scope_ids))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user_ids = result.scalars().all()
|
||||||
|
|
||||||
|
await self.clear(user_ids)
|
||||||
|
|
||||||
|
async def clear_by_data_rule_id(self, db: AsyncSession, rule_ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
通过数据规则 ID 清理用户缓存
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param rule_ids: 数据规则 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(user_role.c.user_id)
|
||||||
|
.join(role_data_scope, user_role.c.role_id == role_data_scope.c.role_id)
|
||||||
|
.join(data_scope_rule, role_data_scope.c.data_scope_id == data_scope_rule.c.data_scope_id)
|
||||||
|
.where(data_scope_rule.c.data_rule_id.in_(rule_ids))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
user_ids = result.scalars().all()
|
||||||
|
|
||||||
|
await self.clear(user_ids)
|
||||||
|
|
||||||
|
|
||||||
|
user_cache_manager: UserCacheManager = UserCacheManager()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from pwdlib import PasswordHash
|
||||||
|
from pwdlib.hashers.bcrypt import BcryptHasher
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from backend.app.admin.crud.crud_user_password_history import user_password_history_dao
|
||||||
|
from backend.common.exception import errors
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.utils.dynamic_config import load_user_security_config
|
||||||
|
from backend.utils.re_verify import is_has_letter, is_has_number, is_has_special_char
|
||||||
|
|
||||||
|
password_hash = PasswordHash((BcryptHasher(),))
|
||||||
|
|
||||||
|
|
||||||
|
def get_hash_password(password: str, salt: bytes | None) -> str:
|
||||||
|
"""
|
||||||
|
使用哈希算法加密密码
|
||||||
|
|
||||||
|
:param password: 密码
|
||||||
|
:param salt: 盐值
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return password_hash.hash(password, salt=salt)
|
||||||
|
|
||||||
|
|
||||||
|
def password_verify(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""
|
||||||
|
密码验证
|
||||||
|
|
||||||
|
:param plain_password: 待验证的密码
|
||||||
|
:param hashed_password: 哈希密码
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return password_hash.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_new_password(db: AsyncSession, user_id: int, new_password: str) -> None:
|
||||||
|
"""
|
||||||
|
验证新密码
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户ID
|
||||||
|
:param new_password: 新密码
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await load_user_security_config(db)
|
||||||
|
|
||||||
|
if len(new_password) < settings.USER_PASSWORD_MIN_LENGTH:
|
||||||
|
raise errors.RequestError(msg=f'密码长度不能少于 {settings.USER_PASSWORD_MIN_LENGTH} 个字符')
|
||||||
|
|
||||||
|
if len(new_password) > settings.USER_PASSWORD_MAX_LENGTH:
|
||||||
|
raise errors.RequestError(msg=f'密码长度不能超过 {settings.USER_PASSWORD_MAX_LENGTH} 个字符')
|
||||||
|
|
||||||
|
if not is_has_number(new_password):
|
||||||
|
raise errors.RequestError(msg='密码必须包含数字')
|
||||||
|
|
||||||
|
if not is_has_letter(new_password):
|
||||||
|
raise errors.RequestError(msg='密码必须包含字母')
|
||||||
|
|
||||||
|
if settings.USER_PASSWORD_REQUIRE_SPECIAL_CHAR and not is_has_special_char(new_password):
|
||||||
|
raise errors.RequestError(msg='密码必须包含特殊字符(如:!@#$%)')
|
||||||
|
|
||||||
|
password_history = await user_password_history_dao.get_by_user_id(db, user_id)
|
||||||
|
|
||||||
|
for hist in password_history[: settings.USER_PASSWORD_HISTORY_CHECK_COUNT]:
|
||||||
|
if password_verify(new_password, hist.password):
|
||||||
|
raise errors.RequestError(
|
||||||
|
msg=f'新密码不能与最近 {settings.USER_PASSWORD_HISTORY_CHECK_COUNT} 次使用的密码相同'
|
||||||
|
)
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.router import v1 as admin_v1
|
from backend.app.admin.api.router import v1 as admin_v1
|
||||||
|
|||||||
@@ -3,20 +3,19 @@
|
|||||||
当前任务使用 Celery
|
当前任务使用 Celery
|
||||||
实现,实施方案请查看 [#225](https://github.com/fastapi-practices/fastapi_best_architecture/discussions/225)
|
实现,实施方案请查看 [#225](https://github.com/fastapi-practices/fastapi_best_architecture/discussions/225)
|
||||||
|
|
||||||
## 添加任务
|
## 定时任务
|
||||||
|
|
||||||
> [!IMPORTANT]
|
在 `backend/app/task/tasks/beat.py` 文件内编写相关定时任务
|
||||||
> 由于 Celery 任务扫描规则,使其对任务的目录结构要求及其严格,务必在 celery_task 目录下添加任务
|
|
||||||
|
|
||||||
### 简单任务
|
### 简单任务
|
||||||
|
|
||||||
可以直接在 `tasks.py` 文件内编写相关任务代码
|
在 `backend/app/task/tasks/tasks.py` 文件内编写相关任务代码
|
||||||
|
|
||||||
### 层级任务
|
### 层级任务
|
||||||
|
|
||||||
如果你想对任务进行目录层级划分,使任务结构更加清晰,你可以新建任意目录,但必须注意的是
|
如果你想对任务进行目录层级划分,使任务结构更加清晰,你可以新建任意目录,但必须注意的是
|
||||||
|
|
||||||
1. 新建目录后,务必更新任务配置 `CELERY_TASKS_PACKAGES`,将新建目录添加到此列表
|
1. 在 `backend/app/task/tasks` 目录下新建 python 包目录
|
||||||
2. 在新建目录下,务必添加 `tasks.py` 文件,并在此文件中编写相关任务代码
|
2. 在新建目录下,务必添加 `tasks.py` 文件,并在此文件中编写相关任务代码
|
||||||
|
|
||||||
## 消息代理
|
## 消息代理
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pathlib import Path
|
from backend.core.path_conf import BASE_PATH
|
||||||
|
|
||||||
|
from .actions import * # noqa: F403
|
||||||
|
|
||||||
# 导入项目根目录
|
# 导入项目根目录
|
||||||
sys.path.append(str(Path(__file__).resolve().parent.parent.parent.parent))
|
sys.path.append(str(BASE_PATH.parent))
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
|
from backend.app.task.celery import celery_app
|
||||||
|
from backend.common.socketio.server import sio
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
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,10 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.task.api.v1.task import router as task_router
|
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
|
from backend.core.conf import settings
|
||||||
|
|
||||||
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
|
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH, tags=['任务'])
|
||||||
|
|
||||||
v1.include_router(task_router, prefix='/tasks', tags=['任务'])
|
v1.include_router(task_control_router, prefix='/tasks')
|
||||||
|
v1.include_router(task_result_router, prefix='/task-results')
|
||||||
|
v1.include_router(task_scheduler_router, prefix='/schedulers')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user