mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Compare commits
276
Commits
v1.0.0
..
mysql-main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
ccfe35ea85 | ||
|
|
4a118c166a | ||
|
|
5f3823cb6d | ||
|
|
19ef52da9b | ||
|
|
39c3339a8d | ||
|
|
9f52629dcb | ||
|
|
1cddd6e2d5 | ||
|
|
ed10e6ff47 | ||
|
|
48574fb50a | ||
|
|
ee7fe3087d | ||
|
|
270ac3d941 | ||
|
|
2f76988076 | ||
|
|
b275481742 | ||
|
|
f3f1ceb595 | ||
|
|
bcc764142b | ||
|
|
9b0d9f8296 | ||
|
|
63ba7646c1 | ||
|
|
1a7734185f | ||
|
|
8541059d23 | ||
|
|
61edc81333 | ||
|
|
8998687adb | ||
|
|
85b0294da9 | ||
|
|
a8ecb4ae3b | ||
|
|
a87ff74b56 | ||
|
|
3c5682988d | ||
|
|
1d32154498 | ||
|
|
f4290ddb8c | ||
|
|
1e54b0ba42 | ||
|
|
27b9cf4c64 | ||
|
|
87f29e52a1 | ||
|
|
16a0f9646d | ||
|
|
032364e48e | ||
|
|
bc5d142920 | ||
|
|
9b3425d801 | ||
|
|
5842127c2b | ||
|
|
193eba3c58 | ||
|
|
338e50e255 | ||
|
|
65500d7a53 | ||
|
|
0833cd5443 | ||
|
|
e8c844ead7 | ||
|
|
1d5f2dc5f0 | ||
|
|
9a36154564 | ||
|
|
5f8ae74ea5 | ||
|
|
da8e7c1b01 | ||
|
|
f513b98f15 | ||
|
|
80cef6dde7 | ||
|
|
2d504f281e | ||
|
|
fc34c98f0a | ||
|
|
1cc020efdd | ||
|
|
bb33e99e33 | ||
|
|
6c70b34ae6 | ||
|
|
960d9f6215 | ||
|
|
085f08ec3b | ||
|
|
2a8722f56e | ||
|
|
4372b9bbc2 | ||
|
|
db81319dc4 | ||
|
|
644f7a0413 | ||
|
|
b93ff19ebe | ||
|
|
4c007cb47d | ||
|
|
c9e9a45288 | ||
|
|
d44a5491f5 | ||
|
|
289cf5b974 | ||
|
|
e207622a70 | ||
|
|
38d9d9fda5 | ||
|
|
996895e775 | ||
|
|
0675e53f2a | ||
|
|
e492cec7d7 | ||
|
|
92fe1e7554 | ||
|
|
7b080600f6 | ||
|
|
71f9667bc0 | ||
|
|
2bf548f8b9 | ||
|
|
15fd4545aa | ||
|
|
7f6c4ad6da | ||
|
|
2be12b4ea4 | ||
|
|
baebdd9870 | ||
|
|
d1648bb709 | ||
|
|
a4a394a18d | ||
|
|
951fdc902a | ||
|
|
18ffa06d70 | ||
|
|
d28f152d78 | ||
|
|
1e3e9ea84a | ||
|
|
5b42e59d65 | ||
|
|
1882467554 | ||
|
|
f1cd48e9d0 | ||
|
|
d1fd84617b | ||
|
|
03fcf82b26 | ||
|
|
f75e1bf3df | ||
|
|
554a6f59bd | ||
|
|
68d41cf028 | ||
|
|
5defeca590 | ||
|
|
4d94418124 | ||
|
|
6c258f056a | ||
|
|
87af1f1319 | ||
|
|
3f9badd4b9 | ||
|
|
06de770c4d | ||
|
|
b5948441ac | ||
|
|
acbab69f12 |
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.ruff_cache/
|
||||||
|
.pytest_cache/
|
||||||
@@ -12,25 +12,23 @@ jobs:
|
|||||||
name: lint ${{ matrix.python-version }}
|
name: lint ${{ matrix.python-version }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ '3.10', '3.11', '3.12' ]
|
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
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v3
|
uses: astral-sh/setup-uv@v5
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
run: uv python install ${{ matrix.python-version }}
|
run: uv python install ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
|
||||||
uv sync --only-group lint
|
uv sync --only-group lint
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
chmod 755 scripts/lint.sh
|
chmod 755 backend/scripts/lint.sh
|
||||||
./scripts/lint.sh
|
./backend/scripts/lint.sh
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.cursor/
|
||||||
|
.DS_Store
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.python-version
|
||||||
|
.ruff_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
|||||||
+4
-14
@@ -1,37 +1,27 @@
|
|||||||
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-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.9.5
|
rev: v0.14.0
|
||||||
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.5.29
|
rev: 0.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
args:
|
|
||||||
- '--directory'
|
|
||||||
- 'backend'
|
|
||||||
files: backend/uv\.lock$
|
|
||||||
- id: uv-export
|
- id: uv-export
|
||||||
args:
|
args:
|
||||||
- '--directory'
|
|
||||||
- 'backend'
|
|
||||||
- '-o'
|
- '-o'
|
||||||
- 'requirements.txt'
|
- 'requirements.txt'
|
||||||
- '--no-hashes'
|
- '--no-hashes'
|
||||||
files: backend/uv\.lock$
|
|
||||||
|
|||||||
+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"
|
||||||
+902
@@ -0,0 +1,902 @@
|
|||||||
|
<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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[Changes][v1.8.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.8.1"></a>
|
||||||
|
# [v1.8.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.1) - 2025-09-09
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.8.0 by [@wu-clan](https://github.com/wu-clan) in [#772](https://github.com/fastapi-practices/fastapi_best_architecture/pull/772)
|
||||||
|
* Optimize the celery crontab expression validation by [@yanlingsishao](https://github.com/yanlingsishao) in [#773](https://github.com/fastapi-practices/fastapi_best_architecture/pull/773)
|
||||||
|
* Add bulk update interface for config plugin by [@wu-clan](https://github.com/wu-clan) in [#774](https://github.com/fastapi-practices/fastapi_best_architecture/pull/774)
|
||||||
|
* Optimize email sending config update logic by [@wu-clan](https://github.com/wu-clan) in [#775](https://github.com/fastapi-practices/fastapi_best_architecture/pull/775)
|
||||||
|
* Add test data SQL script for config plugin by [@wu-clan](https://github.com/wu-clan) in [#776](https://github.com/fastapi-practices/fastapi_best_architecture/pull/776)
|
||||||
|
* Update the env parameter value in env file by [@wu-clan](https://github.com/wu-clan) in [#777](https://github.com/fastapi-practices/fastapi_best_architecture/pull/777)
|
||||||
|
* Fix the docker compose env configuration file by [@wu-clan](https://github.com/wu-clan) in [#778](https://github.com/fastapi-practices/fastapi_best_architecture/pull/778)
|
||||||
|
* Fix redis client not close after plugin parse by [@wu-clan](https://github.com/wu-clan) in [#780](https://github.com/fastapi-practices/fastapi_best_architecture/pull/780)
|
||||||
|
* Fix the celery beat distributed lock timeout by [@wu-clan](https://github.com/wu-clan) in [#779](https://github.com/fastapi-practices/fastapi_best_architecture/pull/779)
|
||||||
|
* Fix the plugin model object detection logic by [@wuyao4](https://github.com/wuyao4) in [#782](https://github.com/fastapi-practices/fastapi_best_architecture/pull/782)
|
||||||
|
* Update the celery task result table creation logic by [@wu-clan](https://github.com/wu-clan) in [#783](https://github.com/fastapi-practices/fastapi_best_architecture/pull/783)
|
||||||
|
* Fix code generation template params and filename by [@wu-clan](https://github.com/wu-clan) in [#784](https://github.com/fastapi-practices/fastapi_best_architecture/pull/784)
|
||||||
|
* Fix CLI openapi url in production environment by [@siyue-wang](https://github.com/siyue-wang) in [#785](https://github.com/fastapi-practices/fastapi_best_architecture/pull/785)
|
||||||
|
* Update the model datetime column type to custom by [@wu-clan](https://github.com/wu-clan) in [#786](https://github.com/fastapi-practices/fastapi_best_architecture/pull/786)
|
||||||
|
* Update the opera log exception message record by [@wu-clan](https://github.com/wu-clan) in [#788](https://github.com/fastapi-practices/fastapi_best_architecture/pull/788)
|
||||||
|
* Update the handling of CORS 500 status code by [@wu-clan](https://github.com/wu-clan) in [#789](https://github.com/fastapi-practices/fastapi_best_architecture/pull/789)
|
||||||
|
* Add the Google OAuth2 login by [@wu-clan](https://github.com/wu-clan) in [#790](https://github.com/fastapi-practices/fastapi_best_architecture/pull/790)
|
||||||
|
* Fix the tzinfo comparison in the timezone type by [@IAseven](https://github.com/IAseven) in [#787](https://github.com/fastapi-practices/fastapi_best_architecture/pull/787)
|
||||||
|
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#791](https://github.com/fastapi-practices/fastapi_best_architecture/pull/791)
|
||||||
|
* Update the notice model and pagination by [@wu-clan](https://github.com/wu-clan) in [#792](https://github.com/fastapi-practices/fastapi_best_architecture/pull/792)
|
||||||
|
* Add the dict type query all interface by [@wu-clan](https://github.com/wu-clan) in [#794](https://github.com/fastapi-practices/fastapi_best_architecture/pull/794)
|
||||||
|
* Fix sql script syntax error in config plugin by [@wu-clan](https://github.com/wu-clan) in [#793](https://github.com/fastapi-practices/fastapi_best_architecture/pull/793)
|
||||||
|
* Fix the psycopg dependency naming by [@wu-clan](https://github.com/wu-clan) in [#795](https://github.com/fastapi-practices/fastapi_best_architecture/pull/795)
|
||||||
|
* Add schema for the tree data by [@wu-clan](https://github.com/wu-clan) in [#796](https://github.com/fastapi-practices/fastapi_best_architecture/pull/796)
|
||||||
|
* Add the code generator to the CLI by [@wu-clan](https://github.com/wu-clan) in [#798](https://github.com/fastapi-practices/fastapi_best_architecture/pull/798)
|
||||||
|
* Optimize the code generation interaction logic by [@wu-clan](https://github.com/wu-clan) in [#799](https://github.com/fastapi-practices/fastapi_best_architecture/pull/799)
|
||||||
|
* Fix console import in main module by [@wu-clan](https://github.com/wu-clan) in [#800](https://github.com/fastapi-practices/fastapi_best_architecture/pull/800)
|
||||||
|
* Fix the subprocess check call failed in windows by [@yzbf-lin](https://github.com/yzbf-lin) in [#802](https://github.com/fastapi-practices/fastapi_best_architecture/pull/802)
|
||||||
|
* Fix alembic migration file missing import by [@wu-clan](https://github.com/wu-clan) in [#803](https://github.com/fastapi-practices/fastapi_best_architecture/pull/803)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@yanlingsishao](https://github.com/yanlingsishao) made their first contribution in [#773](https://github.com/fastapi-practices/fastapi_best_architecture/pull/773)
|
||||||
|
* [@wuyao4](https://github.com/wuyao4) made their first contribution in [#782](https://github.com/fastapi-practices/fastapi_best_architecture/pull/782)
|
||||||
|
* [@siyue-wang](https://github.com/siyue-wang) made their first contribution in [#785](https://github.com/fastapi-practices/fastapi_best_architecture/pull/785)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.8.0...v1.8.1
|
||||||
|
|
||||||
|
[Changes][v1.8.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.8.0"></a>
|
||||||
|
# [v1.8.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.8.0) - 2025-08-15
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.7.0 by [@wu-clan](https://github.com/wu-clan) in [#729](https://github.com/fastapi-practices/fastapi_best_architecture/pull/729)
|
||||||
|
* Simplify task crontab expression validation by [@wu-clan](https://github.com/wu-clan) in [#733](https://github.com/fastapi-practices/fastapi_best_architecture/pull/733)
|
||||||
|
* Add distributed lock for scheduled task by [@wu-clan](https://github.com/wu-clan) in [#732](https://github.com/fastapi-practices/fastapi_best_architecture/pull/732)
|
||||||
|
* Update the default cache period for userinfo by [@wu-clan](https://github.com/wu-clan) in [#734](https://github.com/fastapi-practices/fastapi_best_architecture/pull/734)
|
||||||
|
* Fix celery CLI option to required by [@wu-clan](https://github.com/wu-clan) in [#737](https://github.com/fastapi-practices/fastapi_best_architecture/pull/737)
|
||||||
|
* Add auth whitelist regular expression config by [@wu-clan](https://github.com/wu-clan) in [#738](https://github.com/fastapi-practices/fastapi_best_architecture/pull/738)
|
||||||
|
* Fix the opera log field encryption by [@wu-clan](https://github.com/wu-clan) in [#739](https://github.com/fastapi-practices/fastapi_best_architecture/pull/739)
|
||||||
|
* Update the OAuth2 login password policy by [@wu-clan](https://github.com/wu-clan) in [#741](https://github.com/fastapi-practices/fastapi_best_architecture/pull/741)
|
||||||
|
* Add update support for user email and phone by [@wu-clan](https://github.com/wu-clan) in [#742](https://github.com/fastapi-practices/fastapi_best_architecture/pull/742)
|
||||||
|
* Fix the error trigger when model auto import by [@wu-clan](https://github.com/wu-clan) in [#743](https://github.com/fastapi-practices/fastapi_best_architecture/pull/743)
|
||||||
|
* Simplify the plugin status update logic by [@wu-clan](https://github.com/wu-clan) in [#744](https://github.com/fastapi-practices/fastapi_best_architecture/pull/744)
|
||||||
|
* Add some interfaces for user profiles by [@wu-clan](https://github.com/wu-clan) in [#745](https://github.com/fastapi-practices/fastapi_best_architecture/pull/745)
|
||||||
|
* Add schedule task demo that contains params by [@wu-clan](https://github.com/wu-clan) in [#746](https://github.com/fastapi-practices/fastapi_best_architecture/pull/746)
|
||||||
|
* Fix the kwargs params of schedule task by [@wu-clan](https://github.com/wu-clan) in [#747](https://github.com/fastapi-practices/fastapi_best_architecture/pull/747)
|
||||||
|
* Refactor code generation files and routes by [@wu-clan](https://github.com/wu-clan) in [#748](https://github.com/fastapi-practices/fastapi_best_architecture/pull/748)
|
||||||
|
* Refactor task routes and add control routes by [@wu-clan](https://github.com/wu-clan) in [#749](https://github.com/fastapi-practices/fastapi_best_architecture/pull/749)
|
||||||
|
* Fix message format in validation exception handler by [@wu-clan](https://github.com/wu-clan) in [#755](https://github.com/fastapi-practices/fastapi_best_architecture/pull/755)
|
||||||
|
* Update the opera log desensitization method by [@wu-clan](https://github.com/wu-clan) in [#756](https://github.com/fastapi-practices/fastapi_best_architecture/pull/756)
|
||||||
|
* Add business pagination in the code generator by [@wu-clan](https://github.com/wu-clan) in [#757](https://github.com/fastapi-practices/fastapi_best_architecture/pull/757)
|
||||||
|
* Optimize the data sort logic of tree nodes by [@wu-clan](https://github.com/wu-clan) in [#758](https://github.com/fastapi-practices/fastapi_best_architecture/pull/758)
|
||||||
|
* Update log output config and format by [@wu-clan](https://github.com/wu-clan) in [#759](https://github.com/fastapi-practices/fastapi_best_architecture/pull/759)
|
||||||
|
* Update the naming of table creation function by [@wu-clan](https://github.com/wu-clan) in [#760](https://github.com/fastapi-practices/fastapi_best_architecture/pull/760)
|
||||||
|
* Optimize the opera log storage logic through queue by [@IAseven](https://github.com/IAseven) in [#750](https://github.com/fastapi-practices/fastapi_best_architecture/pull/750)
|
||||||
|
* Optimize naming and preview in code generation by [@wu-clan](https://github.com/wu-clan) in [#764](https://github.com/fastapi-practices/fastapi_best_architecture/pull/764)
|
||||||
|
* Update the description for the run file by [@wu-clan](https://github.com/wu-clan) in [#766](https://github.com/fastapi-practices/fastapi_best_architecture/pull/766)
|
||||||
|
* Optimize the timezone datetime return encoder by [@wu-clan](https://github.com/wu-clan) in [#767](https://github.com/fastapi-practices/fastapi_best_architecture/pull/767)
|
||||||
|
* Update the content layout of the config file by [@wu-clan](https://github.com/wu-clan) in [#768](https://github.com/fastapi-practices/fastapi_best_architecture/pull/768)
|
||||||
|
* Add a standalone email sending plugin by [@wu-clan](https://github.com/wu-clan) in [#769](https://github.com/fastapi-practices/fastapi_best_architecture/pull/769)
|
||||||
|
* Add i18n support for response message by [@downdawn](https://github.com/downdawn) in [#753](https://github.com/fastapi-practices/fastapi_best_architecture/pull/753)
|
||||||
|
* Update the menu title in SQL scripts by [@wu-clan](https://github.com/wu-clan) in [#770](https://github.com/fastapi-practices/fastapi_best_architecture/pull/770)
|
||||||
|
* Update the version number to 1.8.0 by [@wu-clan](https://github.com/wu-clan) in [#771](https://github.com/fastapi-practices/fastapi_best_architecture/pull/771)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@IAseven](https://github.com/IAseven) made their first contribution in [#750](https://github.com/fastapi-practices/fastapi_best_architecture/pull/750)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.7.0...v1.8.0
|
||||||
|
|
||||||
|
[Changes][v1.8.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.7.0"></a>
|
||||||
|
# [v1.7.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.7.0) - 2025-07-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.6.0 by [@wu-clan](https://github.com/wu-clan) in [#703](https://github.com/fastapi-practices/fastapi_best_architecture/pull/703)
|
||||||
|
* Update the CLI to be executed async by [@wu-clan](https://github.com/wu-clan) in [#704](https://github.com/fastapi-practices/fastapi_best_architecture/pull/704)
|
||||||
|
* Fix the code generation schema template by [@wu-clan](https://github.com/wu-clan) in [#706](https://github.com/fastapi-practices/fastapi_best_architecture/pull/706)
|
||||||
|
* Replace gunicorn deployment to granian by [@wu-clan](https://github.com/wu-clan) in [#705](https://github.com/fastapi-practices/fastapi_best_architecture/pull/705)
|
||||||
|
* Fix the code generation delete schema template by [@wu-clan](https://github.com/wu-clan) in [#708](https://github.com/fastapi-practices/fastapi_best_architecture/pull/708)
|
||||||
|
* Update the refresh token verify mechanism by [@wu-clan](https://github.com/wu-clan) in [#710](https://github.com/fastapi-practices/fastapi_best_architecture/pull/710)
|
||||||
|
* Update the reload excludes for CLI run by [@wu-clan](https://github.com/wu-clan) in [#709](https://github.com/fastapi-practices/fastapi_best_architecture/pull/709)
|
||||||
|
* Add CLI support for execute sql scripts by [@wu-clan](https://github.com/wu-clan) in [#711](https://github.com/fastapi-practices/fastapi_best_architecture/pull/711)
|
||||||
|
* Update the granian env to command params by [@wu-clan](https://github.com/wu-clan) in [#712](https://github.com/fastapi-practices/fastapi_best_architecture/pull/712)
|
||||||
|
* Update the middleware logging accuracy by [@wu-clan](https://github.com/wu-clan) in [#713](https://github.com/fastapi-practices/fastapi_best_architecture/pull/713)
|
||||||
|
* Update the log output default style by [@wu-clan](https://github.com/wu-clan) in [#714](https://github.com/fastapi-practices/fastapi_best_architecture/pull/714)
|
||||||
|
* Optimize the analysis of get plugins by [@wu-clan](https://github.com/wu-clan) in [#716](https://github.com/fastapi-practices/fastapi_best_architecture/pull/716)
|
||||||
|
* Simplify user permission database queries by [@wu-clan](https://github.com/wu-clan) in [#717](https://github.com/fastapi-practices/fastapi_best_architecture/pull/717)
|
||||||
|
* Update the CLI startup service mode by [@wu-clan](https://github.com/wu-clan) in [#718](https://github.com/fastapi-practices/fastapi_best_architecture/pull/718)
|
||||||
|
* Add support for celery dynamic tasks by [@wu-clan](https://github.com/wu-clan) in [#715](https://github.com/fastapi-practices/fastapi_best_architecture/pull/715)
|
||||||
|
* Fix the celery task scheduler query by [@wu-clan](https://github.com/wu-clan) in [#719](https://github.com/fastapi-practices/fastapi_best_architecture/pull/719)
|
||||||
|
* Update the celery task comment and name by [@wu-clan](https://github.com/wu-clan) in [#720](https://github.com/fastapi-practices/fastapi_best_architecture/pull/720)
|
||||||
|
* Optimize celery integrations and events by [@wu-clan](https://github.com/wu-clan) in [#721](https://github.com/fastapi-practices/fastapi_best_architecture/pull/721)
|
||||||
|
* Simplify celery task crontab config by [@wu-clan](https://github.com/wu-clan) in [#722](https://github.com/fastapi-practices/fastapi_best_architecture/pull/722)
|
||||||
|
* Delete the default value of schema enum data by [@wu-clan](https://github.com/wu-clan) in [#723](https://github.com/fastapi-practices/fastapi_best_architecture/pull/723)
|
||||||
|
* Fix the parsing of execution task params by [@wu-clan](https://github.com/wu-clan) in [#725](https://github.com/fastapi-practices/fastapi_best_architecture/pull/725)
|
||||||
|
* Bump granian from 2.4.0 to 2.4.2 by [@wu-clan](https://github.com/wu-clan) in [#727](https://github.com/fastapi-practices/fastapi_best_architecture/pull/727)
|
||||||
|
* Add CLI support for startup celery services by [@wu-clan](https://github.com/wu-clan) in [#724](https://github.com/fastapi-practices/fastapi_best_architecture/pull/724)
|
||||||
|
* Fix login and operation log clearing by [@wu-clan](https://github.com/wu-clan) in [#728](https://github.com/fastapi-practices/fastapi_best_architecture/pull/728)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
|
||||||
|
|
||||||
|
[Changes][v1.7.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.6.0"></a>
|
||||||
|
# [v1.6.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.6.0) - 2025-06-30
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[Changes][v1.4.2]
|
||||||
|
|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.0 by [@wu-clan](https://github.com/wu-clan) in [#621](https://github.com/fastapi-practices/fastapi_best_architecture/pull/621)
|
||||||
|
* Update the menu path and type columns by [@wu-clan](https://github.com/wu-clan) in [#622](https://github.com/fastapi-practices/fastapi_best_architecture/pull/622)
|
||||||
|
* Add the deepwiki badge to README by [@wu-clan](https://github.com/wu-clan) in [#623](https://github.com/fastapi-practices/fastapi_best_architecture/pull/623)
|
||||||
|
* Refactor the system token to online users by [@wu-clan](https://github.com/wu-clan) in [#624](https://github.com/fastapi-practices/fastapi_best_architecture/pull/624)
|
||||||
|
* Update the token check for logout interface by [@wu-clan](https://github.com/wu-clan) in [#625](https://github.com/fastapi-practices/fastapi_best_architecture/pull/625)
|
||||||
|
* Update the token decode for logout interface by [@wu-clan](https://github.com/wu-clan) in [#629](https://github.com/fastapi-practices/fastapi_best_architecture/pull/629)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
||||||
|
|
||||||
|
[Changes][v1.4.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.4.0"></a>
|
||||||
|
# [v1.4.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.0) - 2025-05-22
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.3.0 by [@wu-clan](https://github.com/wu-clan) in [#605](https://github.com/fastapi-practices/fastapi_best_architecture/pull/605)
|
||||||
|
* Add new plugin status check interface by [@wu-clan](https://github.com/wu-clan) in [#606](https://github.com/fastapi-practices/fastapi_best_architecture/pull/606)
|
||||||
|
* Update the new plugin status to changed by [@wu-clan](https://github.com/wu-clan) in [#607](https://github.com/fastapi-practices/fastapi_best_architecture/pull/607)
|
||||||
|
* Fix the task result schema param type by [@wu-clan](https://github.com/wu-clan) in [#611](https://github.com/fastapi-practices/fastapi_best_architecture/pull/611)
|
||||||
|
* Fix the plugin status update logic by [@wu-clan](https://github.com/wu-clan) in [#613](https://github.com/fastapi-practices/fastapi_best_architecture/pull/613)
|
||||||
|
* Update uninstall and build plugin api method by [@wu-clan](https://github.com/wu-clan) in [#614](https://github.com/fastapi-practices/fastapi_best_architecture/pull/614)
|
||||||
|
* Fix non-asyncio nested async IO by [@wu-clan](https://github.com/wu-clan) in [#610](https://github.com/fastapi-practices/fastapi_best_architecture/pull/610)
|
||||||
|
* Update the build plugin api params by [@wu-clan](https://github.com/wu-clan) in [#615](https://github.com/fastapi-practices/fastapi_best_architecture/pull/615)
|
||||||
|
* Update uv installation in docker deploy by [@wu-clan](https://github.com/wu-clan) in [#619](https://github.com/fastapi-practices/fastapi_best_architecture/pull/619)
|
||||||
|
* Update the OAuth2 module to plugin by [@wu-clan](https://github.com/wu-clan) in [#620](https://github.com/fastapi-practices/fastapi_best_architecture/pull/620)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
||||||
|
|
||||||
|
[Changes][v1.4.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.3.0"></a>
|
||||||
|
# [v1.3.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.3.0) - 2025-05-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.2.0 by [@wu-clan](https://github.com/wu-clan) in [#598](https://github.com/fastapi-practices/fastapi_best_architecture/pull/598)
|
||||||
|
* Simplify apps and plugins config method by [@wu-clan](https://github.com/wu-clan) in [#600](https://github.com/fastapi-practices/fastapi_best_architecture/pull/600)
|
||||||
|
* Add plugin info config and interfaces by [@wu-clan](https://github.com/wu-clan) in [#601](https://github.com/fastapi-practices/fastapi_best_architecture/pull/601)
|
||||||
|
* Fix the fastapi cli startup event loop by [@wu-clan](https://github.com/wu-clan) in [#602](https://github.com/fastapi-practices/fastapi_best_architecture/pull/602)
|
||||||
|
* Optimize the zip plugin install logic by [@wu-clan](https://github.com/wu-clan) in [#603](https://github.com/fastapi-practices/fastapi_best_architecture/pull/603)
|
||||||
|
* Update the casbin RBAC module path by [@wu-clan](https://github.com/wu-clan) in [#604](https://github.com/fastapi-practices/fastapi_best_architecture/pull/604)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
||||||
|
|
||||||
|
[Changes][v1.3.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.2.0"></a>
|
||||||
|
# [v1.2.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.2.0) - 2025-05-01
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.1.2 by [@wu-clan](https://github.com/wu-clan) in [#589](https://github.com/fastapi-practices/fastapi_best_architecture/pull/589)
|
||||||
|
* Update code generator table columns by [@wu-clan](https://github.com/wu-clan) in [#590](https://github.com/fastapi-practices/fastapi_best_architecture/pull/590)
|
||||||
|
* Update the default RBAC solution by [@wu-clan](https://github.com/wu-clan) in [#593](https://github.com/fastapi-practices/fastapi_best_architecture/pull/593)
|
||||||
|
* Optimize the server information retrieval by [@wu-clan](https://github.com/wu-clan) in [#595](https://github.com/fastapi-practices/fastapi_best_architecture/pull/595)
|
||||||
|
* Refactor the data rule to scope rule by [@wu-clan](https://github.com/wu-clan) in [#596](https://github.com/fastapi-practices/fastapi_best_architecture/pull/596)
|
||||||
|
* Update the SQL script for creat tables by [@wu-clan](https://github.com/wu-clan) in [#597](https://github.com/fastapi-practices/fastapi_best_architecture/pull/597)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
|
||||||
|
|
||||||
|
[Changes][v1.2.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.1.2"></a>
|
||||||
|
# [v1.1.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.2) - 2025-04-23
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.1.1 by [@wu-clan](https://github.com/wu-clan) in [#583](https://github.com/fastapi-practices/fastapi_best_architecture/pull/583)
|
||||||
|
* Fix the condition to query menu by title by [@wu-clan](https://github.com/wu-clan) in [#584](https://github.com/fastapi-practices/fastapi_best_architecture/pull/584)
|
||||||
|
* Fix cache cleanup when updating role menu by [@wu-clan](https://github.com/wu-clan) in [#585](https://github.com/fastapi-practices/fastapi_best_architecture/pull/585)
|
||||||
|
* Optimize the userinfo cache cleaning logic by [@wu-clan](https://github.com/wu-clan) in [#586](https://github.com/fastapi-practices/fastapi_best_architecture/pull/586)
|
||||||
|
* Bump fastapi pagination from 0.12.34 to 0.13.0 by [@wu-clan](https://github.com/wu-clan) in [#587](https://github.com/fastapi-practices/fastapi_best_architecture/pull/587)
|
||||||
|
* Update the routing style of the task app by [@wu-clan](https://github.com/wu-clan) in [#588](https://github.com/fastapi-practices/fastapi_best_architecture/pull/588)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
|
||||||
|
|
||||||
|
[Changes][v1.1.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.1.1"></a>
|
||||||
|
# [v1.1.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.1) - 2025-04-18
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.1.0 by [@wu-clan](https://github.com/wu-clan) in [#580](https://github.com/fastapi-practices/fastapi_best_architecture/pull/580)
|
||||||
|
* Fix the plugin system route injection by [@wu-clan](https://github.com/wu-clan) in [#581](https://github.com/fastapi-practices/fastapi_best_architecture/pull/581)
|
||||||
|
* Fix list query in the dict plugin by [@wu-clan](https://github.com/wu-clan) in [#582](https://github.com/fastapi-practices/fastapi_best_architecture/pull/582)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
|
||||||
|
|
||||||
|
[Changes][v1.1.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.1.0"></a>
|
||||||
|
# [v1.1.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.0) - 2025-04-17
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.0.5 by [@wu-clan](https://github.com/wu-clan) in [#572](https://github.com/fastapi-practices/fastapi_best_architecture/pull/572)
|
||||||
|
* Update the default value for some functions by [@wu-clan](https://github.com/wu-clan) in [#573](https://github.com/fastapi-practices/fastapi_best_architecture/pull/573)
|
||||||
|
* Optimize the file structure of code generator by [@wu-clan](https://github.com/wu-clan) in [#574](https://github.com/fastapi-practices/fastapi_best_architecture/pull/574)
|
||||||
|
* Update casbin RBAC verify to dynamic import by [@wu-clan](https://github.com/wu-clan) in [#576](https://github.com/fastapi-practices/fastapi_best_architecture/pull/576)
|
||||||
|
* Update unique columns in dict models by [@wu-clan](https://github.com/wu-clan) in [#577](https://github.com/fastapi-practices/fastapi_best_architecture/pull/577)
|
||||||
|
* Update the code generator to plugin by [@wu-clan](https://github.com/wu-clan) in [#578](https://github.com/fastapi-practices/fastapi_best_architecture/pull/578)
|
||||||
|
* Fix avatar url type of update avatar by [@huyuwei1996](https://github.com/huyuwei1996) in [#575](https://github.com/fastapi-practices/fastapi_best_architecture/pull/575)
|
||||||
|
* Update code generator file and table naming by [@wu-clan](https://github.com/wu-clan) in [#579](https://github.com/fastapi-practices/fastapi_best_architecture/pull/579)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
|
||||||
|
|
||||||
|
[Changes][v1.1.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.0.5"></a>
|
||||||
|
# [v1.0.5](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.5) - 2025-04-09
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.0.4 by [@wu-clan](https://github.com/wu-clan) in [#558](https://github.com/fastapi-practices/fastapi_best_architecture/pull/558)
|
||||||
|
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#559](https://github.com/fastapi-practices/fastapi_best_architecture/pull/559)
|
||||||
|
* Add python 3.13 to GitHub ci by [@wu-clan](https://github.com/wu-clan) in [#560](https://github.com/fastapi-practices/fastapi_best_architecture/pull/560)
|
||||||
|
* Update the system config to plugin by [@wu-clan](https://github.com/wu-clan) in [#561](https://github.com/fastapi-practices/fastapi_best_architecture/pull/561)
|
||||||
|
* Update dict data and type to plugin by [@wu-clan](https://github.com/wu-clan) in [#562](https://github.com/fastapi-practices/fastapi_best_architecture/pull/562)
|
||||||
|
* Update menu and add vben5 compatibility by [@wu-clan](https://github.com/wu-clan) in [#563](https://github.com/fastapi-practices/fastapi_best_architecture/pull/563)
|
||||||
|
* Update the vben5 tree data structure by [@wu-clan](https://github.com/wu-clan) in [#564](https://github.com/fastapi-practices/fastapi_best_architecture/pull/564)
|
||||||
|
* Update custom validation error messages by [@wu-clan](https://github.com/wu-clan) in [#566](https://github.com/fastapi-practices/fastapi_best_architecture/pull/566)
|
||||||
|
* Update the number of pagination le by [@wu-clan](https://github.com/wu-clan) in [#565](https://github.com/fastapi-practices/fastapi_best_architecture/pull/565)
|
||||||
|
* Fix the login password verification by [@wu-clan](https://github.com/wu-clan) in [#568](https://github.com/fastapi-practices/fastapi_best_architecture/pull/568)
|
||||||
|
* Fix the failure hook of celery task by [@wu-clan](https://github.com/wu-clan) in [#569](https://github.com/fastapi-practices/fastapi_best_architecture/pull/569)
|
||||||
|
* Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 by [@wu-clan](https://github.com/wu-clan) in [#570](https://github.com/fastapi-practices/fastapi_best_architecture/pull/570)
|
||||||
|
* Fix the log rule in gitignore by [@wu-clan](https://github.com/wu-clan) in [#571](https://github.com/fastapi-practices/fastapi_best_architecture/pull/571)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
|
||||||
|
|
||||||
|
[Changes][v1.0.5]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.0.4"></a>
|
||||||
|
# [v1.0.4](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.4) - 2025-03-28
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.0.3 by [@wu-clan](https://github.com/wu-clan) in [#543](https://github.com/fastapi-practices/fastapi_best_architecture/pull/543)
|
||||||
|
* Updated the backend contribution guide by [@wu-clan](https://github.com/wu-clan) in [#544](https://github.com/fastapi-practices/fastapi_best_architecture/pull/544)
|
||||||
|
* Optimize the return of relationship interfaces by [@wu-clan](https://github.com/wu-clan) in [#545](https://github.com/fastapi-practices/fastapi_best_architecture/pull/545)
|
||||||
|
* Optimize the dynamic import of data models by [@wu-clan](https://github.com/wu-clan) in [#546](https://github.com/fastapi-practices/fastapi_best_architecture/pull/546)
|
||||||
|
* Update git and docker ignore files by [@wu-clan](https://github.com/wu-clan) in [#547](https://github.com/fastapi-practices/fastapi_best_architecture/pull/547)
|
||||||
|
* Optimize dependencies to reduce package size by [@wu-clan](https://github.com/wu-clan) in [#548](https://github.com/fastapi-practices/fastapi_best_architecture/pull/548)
|
||||||
|
* Fix async install plugin dependencies for windows by [@wu-clan](https://github.com/wu-clan) in [#549](https://github.com/fastapi-practices/fastapi_best_architecture/pull/549)
|
||||||
|
* Fix return schema of the config api by [@wu-clan](https://github.com/wu-clan) in [#551](https://github.com/fastapi-practices/fastapi_best_architecture/pull/551)
|
||||||
|
* Optimize schemas with model relationships by [@wu-clan](https://github.com/wu-clan) in [#552](https://github.com/fastapi-practices/fastapi_best_architecture/pull/552)
|
||||||
|
* Fix filters for opera log query list by [@ThankCat](https://github.com/ThankCat) in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
|
||||||
|
* Fix the celery env in docker compose by [@wu-clan](https://github.com/wu-clan) in [#555](https://github.com/fastapi-practices/fastapi_best_architecture/pull/555)
|
||||||
|
* Update volumes of redis in docker compose by [@wu-clan](https://github.com/wu-clan) in [#556](https://github.com/fastapi-practices/fastapi_best_architecture/pull/556)
|
||||||
|
* Fix the query for the sub department by [@PoetryL](https://github.com/PoetryL) in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
|
||||||
|
* Optimize codes and comments with cursor by [@wu-clan](https://github.com/wu-clan) in [#550](https://github.com/fastapi-practices/fastapi_best_architecture/pull/550)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@ThankCat](https://github.com/ThankCat) made their first contribution in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
|
||||||
|
* [@PoetryL](https://github.com/PoetryL) made their first contribution in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
|
||||||
|
|
||||||
|
[Changes][v1.0.4]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.0.3"></a>
|
||||||
|
# [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.0.2 by [@wu-clan](https://github.com/wu-clan) in [#536](https://github.com/fastapi-practices/fastapi_best_architecture/pull/536)
|
||||||
|
* Update docker scripts in backend README by [@wu-clan](https://github.com/wu-clan) in [#537](https://github.com/fastapi-practices/fastapi_best_architecture/pull/537)
|
||||||
|
* Refactor toml and dependencies file dir by [@wu-clan](https://github.com/wu-clan) in [#538](https://github.com/fastapi-practices/fastapi_best_architecture/pull/538)
|
||||||
|
* Fix typos in Dockerfile comments by [@huyuwei1996](https://github.com/huyuwei1996) in [#539](https://github.com/fastapi-practices/fastapi_best_architecture/pull/539)
|
||||||
|
* Fix Dockerfile mounts for dependency installation by [@huyuwei1996](https://github.com/huyuwei1996) in [#540](https://github.com/fastapi-practices/fastapi_best_architecture/pull/540)
|
||||||
|
* Add Aliyun mirror to PyPI index in pyproject.toml by [@huyuwei1996](https://github.com/huyuwei1996) in [#541](https://github.com/fastapi-practices/fastapi_best_architecture/pull/541)
|
||||||
|
* Update docker scripts and nginx conf by [@wu-clan](https://github.com/wu-clan) in [#542](https://github.com/fastapi-practices/fastapi_best_architecture/pull/542)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
|
||||||
|
|
||||||
|
[Changes][v1.0.3]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.0.2"></a>
|
||||||
|
# [v1.0.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.2) - 2025-03-01
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.0.1 by [@wu-clan](https://github.com/wu-clan) in [#532](https://github.com/fastapi-practices/fastapi_best_architecture/pull/532)
|
||||||
|
* Fix celery async task worker pool by [@wu-clan](https://github.com/wu-clan) in [#533](https://github.com/fastapi-practices/fastapi_best_architecture/pull/533)
|
||||||
|
* Add log module root and output levels by [@wu-clan](https://github.com/wu-clan) in [#534](https://github.com/fastapi-practices/fastapi_best_architecture/pull/534)
|
||||||
|
* Add plugin related interfaces by [@wu-clan](https://github.com/wu-clan) in [#535](https://github.com/fastapi-practices/fastapi_best_architecture/pull/535)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
|
||||||
|
|
||||||
|
[Changes][v1.0.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.0.1"></a>
|
||||||
|
# [v1.0.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.1) - 2025-02-26
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update the changelog for v1.0.0 by [@wu-clan](https://github.com/wu-clan) in [#524](https://github.com/fastapi-practices/fastapi_best_architecture/pull/524)
|
||||||
|
* Add missing volume config for docker deploy by [@huyuwei1996](https://github.com/huyuwei1996) in [#525](https://github.com/fastapi-practices/fastapi_best_architecture/pull/525)
|
||||||
|
* Add async attrs for sqla mapped base by [@wu-clan](https://github.com/wu-clan) in [#528](https://github.com/fastapi-practices/fastapi_best_architecture/pull/528)
|
||||||
|
* Add sqlalchemy connection pool config by [@wu-clan](https://github.com/wu-clan) in [#529](https://github.com/fastapi-practices/fastapi_best_architecture/pull/529)
|
||||||
|
* Fix the sql script for init data by [@wu-clan](https://github.com/wu-clan) in [#530](https://github.com/fastapi-practices/fastapi_best_architecture/pull/530)
|
||||||
|
* Optimize Dockerfile for faster builds by [@huyuwei1996](https://github.com/huyuwei1996) in [#526](https://github.com/fastapi-practices/fastapi_best_architecture/pull/526)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@huyuwei1996](https://github.com/huyuwei1996) made their first contribution in [#525](https://github.com/fastapi-practices/fastapi_best_architecture/pull/525)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
|
||||||
|
|
||||||
|
[Changes][v1.0.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.0.0"></a>
|
||||||
|
# [v1.0.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.0) - 2025-02-24
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* add base code by [@wu-clan](https://github.com/wu-clan) in [#12](https://github.com/fastapi-practices/fastapi_best_architecture/pull/12)
|
||||||
|
* fix get_user_info func return None [#9](https://github.com/fastapi-practices/fastapi_best_architecture/issues/9) by [@wu-clan](https://github.com/wu-clan) in [#14](https://github.com/fastapi-practices/fastapi_best_architecture/pull/14)
|
||||||
|
* simplify user apis [#11](https://github.com/fastapi-practices/fastapi_best_architecture/issues/11) by [@wu-clan](https://github.com/wu-clan) in [#18](https://github.com/fastapi-practices/fastapi_best_architecture/pull/18)
|
||||||
|
* Add environment variable management and fix Pydantic validation error by [@downdawn](https://github.com/downdawn) in [#15](https://github.com/fastapi-practices/fastapi_best_architecture/pull/15)
|
||||||
|
* update the ruff rules and format the code by [@wu-clan](https://github.com/wu-clan) in [#24](https://github.com/fastapi-practices/fastapi_best_architecture/pull/24)
|
||||||
|
* improving project configuration by [@wu-clan](https://github.com/wu-clan) in [#25](https://github.com/fastapi-practices/fastapi_best_architecture/pull/25)
|
||||||
|
* update to python3.10 by [@downdawn](https://github.com/downdawn) in [#29](https://github.com/fastapi-practices/fastapi_best_architecture/pull/29)
|
||||||
|
* update Dockerfile and docker-compose.yml by [@wu-clan](https://github.com/wu-clan) in [#31](https://github.com/fastapi-practices/fastapi_best_architecture/pull/31)
|
||||||
|
* Update the sub routers setting in the subdirectory by [@wu-clan](https://github.com/wu-clan) in [#32](https://github.com/fastapi-practices/fastapi_best_architecture/pull/32)
|
||||||
|
* add the get project config api by [@wu-clan](https://github.com/wu-clan) in [#33](https://github.com/fastapi-practices/fastapi_best_architecture/pull/33)
|
||||||
|
* add test code by [@wu-clan](https://github.com/wu-clan) in [#37](https://github.com/fastapi-practices/fastapi_best_architecture/pull/37)
|
||||||
|
* fix that the data validation global exception handler does not work by [@wu-clan](https://github.com/wu-clan) in [#40](https://github.com/fastapi-practices/fastapi_best_architecture/pull/40)
|
||||||
|
* Fix the swagger form login structure abnormality by [@wu-clan](https://github.com/wu-clan) in [#46](https://github.com/fastapi-practices/fastapi_best_architecture/pull/46)
|
||||||
|
* Bump starlette from 0.26.1 to 0.27.0 by [@dependabot](https://github.com/dependabot) in [#48](https://github.com/fastapi-practices/fastapi_best_architecture/pull/48)
|
||||||
|
* add rbac authorization by [@wu-clan](https://github.com/wu-clan) in [#41](https://github.com/fastapi-practices/fastapi_best_architecture/pull/41)
|
||||||
|
* Bump fastapi from 0.95.0 to 0.95.2 by [@wu-clan](https://github.com/wu-clan) in [#53](https://github.com/fastapi-practices/fastapi_best_architecture/pull/53)
|
||||||
|
* Update the uniform return method to success by [@wu-clan](https://github.com/wu-clan) in [#55](https://github.com/fastapi-practices/fastapi_best_architecture/pull/55)
|
||||||
|
* add token storage and logout by [@downdawn](https://github.com/downdawn) in [#57](https://github.com/fastapi-practices/fastapi_best_architecture/pull/57)
|
||||||
|
* fix jwt parameter parsing error by [@downdawn](https://github.com/downdawn) in [#61](https://github.com/fastapi-practices/fastapi_best_architecture/pull/61)
|
||||||
|
* add token refreshing mechanism by [@wu-clan](https://github.com/wu-clan) in [#62](https://github.com/fastapi-practices/fastapi_best_architecture/pull/62)
|
||||||
|
* Update uniform return to custom encoder by [@wu-clan](https://github.com/wu-clan) in [#60](https://github.com/fastapi-practices/fastapi_best_architecture/pull/60)
|
||||||
|
* update token default exception return message by [@wu-clan](https://github.com/wu-clan) in [#65](https://github.com/fastapi-practices/fastapi_best_architecture/pull/65)
|
||||||
|
* add English and Chinese README jump links by [@wu-clan](https://github.com/wu-clan) in [#66](https://github.com/fastapi-practices/fastapi_best_architecture/pull/66)
|
||||||
|
* update token refresh expire time rule by [@wu-clan](https://github.com/wu-clan) in [#67](https://github.com/fastapi-practices/fastapi_best_architecture/pull/67)
|
||||||
|
* update the casbin to asynchronous by [@wu-clan](https://github.com/wu-clan) in [#69](https://github.com/fastapi-practices/fastapi_best_architecture/pull/69)
|
||||||
|
* Update tests structure. by [@downdawn](https://github.com/downdawn) in [#68](https://github.com/fastapi-practices/fastapi_best_architecture/pull/68)
|
||||||
|
* Add apis rate limiter by [@wu-clan](https://github.com/wu-clan) in [#72](https://github.com/fastapi-practices/fastapi_best_architecture/pull/72)
|
||||||
|
* add tests exclusion E402 rule by [@wu-clan](https://github.com/wu-clan) in [#73](https://github.com/fastapi-practices/fastapi_best_architecture/pull/73)
|
||||||
|
* update where query by [@downdawn](https://github.com/downdawn) in [#74](https://github.com/fastapi-practices/fastapi_best_architecture/pull/74)
|
||||||
|
* simplify crud method naming by [@wu-clan](https://github.com/wu-clan) in [#75](https://github.com/fastapi-practices/fastapi_best_architecture/pull/75)
|
||||||
|
* add login logs by [@wu-clan](https://github.com/wu-clan) in [#76](https://github.com/fastapi-practices/fastapi_best_architecture/pull/76)
|
||||||
|
* add different log files by [@wu-clan](https://github.com/wu-clan) in [#77](https://github.com/fastapi-practices/fastapi_best_architecture/pull/77)
|
||||||
|
* add offline ip location resolution by [@wu-clan](https://github.com/wu-clan) in [#78](https://github.com/fastapi-practices/fastapi_best_architecture/pull/78)
|
||||||
|
* add api module Interfaces by [@wu-clan](https://github.com/wu-clan) in [#79](https://github.com/fastapi-practices/fastapi_best_architecture/pull/79)
|
||||||
|
* update token handling logic by [@wu-clan](https://github.com/wu-clan) in [#83](https://github.com/fastapi-practices/fastapi_best_architecture/pull/83)
|
||||||
|
* add jwt authentication middleware by [@wu-clan](https://github.com/wu-clan) in [#84](https://github.com/fastapi-practices/fastapi_best_architecture/pull/84)
|
||||||
|
* Fix background task not executed by [@wu-clan](https://github.com/wu-clan) in [#86](https://github.com/fastapi-practices/fastapi_best_architecture/pull/86)
|
||||||
|
* Fix the merge issues by [@wu-clan](https://github.com/wu-clan) in [#87](https://github.com/fastapi-practices/fastapi_best_architecture/pull/87)
|
||||||
|
* Update docker one-click deployment by [@wu-clan](https://github.com/wu-clan) in [#88](https://github.com/fastapi-practices/fastapi_best_architecture/pull/88)
|
||||||
|
* Add role-related interfaces by [@wu-clan](https://github.com/wu-clan) in [#89](https://github.com/fastapi-practices/fastapi_best_architecture/pull/89)
|
||||||
|
* Bump cryptography from 39.0.1 to 41.0.0 by [@dependabot](https://github.com/dependabot) in [#90](https://github.com/fastapi-practices/fastapi_best_architecture/pull/90)
|
||||||
|
* Add assertion error handler. by [@wu-clan](https://github.com/wu-clan) in [#93](https://github.com/fastapi-practices/fastapi_best_architecture/pull/93)
|
||||||
|
* Add operation log related interfaces by [@wu-clan](https://github.com/wu-clan) in [#92](https://github.com/fastapi-practices/fastapi_best_architecture/pull/92)
|
||||||
|
* Fix user authorization lock by [@wu-clan](https://github.com/wu-clan) in [#94](https://github.com/fastapi-practices/fastapi_best_architecture/pull/94)
|
||||||
|
* Fix the opera log cost_time parameter by [@wu-clan](https://github.com/wu-clan) in [#95](https://github.com/fastapi-practices/fastapi_best_architecture/pull/95)
|
||||||
|
* Add os and browser parameters to opera log by [@wu-clan](https://github.com/wu-clan) in [#97](https://github.com/fastapi-practices/fastapi_best_architecture/pull/97)
|
||||||
|
* Uniform schema class naming convention style. by [@wu-clan](https://github.com/wu-clan) in [#98](https://github.com/fastapi-practices/fastapi_best_architecture/pull/98)
|
||||||
|
* Add sync to async decorator support by [@wu-clan](https://github.com/wu-clan) in [#96](https://github.com/fastapi-practices/fastapi_best_architecture/pull/96)
|
||||||
|
* Add department-related interfaces and others by [@wu-clan](https://github.com/wu-clan) in [#101](https://github.com/fastapi-practices/fastapi_best_architecture/pull/101)
|
||||||
|
* Remove useless jwt role_ids by [@downdawn](https://github.com/downdawn) in [#103](https://github.com/fastapi-practices/fastapi_best_architecture/pull/103)
|
||||||
|
* Add departmental status authentication by [@wu-clan](https://github.com/wu-clan) in [#104](https://github.com/fastapi-practices/fastapi_best_architecture/pull/104)
|
||||||
|
* Add casbine-related interfaces by [@wu-clan](https://github.com/wu-clan) in [#107](https://github.com/fastapi-practices/fastapi_best_architecture/pull/107)
|
||||||
|
* Replace aioredis to redis. by [@wu-clan](https://github.com/wu-clan) in [#108](https://github.com/fastapi-practices/fastapi_best_architecture/pull/108)
|
||||||
|
* opera_log_middleware method split by [@downdawn](https://github.com/downdawn) in [#105](https://github.com/fastapi-practices/fastapi_best_architecture/pull/105)
|
||||||
|
* Fix offline parse ip info by [@wu-clan](https://github.com/wu-clan) in [#112](https://github.com/fastapi-practices/fastapi_best_architecture/pull/112)
|
||||||
|
* Update the README document by [@wu-clan](https://github.com/wu-clan) in [#113](https://github.com/fastapi-practices/fastapi_best_architecture/pull/113)
|
||||||
|
* Update development process suggestions by [@wu-clan](https://github.com/wu-clan) in [#114](https://github.com/fastapi-practices/fastapi_best_architecture/pull/114)
|
||||||
|
* Fix log table msg field length by [@wu-clan](https://github.com/wu-clan) in [#117](https://github.com/fastapi-practices/fastapi_best_architecture/pull/117)
|
||||||
|
* Add menu-related interfaces by [@wu-clan](https://github.com/wu-clan) in [#118](https://github.com/fastapi-practices/fastapi_best_architecture/pull/118)
|
||||||
|
* Omitting table names from the autogenerate process by [@downdawn](https://github.com/downdawn) in [#125](https://github.com/fastapi-practices/fastapi_best_architecture/pull/125)
|
||||||
|
* Add login graphic captcha by [@wu-clan](https://github.com/wu-clan) in [#124](https://github.com/fastapi-practices/fastapi_best_architecture/pull/124)
|
||||||
|
* fix the operation log storage exception by [@wu-clan](https://github.com/wu-clan) in [#130](https://github.com/fastapi-practices/fastapi_best_architecture/pull/130)
|
||||||
|
* add dictionary management interface by [@downdawn](https://github.com/downdawn) in [#127](https://github.com/fastapi-practices/fastapi_best_architecture/pull/127)
|
||||||
|
* Update and fix permissions logic by [@wu-clan](https://github.com/wu-clan) in [#129](https://github.com/fastapi-practices/fastapi_best_architecture/pull/129)
|
||||||
|
* Update JWT status detection by [@wu-clan](https://github.com/wu-clan) in [#133](https://github.com/fastapi-practices/fastapi_best_architecture/pull/133)
|
||||||
|
* The level field is deprecated but remained by [@wu-clan](https://github.com/wu-clan) in [#134](https://github.com/fastapi-practices/fastapi_best_architecture/pull/134)
|
||||||
|
* Add system monitoring interface by [@wu-clan](https://github.com/wu-clan) in [#135](https://github.com/fastapi-practices/fastapi_best_architecture/pull/135)
|
||||||
|
* Fix the operation log message error by [@wu-clan](https://github.com/wu-clan) in [#140](https://github.com/fastapi-practices/fastapi_best_architecture/pull/140)
|
||||||
|
* Update the server monitoring interface by [@wu-clan](https://github.com/wu-clan) in [#141](https://github.com/fastapi-practices/fastapi_best_architecture/pull/141)
|
||||||
|
* Update the status field type to int by [@wu-clan](https://github.com/wu-clan) in [#143](https://github.com/fastapi-practices/fastapi_best_architecture/pull/143)
|
||||||
|
* Fix the operation log field type error by [@wu-clan](https://github.com/wu-clan) in [#145](https://github.com/fastapi-practices/fastapi_best_architecture/pull/145)
|
||||||
|
* Fix the exception handler HTTPException type error by [@wu-clan](https://github.com/wu-clan) in [#146](https://github.com/fastapi-practices/fastapi_best_architecture/pull/146)
|
||||||
|
* Add the schema base class by [@wu-clan](https://github.com/wu-clan) in [#148](https://github.com/fastapi-practices/fastapi_best_architecture/pull/148)
|
||||||
|
* Add datetime util by [@wu-clan](https://github.com/wu-clan) in [#149](https://github.com/fastapi-practices/fastapi_best_architecture/pull/149)
|
||||||
|
* Fix permitted exception. by [@downdawn](https://github.com/downdawn) in [#151](https://github.com/fastapi-practices/fastapi_best_architecture/pull/151)
|
||||||
|
* Refactor global datetime to timezone datetime by [@wu-clan](https://github.com/wu-clan) in [#152](https://github.com/fastapi-practices/fastapi_best_architecture/pull/152)
|
||||||
|
* Add processing after password reset by [@wu-clan](https://github.com/wu-clan) in [#154](https://github.com/fastapi-practices/fastapi_best_architecture/pull/154)
|
||||||
|
* Update some routing groups by [@wu-clan](https://github.com/wu-clan) in [#155](https://github.com/fastapi-practices/fastapi_best_architecture/pull/155)
|
||||||
|
* Add task-related interfaces by [@wu-clan](https://github.com/wu-clan) in [#157](https://github.com/fastapi-practices/fastapi_best_architecture/pull/157)
|
||||||
|
* Update the instructions in the readme by [@wu-clan](https://github.com/wu-clan) in [#159](https://github.com/fastapi-practices/fastapi_best_architecture/pull/159)
|
||||||
|
* Update some interface permission checks by [@wu-clan](https://github.com/wu-clan) in [#158](https://github.com/fastapi-practices/fastapi_best_architecture/pull/158)
|
||||||
|
* Add database init sql files by [@wu-clan](https://github.com/wu-clan) in [#160](https://github.com/fastapi-practices/fastapi_best_architecture/pull/160)
|
||||||
|
* Adapt to frontend by [@downdawn](https://github.com/downdawn) in [#162](https://github.com/fastapi-practices/fastapi_best_architecture/pull/162)
|
||||||
|
* Update menu handling logic by [@wu-clan](https://github.com/wu-clan) in [#163](https://github.com/fastapi-practices/fastapi_best_architecture/pull/163)
|
||||||
|
* Bump fastapi from 0.95.2 to 0.99.0 by [@wu-clan](https://github.com/wu-clan) in [#164](https://github.com/fastapi-practices/fastapi_best_architecture/pull/164)
|
||||||
|
* Enable login interface captcha function by [@wu-clan](https://github.com/wu-clan) in [#165](https://github.com/fastapi-practices/fastapi_best_architecture/pull/165)
|
||||||
|
* Fix CORS 500 status code exception by [@wu-clan](https://github.com/wu-clan) in [#167](https://github.com/fastapi-practices/fastapi_best_architecture/pull/167)
|
||||||
|
* Add menu table title field by [@wu-clan](https://github.com/wu-clan) in [#170](https://github.com/fastapi-practices/fastapi_best_architecture/pull/170)
|
||||||
|
* fix tree data algorithms exception by [@downdawn](https://github.com/downdawn) in [#169](https://github.com/fastapi-practices/fastapi_best_architecture/pull/169)
|
||||||
|
* Fix the menu query children exception by [@wu-clan](https://github.com/wu-clan) in [#171](https://github.com/fastapi-practices/fastapi_best_architecture/pull/171)
|
||||||
|
* Custom request rate limit callback function by [@wu-clan](https://github.com/wu-clan) in [#174](https://github.com/fastapi-practices/fastapi_best_architecture/pull/174)
|
||||||
|
* Add demo site mode by [@wu-clan](https://github.com/wu-clan) in [#173](https://github.com/fastapi-practices/fastapi_best_architecture/pull/173)
|
||||||
|
* Add query users by department ID by [@wu-clan](https://github.com/wu-clan) in [#175](https://github.com/fastapi-practices/fastapi_best_architecture/pull/175)
|
||||||
|
* Update monitoring return data by [@wu-clan](https://github.com/wu-clan) in [#176](https://github.com/fastapi-practices/fastapi_best_architecture/pull/176)
|
||||||
|
* Update user role interface to standalone by [@wu-clan](https://github.com/wu-clan) in [#177](https://github.com/fastapi-practices/fastapi_best_architecture/pull/177)
|
||||||
|
* Add get roles related interface by [@wu-clan](https://github.com/wu-clan) in [#178](https://github.com/fastapi-practices/fastapi_best_architecture/pull/178)
|
||||||
|
* Add the role status conditional query by [@wu-clan](https://github.com/wu-clan) in [#181](https://github.com/fastapi-practices/fastapi_best_architecture/pull/181)
|
||||||
|
* Update role menu interface is standalone by [@wu-clan](https://github.com/wu-clan) in [#182](https://github.com/fastapi-practices/fastapi_best_architecture/pull/182)
|
||||||
|
* Add interface to get all menus of a role by [@wu-clan](https://github.com/wu-clan) in [#183](https://github.com/fastapi-practices/fastapi_best_architecture/pull/183)
|
||||||
|
* Fix schema enum condition exception by [@wu-clan](https://github.com/wu-clan) in [#185](https://github.com/fastapi-practices/fastapi_best_architecture/pull/185)
|
||||||
|
* Bump Async SQLAlchemy Adapter from 1.1.0 to 1.2.0 by [@wu-clan](https://github.com/wu-clan) in [#187](https://github.com/fastapi-practices/fastapi_best_architecture/pull/187)
|
||||||
|
* Bump cryptography from 41.0.0 to 41.0.2 by [@dependabot](https://github.com/dependabot) in [#179](https://github.com/fastapi-practices/fastapi_best_architecture/pull/179)
|
||||||
|
* Update SQL files and use them as execution targets by [@wu-clan](https://github.com/wu-clan) in [#188](https://github.com/fastapi-practices/fastapi_best_architecture/pull/188)
|
||||||
|
* Add user password encryption salt by [@wu-clan](https://github.com/wu-clan) in [#191](https://github.com/fastapi-practices/fastapi_best_architecture/pull/191)
|
||||||
|
* Update roles and nickname fields to be optiona by [@wu-clan](https://github.com/wu-clan) in [#190](https://github.com/fastapi-practices/fastapi_best_architecture/pull/190)
|
||||||
|
* Fix casbin async enforcer by [@wu-clan](https://github.com/wu-clan) in [#192](https://github.com/fastapi-practices/fastapi_best_architecture/pull/192)
|
||||||
|
* Add more Casbin related interfaces by [@wu-clan](https://github.com/wu-clan) in [#195](https://github.com/fastapi-practices/fastapi_best_architecture/pull/195)
|
||||||
|
* Update the nickname field creation logic by [@wu-clan](https://github.com/wu-clan) in [#196](https://github.com/fastapi-practices/fastapi_best_architecture/pull/196)
|
||||||
|
* Update the Casbin model matcher rules by [@wu-clan](https://github.com/wu-clan) in [#197](https://github.com/fastapi-practices/fastapi_best_architecture/pull/197)
|
||||||
|
* Add api and casbin related interfaces by [@wu-clan](https://github.com/wu-clan) in [#198](https://github.com/fastapi-practices/fastapi_best_architecture/pull/198)
|
||||||
|
* Update asynccasbin to casbin async api by [@wu-clan](https://github.com/wu-clan) in [#199](https://github.com/fastapi-practices/fastapi_best_architecture/pull/199)
|
||||||
|
* Fix the interface logic for dept details by [@wu-clan](https://github.com/wu-clan) in [#201](https://github.com/fastapi-practices/fastapi_best_architecture/pull/201)
|
||||||
|
* Add ItsDangerous request parameters encryption by [@wu-clan](https://github.com/wu-clan) in [#203](https://github.com/fastapi-practices/fastapi_best_architecture/pull/203)
|
||||||
|
* Add jwt login whitelist by [@downdawn](https://github.com/downdawn) in [#204](https://github.com/fastapi-practices/fastapi_best_architecture/pull/204)
|
||||||
|
* Add ip location cache by [@downdawn](https://github.com/downdawn) in [#205](https://github.com/fastapi-practices/fastapi_best_architecture/pull/205)
|
||||||
|
* Fix int enum class inheritance by [@wu-clan](https://github.com/wu-clan) in [#208](https://github.com/fastapi-practices/fastapi_best_architecture/pull/208)
|
||||||
|
* Fix the task interface return data by [@wu-clan](https://github.com/wu-clan) in [#215](https://github.com/fastapi-practices/fastapi_best_architecture/pull/215)
|
||||||
|
* Update the README document by [@wu-clan](https://github.com/wu-clan) in [#214](https://github.com/fastapi-practices/fastapi_best_architecture/pull/214)
|
||||||
|
* Fix token whitelist and new token storage by [@wu-clan](https://github.com/wu-clan) in [#220](https://github.com/fastapi-practices/fastapi_best_architecture/pull/220)
|
||||||
|
* Optimize role menu authorization logic by [@wu-clan](https://github.com/wu-clan) in [#221](https://github.com/fastapi-practices/fastapi_best_architecture/pull/221)
|
||||||
|
* Simplified query interface returns data serialization by [@wu-clan](https://github.com/wu-clan) in [#219](https://github.com/fastapi-practices/fastapi_best_architecture/pull/219)
|
||||||
|
* Update the global unified response code by [@wu-clan](https://github.com/wu-clan) in [#223](https://github.com/fastapi-practices/fastapi_best_architecture/pull/223)
|
||||||
|
* Fix global unknown exception return by [@wu-clan](https://github.com/wu-clan) in [#224](https://github.com/fastapi-practices/fastapi_best_architecture/pull/224)
|
||||||
|
* Update the pytz library to zoneinfo by [@wu-clan](https://github.com/wu-clan) in [#226](https://github.com/fastapi-practices/fastapi_best_architecture/pull/226)
|
||||||
|
* Add token decoding expiration exception by [@wu-clan](https://github.com/wu-clan) in [#227](https://github.com/fastapi-practices/fastapi_best_architecture/pull/227)
|
||||||
|
* Fix the task run method and data type by [@wu-clan](https://github.com/wu-clan) in [#228](https://github.com/fastapi-practices/fastapi_best_architecture/pull/228)
|
||||||
|
* Remove the NoReturn return type by [@wu-clan](https://github.com/wu-clan) in [#232](https://github.com/fastapi-practices/fastapi_best_architecture/pull/232)
|
||||||
|
* Add init pytest data sql file by [@wu-clan](https://github.com/wu-clan) in [#231](https://github.com/fastapi-practices/fastapi_best_architecture/pull/231)
|
||||||
|
* Fix pytest interface unit tests by [@wu-clan](https://github.com/wu-clan) in [#233](https://github.com/fastapi-practices/fastapi_best_architecture/pull/233)
|
||||||
|
* Replace APScheduler to Celery asynchronous tasks by [@wu-clan](https://github.com/wu-clan) in [#229](https://github.com/fastapi-practices/fastapi_best_architecture/pull/229)
|
||||||
|
* Fix the conflict between Access and OperaLog middleware by [@wu-clan](https://github.com/wu-clan) in [#236](https://github.com/fastapi-practices/fastapi_best_architecture/pull/236)
|
||||||
|
* Fix unregistered error received when celery call task by [@wu-clan](https://github.com/wu-clan) in [#239](https://github.com/fastapi-practices/fastapi_best_architecture/pull/239)
|
||||||
|
* Fix database engine UUID type compatibility by [@wu-clan](https://github.com/wu-clan) in [#241](https://github.com/fastapi-practices/fastapi_best_architecture/pull/241)
|
||||||
|
* adopt ruff formatter by [@wu-clan](https://github.com/wu-clan) in [#242](https://github.com/fastapi-practices/fastapi_best_architecture/pull/242)
|
||||||
|
* Bump cryptography from 41.0.2 to 41.0.6 by [@dependabot](https://github.com/dependabot) in [#243](https://github.com/fastapi-practices/fastapi_best_architecture/pull/243)
|
||||||
|
* Add a telegram interactive link by [@wu-clan](https://github.com/wu-clan) in [#245](https://github.com/fastapi-practices/fastapi_best_architecture/pull/245)
|
||||||
|
* Fix validation error log code return type by [@wu-clan](https://github.com/wu-clan) in [#247](https://github.com/fastapi-practices/fastapi_best_architecture/pull/247)
|
||||||
|
* Fix refresh token interface user type format by [@wu-clan](https://github.com/wu-clan) in [#248](https://github.com/fastapi-practices/fastapi_best_architecture/pull/248)
|
||||||
|
* Optimize operation log code type logic by [@wu-clan](https://github.com/wu-clan) in [#249](https://github.com/fastapi-practices/fastapi_best_architecture/pull/249)
|
||||||
|
* Fix get all G rules interface logic by [@wu-clan](https://github.com/wu-clan) in [#250](https://github.com/fastapi-practices/fastapi_best_architecture/pull/250)
|
||||||
|
* Simplify the multivariate expression of exceptions handler by [@wu-clan](https://github.com/wu-clan) in [#252](https://github.com/fastapi-practices/fastapi_best_architecture/pull/252)
|
||||||
|
* Fix exception handler parameter call by [@wu-clan](https://github.com/wu-clan) in [#253](https://github.com/fastapi-practices/fastapi_best_architecture/pull/253)
|
||||||
|
* Prepare to lock the pydantic-v1 branch by [@wu-clan](https://github.com/wu-clan) in [#254](https://github.com/fastapi-practices/fastapi_best_architecture/pull/254)
|
||||||
|
* Add a stand-alone assertion error handler by [@wu-clan](https://github.com/wu-clan) in [#255](https://github.com/fastapi-practices/fastapi_best_architecture/pull/255)
|
||||||
|
* Clean up todo and fix typo by [@wu-clan](https://github.com/wu-clan) in [#256](https://github.com/fastapi-practices/fastapi_best_architecture/pull/256)
|
||||||
|
* Migrate to pydantic-v2 by [@wu-clan](https://github.com/wu-clan) in [#246](https://github.com/fastapi-practices/fastapi_best_architecture/pull/246)
|
||||||
|
* Add pydantic-v2 migration reminder by [@wu-clan](https://github.com/wu-clan) in [#257](https://github.com/fastapi-practices/fastapi_best_architecture/pull/257)
|
||||||
|
* Add the project status page to the README by [@wu-clan](https://github.com/wu-clan) in [#259](https://github.com/fastapi-practices/fastapi_best_architecture/pull/259)
|
||||||
|
* Clean up outdated pydantic dict methods by [@wu-clan](https://github.com/wu-clan) in [#262](https://github.com/fastapi-practices/fastapi_best_architecture/pull/262)
|
||||||
|
* Fix use request.form() in middleware by [@wu-clan](https://github.com/wu-clan) in [#260](https://github.com/fastapi-practices/fastapi_best_architecture/pull/260)
|
||||||
|
* Reconstruct RBAC authentication logic by [@wu-clan](https://github.com/wu-clan) in [#264](https://github.com/fastapi-practices/fastapi_best_architecture/pull/264)
|
||||||
|
* Attempt to optimize serialization performance by [@wu-clan](https://github.com/wu-clan) in [#266](https://github.com/fastapi-practices/fastapi_best_architecture/pull/266)
|
||||||
|
* Update schemas naming style by [@wu-clan](https://github.com/wu-clan) in [#272](https://github.com/fastapi-practices/fastapi_best_architecture/pull/272)
|
||||||
|
* Update sponsor links and FUNDING by [@wu-clan](https://github.com/wu-clan) in [#273](https://github.com/fastapi-practices/fastapi_best_architecture/pull/273)
|
||||||
|
* Fix dept and menu parent id update logic by [@wu-clan](https://github.com/wu-clan) in [#274](https://github.com/fastapi-practices/fastapi_best_architecture/pull/274)
|
||||||
|
* Update interface coding style by [@wu-clan](https://github.com/wu-clan) in [#275](https://github.com/fastapi-practices/fastapi_best_architecture/pull/275)
|
||||||
|
* Update dao and service instantiation styles by [@wu-clan](https://github.com/wu-clan) in [#276](https://github.com/fastapi-practices/fastapi_best_architecture/pull/276)
|
||||||
|
* Add custom email string type by [@wu-clan](https://github.com/wu-clan) in [#277](https://github.com/fastapi-practices/fastapi_best_architecture/pull/277)
|
||||||
|
* Fix custom validator exception serialization in dev mode by [@wu-clan](https://github.com/wu-clan) in [#278](https://github.com/fastapi-practices/fastapi_best_architecture/pull/278)
|
||||||
|
* Restore the Github ci workflows by [@wu-clan](https://github.com/wu-clan) in [#281](https://github.com/fastapi-practices/fastapi_best_architecture/pull/281)
|
||||||
|
* Add the pdm project manager by [@wu-clan](https://github.com/wu-clan) in [#282](https://github.com/fastapi-practices/fastapi_best_architecture/pull/282)
|
||||||
|
* Add the front-end docker-compose script by [@wu-clan](https://github.com/wu-clan) in [#283](https://github.com/fastapi-practices/fastapi_best_architecture/pull/283)
|
||||||
|
* Update the response status code in exception handlers by [@wu-clan](https://github.com/wu-clan) in [#292](https://github.com/fastapi-practices/fastapi_best_architecture/pull/292)
|
||||||
|
* Update interface file directory level by [@wu-clan](https://github.com/wu-clan) in [#295](https://github.com/fastapi-practices/fastapi_best_architecture/pull/295)
|
||||||
|
* Add the repository star map by [@wu-clan](https://github.com/wu-clan) in [#296](https://github.com/fastapi-practices/fastapi_best_architecture/pull/296)
|
||||||
|
* Add OAuth 2.0 authorization login by [@wu-clan](https://github.com/wu-clan) in [#293](https://github.com/fastapi-practices/fastapi_best_architecture/pull/293)
|
||||||
|
* Prepare to lock the legacy branch by [@wu-clan](https://github.com/wu-clan) in [#301](https://github.com/fastapi-practices/fastapi_best_architecture/pull/301)
|
||||||
|
* Update the README.md branch prompt by [@wu-clan](https://github.com/wu-clan) in [#302](https://github.com/fastapi-practices/fastapi_best_architecture/pull/302)
|
||||||
|
* Refactor the backend architecture by [@wu-clan](https://github.com/wu-clan) in [#299](https://github.com/fastapi-practices/fastapi_best_architecture/pull/299)
|
||||||
|
* Fix English README.md update date by [@wu-clan](https://github.com/wu-clan) in [#308](https://github.com/fastapi-practices/fastapi_best_architecture/pull/308)
|
||||||
|
* Add backend scripts description by [@wu-clan](https://github.com/wu-clan) in [#309](https://github.com/fastapi-practices/fastapi_best_architecture/pull/309)
|
||||||
|
* Fix missing data from alembic migration by [@wu-clan](https://github.com/wu-clan) in [#312](https://github.com/fastapi-practices/fastapi_best_architecture/pull/312)
|
||||||
|
* Update CRUDBase to sqlalchemy-crud-plus by [@wu-clan](https://github.com/wu-clan) in [#317](https://github.com/fastapi-practices/fastapi_best_architecture/pull/317)
|
||||||
|
* Upgrade and update fastapi service startup by [@wu-clan](https://github.com/wu-clan) in [#319](https://github.com/fastapi-practices/fastapi_best_architecture/pull/319)
|
||||||
|
* Delete the gzip middleware to improve performance by [@wu-clan](https://github.com/wu-clan) in [#325](https://github.com/fastapi-practices/fastapi_best_architecture/pull/325)
|
||||||
|
* Add interface fast response method by [@wu-clan](https://github.com/wu-clan) in [#327](https://github.com/fastapi-practices/fastapi_best_architecture/pull/327)
|
||||||
|
* Update the opera log middleware task by [@wu-clan](https://github.com/wu-clan) in [#326](https://github.com/fastapi-practices/fastapi_best_architecture/pull/326)
|
||||||
|
* Add test account to README by [@wu-clan](https://github.com/wu-clan) in [#330](https://github.com/fastapi-practices/fastapi_best_architecture/pull/330)
|
||||||
|
* Restore the main startup of fastapi service by [@wu-clan](https://github.com/wu-clan) in [#336](https://github.com/fastapi-practices/fastapi_best_architecture/pull/336)
|
||||||
|
* Update app route definition rules by [@wu-clan](https://github.com/wu-clan) in [#341](https://github.com/fastapi-practices/fastapi_best_architecture/pull/341)
|
||||||
|
* Add Linux Do OAuth2 login by [@wu-clan](https://github.com/wu-clan) in [#343](https://github.com/fastapi-practices/fastapi_best_architecture/pull/343)
|
||||||
|
* Fix user social binding query by [@wu-clan](https://github.com/wu-clan) in [#344](https://github.com/fastapi-practices/fastapi_best_architecture/pull/344)
|
||||||
|
* Refactor global log default handler by [@obrua](https://github.com/obrua) in [#347](https://github.com/fastapi-practices/fastapi_best_architecture/pull/347)
|
||||||
|
* Add code generator app by [@wu-clan](https://github.com/wu-clan) in [#318](https://github.com/fastapi-practices/fastapi_best_architecture/pull/318)
|
||||||
|
* Update and enable access log middleware by [@wu-clan](https://github.com/wu-clan) in [#348](https://github.com/fastapi-practices/fastapi_best_architecture/pull/348)
|
||||||
|
* Add code generator README document by [@wu-clan](https://github.com/wu-clan) in [#349](https://github.com/fastapi-practices/fastapi_best_architecture/pull/349)
|
||||||
|
* Fix model template conditional syntax by [@wu-clan](https://github.com/wu-clan) in [#351](https://github.com/fastapi-practices/fastapi_best_architecture/pull/351)
|
||||||
|
* Update code generation model column type storage by [@wu-clan](https://github.com/wu-clan) in [#352](https://github.com/fastapi-practices/fastapi_best_architecture/pull/352)
|
||||||
|
* Fix gen model and schema template formatting by [@wu-clan](https://github.com/wu-clan) in [#356](https://github.com/fastapi-practices/fastapi_best_architecture/pull/356)
|
||||||
|
* Add code generator to create init files by [@wu-clan](https://github.com/wu-clan) in [#358](https://github.com/fastapi-practices/fastapi_best_architecture/pull/358)
|
||||||
|
* Fix alembic migration failure caused by model by [@wu-clan](https://github.com/wu-clan) in [#359](https://github.com/fastapi-practices/fastapi_best_architecture/pull/359)
|
||||||
|
* Update the docker-compose deployment script by [@wu-clan](https://github.com/wu-clan) in [#360](https://github.com/fastapi-practices/fastapi_best_architecture/pull/360)
|
||||||
|
* Update oauth2 route naming and return by [@wu-clan](https://github.com/wu-clan) in [#361](https://github.com/fastapi-practices/fastapi_best_architecture/pull/361)
|
||||||
|
* Bump fast captcha version to 0.3.2 by [@wu-clan](https://github.com/wu-clan) in [#362](https://github.com/fastapi-practices/fastapi_best_architecture/pull/362)
|
||||||
|
* Update crud user staff field logic by [@wu-clan](https://github.com/wu-clan) in [#363](https://github.com/fastapi-practices/fastapi_best_architecture/pull/363)
|
||||||
|
* Fix code auto-generated model creation by [@wu-clan](https://github.com/wu-clan) in [#364](https://github.com/fastapi-practices/fastapi_best_architecture/pull/364)
|
||||||
|
* Add page to display dynamic configuration by [@wu-clan](https://github.com/wu-clan) in [#365](https://github.com/fastapi-practices/fastapi_best_architecture/pull/365)
|
||||||
|
* Fix celery asynchronous task execution by [@wu-clan](https://github.com/wu-clan) in [#367](https://github.com/fastapi-practices/fastapi_best_architecture/pull/367)
|
||||||
|
* Update operation log middleware info reading by [@wu-clan](https://github.com/wu-clan) in [#368](https://github.com/fastapi-practices/fastapi_best_architecture/pull/368)
|
||||||
|
* Update create new token function return type by [@wu-clan](https://github.com/wu-clan) in [#369](https://github.com/fastapi-practices/fastapi_best_architecture/pull/369)
|
||||||
|
* Update access log cost time style by [@wu-clan](https://github.com/wu-clan) in [#370](https://github.com/fastapi-practices/fastapi_best_architecture/pull/370)
|
||||||
|
* Update code generate business model time column by [@wu-clan](https://github.com/wu-clan) in [#371](https://github.com/fastapi-practices/fastapi_best_architecture/pull/371)
|
||||||
|
* Add custom code template pathname config by [@wu-clan](https://github.com/wu-clan) in [#372](https://github.com/fastapi-practices/fastapi_best_architecture/pull/372)
|
||||||
|
* Update some code generation api and params by [@wu-clan](https://github.com/wu-clan) in [#373](https://github.com/fastapi-practices/fastapi_best_architecture/pull/373)
|
||||||
|
* Update code generate download api auth by [@wu-clan](https://github.com/wu-clan) in [#376](https://github.com/fastapi-practices/fastapi_best_architecture/pull/376)
|
||||||
|
* Update current menu status auth by [@wu-clan](https://github.com/wu-clan) in [#374](https://github.com/fastapi-practices/fastapi_best_architecture/pull/374)
|
||||||
|
* Fix code generation model create and update by [@wu-clan](https://github.com/wu-clan) in [#378](https://github.com/fastapi-practices/fastapi_best_architecture/pull/378)
|
||||||
|
* Update user and auth error message by [@wu-clan](https://github.com/wu-clan) in [#379](https://github.com/fastapi-practices/fastapi_best_architecture/pull/379)
|
||||||
|
* Add directory tree and update app notes by [@wu-clan](https://github.com/wu-clan) in [#380](https://github.com/fastapi-practices/fastapi_best_architecture/pull/380)
|
||||||
|
* Optimize serialization and jwt performance by [@wu-clan](https://github.com/wu-clan) in [#382](https://github.com/fastapi-practices/fastapi_best_architecture/pull/382)
|
||||||
|
* Fix arm system cpu frequency retrieval by [@yshan2028](https://github.com/yshan2028) in [#385](https://github.com/fastapi-practices/fastapi_best_architecture/pull/385)
|
||||||
|
* Fix logging when a login error occurs by [@wu-clan](https://github.com/wu-clan) in [#386](https://github.com/fastapi-practices/fastapi_best_architecture/pull/386)
|
||||||
|
* Update redis cache prefix separator to `:` by [@wu-clan](https://github.com/wu-clan) in [#387](https://github.com/fastapi-practices/fastapi_best_architecture/pull/387)
|
||||||
|
* Bump sqlalchemy crud plus version to 1.3.0 by [@wu-clan](https://github.com/wu-clan) in [#388](https://github.com/fastapi-practices/fastapi_best_architecture/pull/388)
|
||||||
|
* Update the README announcement to note by [@wu-clan](https://github.com/wu-clan) in [#390](https://github.com/fastapi-practices/fastapi_best_architecture/pull/390)
|
||||||
|
* Fix code generation to new features by [@wu-clan](https://github.com/wu-clan) in [#393](https://github.com/fastapi-practices/fastapi_best_architecture/pull/393)
|
||||||
|
* Fix OAuth2 user query conditions by [@wu-clan](https://github.com/wu-clan) in [#396](https://github.com/fastapi-practices/fastapi_best_architecture/pull/396)
|
||||||
|
* Fix the user permissions update services by [@wu-clan](https://github.com/wu-clan) in [#397](https://github.com/fastapi-practices/fastapi_best_architecture/pull/397)
|
||||||
|
* Update code generate comment column format by [@wu-clan](https://github.com/wu-clan) in [#399](https://github.com/fastapi-practices/fastapi_best_architecture/pull/399)
|
||||||
|
* Update the interactive link address by [@wu-clan](https://github.com/wu-clan) in [#402](https://github.com/fastapi-practices/fastapi_best_architecture/pull/402)
|
||||||
|
* Updated refresh token storage logic by [@wu-clan](https://github.com/wu-clan) in [#403](https://github.com/fastapi-practices/fastapi_best_architecture/pull/403)
|
||||||
|
* Fix server monitor io blocking by [@wu-clan](https://github.com/wu-clan) in [#404](https://github.com/fastapi-practices/fastapi_best_architecture/pull/404)
|
||||||
|
* Fix cookie expiration time zone by [@wu-clan](https://github.com/wu-clan) in [#408](https://github.com/fastapi-practices/fastapi_best_architecture/pull/408)
|
||||||
|
* Add request trace ID record by [@wu-clan](https://github.com/wu-clan) in [#409](https://github.com/fastapi-practices/fastapi_best_architecture/pull/409)
|
||||||
|
* Optimize the naming of setting params by [@wu-clan](https://github.com/wu-clan) in [#410](https://github.com/fastapi-practices/fastapi_best_architecture/pull/410)
|
||||||
|
* Add trace ID to exception handlers by [@wu-clan](https://github.com/wu-clan) in [#411](https://github.com/fastapi-practices/fastapi_best_architecture/pull/411)
|
||||||
|
* Update the global exception log stack by [@wu-clan](https://github.com/wu-clan) in [#406](https://github.com/fastapi-practices/fastapi_best_architecture/pull/406)
|
||||||
|
* Bump pydantic from 2.8.1 to 2.9.1 by [@wu-clan](https://github.com/wu-clan) in [#412](https://github.com/fastapi-practices/fastapi_best_architecture/pull/412)
|
||||||
|
* Optimize exception info opera log record by [@wu-clan](https://github.com/wu-clan) in [#413](https://github.com/fastapi-practices/fastapi_best_architecture/pull/413)
|
||||||
|
* Fix log output and logging levels by [@wu-clan](https://github.com/wu-clan) in [#414](https://github.com/fastapi-practices/fastapi_best_architecture/pull/414)
|
||||||
|
* Fix exception logging in opera log by [@wu-clan](https://github.com/wu-clan) in [#417](https://github.com/fastapi-practices/fastapi_best_architecture/pull/417)
|
||||||
|
* Fix the gen model template formatting by [@wu-clan](https://github.com/wu-clan) in [#416](https://github.com/fastapi-practices/fastapi_best_architecture/pull/416)
|
||||||
|
* Optimize the internal implementation of serializers by [@wu-clan](https://github.com/wu-clan) in [#419](https://github.com/fastapi-practices/fastapi_best_architecture/pull/419)
|
||||||
|
* Fix for create new token cache delete by [@wu-clan](https://github.com/wu-clan) in [#420](https://github.com/fastapi-practices/fastapi_best_architecture/pull/420)
|
||||||
|
* Update JWT errors class import by [@wu-clan](https://github.com/wu-clan) in [#421](https://github.com/fastapi-practices/fastapi_best_architecture/pull/421)
|
||||||
|
* Update multi login sync update refresh tokens by [@wu-clan](https://github.com/wu-clan) in [#422](https://github.com/fastapi-practices/fastapi_best_architecture/pull/422)
|
||||||
|
* Update sync function calls in JWT by [@wu-clan](https://github.com/wu-clan) in [#423](https://github.com/fastapi-practices/fastapi_best_architecture/pull/423)
|
||||||
|
* Fix the missing OAuth2 interface parameters by [@wu-clan](https://github.com/wu-clan) in [#425](https://github.com/fastapi-practices/fastapi_best_architecture/pull/425)
|
||||||
|
* Add request state middleware by [@wu-clan](https://github.com/wu-clan) in [#426](https://github.com/fastapi-practices/fastapi_best_architecture/pull/426)
|
||||||
|
* Fix pydantic field and model validator by [@wu-clan](https://github.com/wu-clan) in [#427](https://github.com/fastapi-practices/fastapi_best_architecture/pull/427)
|
||||||
|
* Fix the OAuth2 service login log task by [@wu-clan](https://github.com/wu-clan) in [#428](https://github.com/fastapi-practices/fastapi_best_architecture/pull/428)
|
||||||
|
* Update official documentation link to README by [@wu-clan](https://github.com/wu-clan) in [#429](https://github.com/fastapi-practices/fastapi_best_architecture/pull/429)
|
||||||
|
* Optimize and normalize the code generator by [@wu-clan](https://github.com/wu-clan) in [#430](https://github.com/fastapi-practices/fastapi_best_architecture/pull/430)
|
||||||
|
* Bump redis from 5.0.1 to 5.1.0 by [@wu-clan](https://github.com/wu-clan) in [#433](https://github.com/fastapi-practices/fastapi_best_architecture/pull/433)
|
||||||
|
* Update interactive link descriptions by [@wu-clan](https://github.com/wu-clan) in [#434](https://github.com/fastapi-practices/fastapi_best_architecture/pull/434)
|
||||||
|
* Optimize the serialize return of SQLA select by [@wu-clan](https://github.com/wu-clan) in [#436](https://github.com/fastapi-practices/fastapi_best_architecture/pull/436)
|
||||||
|
* Update project manager pdm to uv by [@wu-clan](https://github.com/wu-clan) in [#440](https://github.com/fastapi-practices/fastapi_best_architecture/pull/440)
|
||||||
|
* Add asynchronous socketio application server by [@wu-clan](https://github.com/wu-clan) in [#437](https://github.com/fastapi-practices/fastapi_best_architecture/pull/437)
|
||||||
|
* Add dependency-groups by PEP 735 by [@wu-clan](https://github.com/wu-clan) in [#444](https://github.com/fastapi-practices/fastapi_best_architecture/pull/444)
|
||||||
|
* Update the usage documentation in README by [@wu-clan](https://github.com/wu-clan) in [#449](https://github.com/fastapi-practices/fastapi_best_architecture/pull/449)
|
||||||
|
* Bump sqlalchemy crud plus version to 1.5.0 by [@wu-clan](https://github.com/wu-clan) in [#450](https://github.com/fastapi-practices/fastapi_best_architecture/pull/450)
|
||||||
|
* Update singleton pattern class typing by [@wu-clan](https://github.com/wu-clan) in [#452](https://github.com/fastapi-practices/fastapi_best_architecture/pull/452)
|
||||||
|
* Update system config to be dynamic by [@wu-clan](https://github.com/wu-clan) in [#447](https://github.com/fastapi-practices/fastapi_best_architecture/pull/447)
|
||||||
|
* Update multiple version dependency specifiers by [@wu-clan](https://github.com/wu-clan) in [#454](https://github.com/fastapi-practices/fastapi_best_architecture/pull/454)
|
||||||
|
* Fix typo in contribution description by [@wu-clan](https://github.com/wu-clan) in [#456](https://github.com/fastapi-practices/fastapi_best_architecture/pull/456)
|
||||||
|
* Fix code generation file missing by [@wu-clan](https://github.com/wu-clan) in [#457](https://github.com/fastapi-practices/fastapi_best_architecture/pull/457)
|
||||||
|
* Update the celery configuration and tasks by [@wu-clan](https://github.com/wu-clan) in [#458](https://github.com/fastapi-practices/fastapi_best_architecture/pull/458)
|
||||||
|
* Update some service class invocations by [@wu-clan](https://github.com/wu-clan) in [#459](https://github.com/fastapi-practices/fastapi_best_architecture/pull/459)
|
||||||
|
* Update code generator API file structure by [@wu-clan](https://github.com/wu-clan) in [#460](https://github.com/fastapi-practices/fastapi_best_architecture/pull/460)
|
||||||
|
* Update api body params to schema by [@wu-clan](https://github.com/wu-clan) in [#461](https://github.com/fastapi-practices/fastapi_best_architecture/pull/461)
|
||||||
|
* Fix celery service functions error by [@wu-clan](https://github.com/wu-clan) in [#462](https://github.com/fastapi-practices/fastapi_best_architecture/pull/462)
|
||||||
|
* Update user password encryption method by [@wu-clan](https://github.com/wu-clan) in [#463](https://github.com/fastapi-practices/fastapi_best_architecture/pull/463)
|
||||||
|
* Update role-based data permissions by [@wu-clan](https://github.com/wu-clan) in [#465](https://github.com/fastapi-practices/fastapi_best_architecture/pull/465)
|
||||||
|
* Bump tornado from 6.4.1 to 6.4.2 in /backend by [@dependabot](https://github.com/dependabot) in [#466](https://github.com/fastapi-practices/fastapi_best_architecture/pull/466)
|
||||||
|
* Fix schema type of user role rule by [@wu-clan](https://github.com/wu-clan) in [#467](https://github.com/fastapi-practices/fastapi_best_architecture/pull/467)
|
||||||
|
* Simplify data rule and remove type by [@wu-clan](https://github.com/wu-clan) in [#468](https://github.com/fastapi-practices/fastapi_best_architecture/pull/468)
|
||||||
|
* Add the project logo to README by [@wu-clan](https://github.com/wu-clan) in [#469](https://github.com/fastapi-practices/fastapi_best_architecture/pull/469)
|
||||||
|
* Optimized user auth for auth service by [@wu-clan](https://github.com/wu-clan) in [#472](https://github.com/fastapi-practices/fastapi_best_architecture/pull/472)
|
||||||
|
* Fix data rule expression column comment by [@wu-clan](https://github.com/wu-clan) in [#473](https://github.com/fastapi-practices/fastapi_best_architecture/pull/473)
|
||||||
|
* Fix and update alembic env and ini by [@wu-clan](https://github.com/wu-clan) in [#474](https://github.com/fastapi-practices/fastapi_best_architecture/pull/474)
|
||||||
|
* Fix login log parameter error in task by [@wu-clan](https://github.com/wu-clan) in [#476](https://github.com/fastapi-practices/fastapi_best_architecture/pull/476)
|
||||||
|
* Remove data scope in the role model by [@wu-clan](https://github.com/wu-clan) in [#478](https://github.com/fastapi-practices/fastapi_best_architecture/pull/478)
|
||||||
|
* Add postgresql database support by [@Meepoljdx](https://github.com/Meepoljdx) in [#475](https://github.com/fastapi-practices/fastapi_best_architecture/pull/475)
|
||||||
|
* Update opera log cost time precision by [@wu-clan](https://github.com/wu-clan) in [#479](https://github.com/fastapi-practices/fastapi_best_architecture/pull/479)
|
||||||
|
* Update opera middleware request args parse by [@wu-clan](https://github.com/wu-clan) in [#481](https://github.com/fastapi-practices/fastapi_best_architecture/pull/481)
|
||||||
|
* Bump msgspec from 0.18.6 to 0.19.0 by [@wu-clan](https://github.com/wu-clan) in [#482](https://github.com/fastapi-practices/fastapi_best_architecture/pull/482)
|
||||||
|
* Fix user cache when updated user role by [@wu-clan](https://github.com/wu-clan) in [#483](https://github.com/fastapi-practices/fastapi_best_architecture/pull/483)
|
||||||
|
* Update the route version define location by [@wu-clan](https://github.com/wu-clan) in [#485](https://github.com/fastapi-practices/fastapi_best_architecture/pull/485)
|
||||||
|
* Optimize docker deploy settings and scripts by [@wu-clan](https://github.com/wu-clan) in [#486](https://github.com/fastapi-practices/fastapi_best_architecture/pull/486)
|
||||||
|
* Add system notice interface by [@dividduang](https://github.com/dividduang) in [#487](https://github.com/fastapi-practices/fastapi_best_architecture/pull/487)
|
||||||
|
* Add response model include data schema by [@wu-clan](https://github.com/wu-clan) in [#490](https://github.com/fastapi-practices/fastapi_best_architecture/pull/490)
|
||||||
|
* Update redocs arg and url to redoc by [@wu-clan](https://github.com/wu-clan) in [#493](https://github.com/fastapi-practices/fastapi_best_architecture/pull/493)
|
||||||
|
* Fix serialization when pagination is empty by [@qhp13654398483](https://github.com/qhp13654398483) in [#491](https://github.com/fastapi-practices/fastapi_best_architecture/pull/491)
|
||||||
|
* Update return schema of query interface by [@wu-clan](https://github.com/wu-clan) in [#492](https://github.com/fastapi-practices/fastapi_best_architecture/pull/492)
|
||||||
|
* Add token related interfaces by [@wu-clan](https://github.com/wu-clan) in [#495](https://github.com/fastapi-practices/fastapi_best_architecture/pull/495)
|
||||||
|
* Fix return schema of user me api by [@wu-clan](https://github.com/wu-clan) in [#497](https://github.com/fastapi-practices/fastapi_best_architecture/pull/497)
|
||||||
|
* Fix current user info detail schema by [@wu-clan](https://github.com/wu-clan) in [#499](https://github.com/fastapi-practices/fastapi_best_architecture/pull/499)
|
||||||
|
* Update menu field show to display by [@wu-clan](https://github.com/wu-clan) in [#498](https://github.com/fastapi-practices/fastapi_best_architecture/pull/498)
|
||||||
|
* Fix casbin policy api return schema by [@wu-clan](https://github.com/wu-clan) in [#500](https://github.com/fastapi-practices/fastapi_best_architecture/pull/500)
|
||||||
|
* Fix opera log of non-dict request body by [@wu-clan](https://github.com/wu-clan) in [#501](https://github.com/fastapi-practices/fastapi_best_architecture/pull/501)
|
||||||
|
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#504](https://github.com/fastapi-practices/fastapi_best_architecture/pull/504)
|
||||||
|
* Fix the return datetime data encoder by [@wu-clan](https://github.com/wu-clan) in [#505](https://github.com/fastapi-practices/fastapi_best_architecture/pull/505)
|
||||||
|
* Fix fastapi config variable naming and type by [@wu-clan](https://github.com/wu-clan) in [#506](https://github.com/fastapi-practices/fastapi_best_architecture/pull/506)
|
||||||
|
* Fix the user pagination api return schema by [@wu-clan](https://github.com/wu-clan) in [#507](https://github.com/fastapi-practices/fastapi_best_architecture/pull/507)
|
||||||
|
* Fix OAuth2 service register user args by [@wu-clan](https://github.com/wu-clan) in [#508](https://github.com/fastapi-practices/fastapi_best_architecture/pull/508)
|
||||||
|
* Fix OAuth2 service user last login time by [@wu-clan](https://github.com/wu-clan) in [#509](https://github.com/fastapi-practices/fastapi_best_architecture/pull/509)
|
||||||
|
* Fix OAuth2 service user last login time by [@wu-clan](https://github.com/wu-clan) in [#510](https://github.com/fastapi-practices/fastapi_best_architecture/pull/510)
|
||||||
|
* Add plugin system and notice plugin by [@wu-clan](https://github.com/wu-clan) in [#503](https://github.com/fastapi-practices/fastapi_best_architecture/pull/503)
|
||||||
|
* Delete the threads in gunicorn config by [@wu-clan](https://github.com/wu-clan) in [#512](https://github.com/fastapi-practices/fastapi_best_architecture/pull/512)
|
||||||
|
* Add plugin requirements auto install functions by [@wu-clan](https://github.com/wu-clan) in [#514](https://github.com/fastapi-practices/fastapi_best_architecture/pull/514)
|
||||||
|
* Update casbin rbac verify to plugin by [@wu-clan](https://github.com/wu-clan) in [#513](https://github.com/fastapi-practices/fastapi_best_architecture/pull/513)
|
||||||
|
* Update the logic for create new token by [@wu-clan](https://github.com/wu-clan) in [#516](https://github.com/fastapi-practices/fastapi_best_architecture/pull/516)
|
||||||
|
* Add local file upload interfaces by [@wu-clan](https://github.com/wu-clan) in [#489](https://github.com/fastapi-practices/fastapi_best_architecture/pull/489)
|
||||||
|
* Update loguru and deploy log config by [@wu-clan](https://github.com/wu-clan) in [#517](https://github.com/fastapi-practices/fastapi_best_architecture/pull/517)
|
||||||
|
* Delete the model redundancy level field by [@wu-clan](https://github.com/wu-clan) in [#518](https://github.com/fastapi-practices/fastapi_best_architecture/pull/518)
|
||||||
|
* Update the built-in features in README by [@wu-clan](https://github.com/wu-clan) in [#519](https://github.com/fastapi-practices/fastapi_best_architecture/pull/519)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@downdawn](https://github.com/downdawn) made their first contribution in [#15](https://github.com/fastapi-practices/fastapi_best_architecture/pull/15)
|
||||||
|
* [@dependabot](https://github.com/dependabot) made their first contribution in [#48](https://github.com/fastapi-practices/fastapi_best_architecture/pull/48)
|
||||||
|
* [@obrua](https://github.com/obrua) made their first contribution in [#347](https://github.com/fastapi-practices/fastapi_best_architecture/pull/347)
|
||||||
|
* [@yshan2028](https://github.com/yshan2028) made their first contribution in [#385](https://github.com/fastapi-practices/fastapi_best_architecture/pull/385)
|
||||||
|
* [@Meepoljdx](https://github.com/Meepoljdx) made their first contribution in [#475](https://github.com/fastapi-practices/fastapi_best_architecture/pull/475)
|
||||||
|
* [@dividduang](https://github.com/dividduang) made their first contribution in [#487](https://github.com/fastapi-practices/fastapi_best_architecture/pull/487)
|
||||||
|
* [@qhp13654398483](https://github.com/qhp13654398483) made their first contribution in [#491](https://github.com/fastapi-practices/fastapi_best_architecture/pull/491)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/commits/v1.0.0
|
||||||
|
|
||||||
|
[Changes][v1.0.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.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.2.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
|
||||||
|
[v1.1.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
|
||||||
|
[v1.1.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
|
||||||
|
[v1.1.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
|
||||||
|
[v1.0.5]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
|
||||||
|
[v1.0.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
|
||||||
|
[v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
|
||||||
|
[v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
|
||||||
|
[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
|
||||||
|
|
||||||
|
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.0 -->
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
# Select the image to build based on SERVER_TYPE, defaulting to fastapi_server, or docker-compose build args
|
||||||
|
ARG SERVER_TYPE=fastapi_server
|
||||||
|
|
||||||
|
# === Python environment from uv ===
|
||||||
|
FROM ghcr.io/astral-sh/uv:python3.10-bookworm-slim AS builder
|
||||||
|
|
||||||
|
# Used for build Python packages
|
||||||
|
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends gcc python3-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . /fba
|
||||||
|
|
||||||
|
WORKDIR /fba
|
||||||
|
|
||||||
|
# Configure uv environment
|
||||||
|
ENV UV_COMPILE_BYTECODE=1 \
|
||||||
|
UV_NO_CACHE=1 \
|
||||||
|
UV_LINK_MODE=copy \
|
||||||
|
UV_PROJECT_ENVIRONMENT=/usr/local
|
||||||
|
|
||||||
|
# Install dependencies with cache
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --frozen --no-default-groups --group server
|
||||||
|
|
||||||
|
# === Runtime base server image ===
|
||||||
|
FROM python:3.10-slim AS base_server
|
||||||
|
|
||||||
|
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends supervisor \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /fba /fba
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local /usr/local
|
||||||
|
|
||||||
|
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
|
|
||||||
|
WORKDIR /fba/backend
|
||||||
|
|
||||||
|
# === FastAPI server image ===
|
||||||
|
FROM base_server AS fastapi_server
|
||||||
|
|
||||||
|
COPY deploy/backend/fba_server.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log/fastapi_server
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/granian", "main:app", "--interface", "asgi", "--host", "0.0.0.0", "--port","8000"]
|
||||||
|
|
||||||
|
# === Celery server image ===
|
||||||
|
FROM base_server AS celery
|
||||||
|
|
||||||
|
COPY deploy/backend/fba_celery.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log/celery
|
||||||
|
|
||||||
|
RUN chmod +x celery-start.sh
|
||||||
|
|
||||||
|
EXPOSE 8555
|
||||||
|
|
||||||
|
CMD ["./celery-start.sh"]
|
||||||
|
|
||||||
|
# Build image
|
||||||
|
FROM ${SERVER_TYPE}
|
||||||
@@ -4,14 +4,10 @@
|
|||||||
|
|
||||||
# FastAPI Best Architecture
|
# FastAPI Best Architecture
|
||||||
|
|
||||||
|
Enterprise-level backend architecture solution
|
||||||
|
|
||||||
English | [简体中文](./README.zh-CN.md)
|
English | [简体中文](./README.zh-CN.md)
|
||||||
|
|
||||||
A backend and frontend separation solution based on the FastAPI framework, following
|
|
||||||
the [pseudo 3-tier architecture](#pseudo-3-tier-architecture) design, supporting **Python 3.10** and above
|
|
||||||
versions
|
|
||||||
|
|
||||||
**🔥Continuously updated and maintained🔥**
|
|
||||||
|
|
||||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||

|

|
||||||
@@ -23,12 +19,10 @@ versions
|
|||||||

|

|
||||||
[](https://discord.com/invite/yNN3wTbVAC)
|
[](https://discord.com/invite/yNN3wTbVAC)
|
||||||

|

|
||||||
|
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> This repository as a template library open to any person or enterprise can be used for free!
|
|
||||||
|
|
||||||
## Pseudo 3-tier architecture
|
## Pseudo 3-tier architecture
|
||||||
|
|
||||||
The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating
|
The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating
|
||||||
@@ -47,41 +41,7 @@ pattern, use templates to transform it to your heart's content!
|
|||||||
| data access | dao / mapper | crud |
|
| data access | dao / mapper | crud |
|
||||||
| model | model / entity | model |
|
| model | model / entity | model |
|
||||||
|
|
||||||
## Features
|
## Help
|
||||||
|
|
||||||
- [x] Design with FastAPI PEP 593 Annotated Parameters
|
|
||||||
- [x] Global asynchronous design with async/await + asgiref
|
|
||||||
- [x] Follows Restful API specification
|
|
||||||
- [x] Global SQLAlchemy 2.0 syntax
|
|
||||||
- [x] Pydantic v1 and v2 (different branches)
|
|
||||||
- [x] Casbin RBAC access control model
|
|
||||||
- [x] Role menu RBAC access control model
|
|
||||||
- [x] Celery asynchronous tasks
|
|
||||||
- [x] JWT middleware whitelist authentication
|
|
||||||
- [x] Global customizable time zone time
|
|
||||||
- [x] Docker / Docker-compose deployment
|
|
||||||
- [x] Pytest Unit Testing
|
|
||||||
|
|
||||||
## Built-in features
|
|
||||||
|
|
||||||
- [x] User management: System User Role Management, Permission Allocation
|
|
||||||
- [x] Department management: Configure system organization (company, department, team...)
|
|
||||||
- [x] Menu management: Configure system menu, user menu, button permission tags
|
|
||||||
- [x] Role management: role menu permission allocation, role route permission allocation
|
|
||||||
- [x] Dictionary management: Maintain commonly used fixed data or parameters within the system
|
|
||||||
- [x] Token management: System user online status detection, supports kicking users offline
|
|
||||||
- [x] Login authentication: backend-based graphical captcha background authentication login
|
|
||||||
- [x] Multipoint login: One-click modification of multipoint login through user information
|
|
||||||
- [x] OAuth 2.0: Built-in self-developed OAuth 2.0 login integration
|
|
||||||
- [x] Code generation: automatic backend code generation, supports preview, writing, and download
|
|
||||||
- [x] Scheduled task: Automated task, asynchronous task, supports function calls
|
|
||||||
- [x] Plugin system: Say goodbye to high coupling integration through hot-pluggable plugin mode
|
|
||||||
- [x] Operation log: Record and query of system normal and abnormal operations
|
|
||||||
- [x] Login log: Record and query of normal and abnormal user login
|
|
||||||
- [x] Service monitoring: Server hardware device information and status
|
|
||||||
- [x] API documentation: Automatically generate online interactive API documentation
|
|
||||||
|
|
||||||
## Development and deployment
|
|
||||||
|
|
||||||
For more details, please check
|
For more details, please check
|
||||||
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||||
@@ -103,7 +63,7 @@ the [official documentation](https://fastapi-practices.github.io/fastapi_best_ar
|
|||||||
|
|
||||||
## Interactivity
|
## Interactivity
|
||||||
|
|
||||||
[TG / Discord](https://wu-clan.github.io/homepage/)
|
[Discord](https://wu-clan.github.io/homepage/)
|
||||||
|
|
||||||
## Sponsor us
|
## Sponsor us
|
||||||
|
|
||||||
|
|||||||
+5
-43
@@ -4,12 +4,10 @@
|
|||||||
|
|
||||||
# FastAPI Best Architecture
|
# FastAPI Best Architecture
|
||||||
|
|
||||||
|
企业级后端架构解决方案
|
||||||
|
|
||||||
简体中文 | [English](./README.md)
|
简体中文 | [English](./README.md)
|
||||||
|
|
||||||
基于 FastAPI 框架的前后端分离中后台解决方案,遵循[伪三层架构](#伪三层架构)设计, 支持 **python3.10** 及以上版本
|
|
||||||
|
|
||||||
**🔥持续更新维护中🔥**
|
|
||||||
|
|
||||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||

|

|
||||||
@@ -21,12 +19,10 @@
|
|||||||

|

|
||||||
[](https://discord.com/invite/yNN3wTbVAC)
|
[](https://discord.com/invite/yNN3wTbVAC)
|
||||||

|

|
||||||
|
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 此仓库作为模板库公开,任何人或企业均可免费使用!
|
|
||||||
|
|
||||||
## 伪三层架构
|
## 伪三层架构
|
||||||
|
|
||||||
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
|
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
|
||||||
@@ -43,41 +39,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
|||||||
| 数据访问 | dao / mapper | crud |
|
| 数据访问 | dao / mapper | crud |
|
||||||
| 模型 | model / entity | model |
|
| 模型 | model / entity | model |
|
||||||
|
|
||||||
## 特征
|
## 帮助
|
||||||
|
|
||||||
- [x] 全局 FastAPI PEP 593 Annotated 参数风格
|
|
||||||
- [x] async/await + asgiref 的全局异步设计
|
|
||||||
- [x] 遵循 Restful API 规范
|
|
||||||
- [x] 全局 SQLAlchemy 2.0 语法
|
|
||||||
- [x] Pydantic v1 和 v2 (不同分支)
|
|
||||||
- [x] Casbin RBAC 访问控制模型
|
|
||||||
- [x] 角色菜单 RBAC 访问控制模型
|
|
||||||
- [x] Celery 异步任务
|
|
||||||
- [x] JWT 中间件白名单认证
|
|
||||||
- [x] 全局自定义时区时间
|
|
||||||
- [x] Docker / Docker-compose 部署
|
|
||||||
- [x] Pytest 单元测试
|
|
||||||
|
|
||||||
## 内置功能
|
|
||||||
|
|
||||||
- [x] 用户管理:系统用户角色管理,权限分配
|
|
||||||
- [x] 部门管理:配置系统组织机构(公司、部门、小组...)
|
|
||||||
- [x] 菜单管理:配置系统菜单,用户菜单,按钮权限标识
|
|
||||||
- [x] 角色管理:角色菜单权限分配,角色路由权限分配
|
|
||||||
- [x] 字典管理:维护系统内部常用固定数据或参数
|
|
||||||
- [x] 令牌管理:系统用户在线状态检测,支持踢人下线
|
|
||||||
- [x] 登录认证:基于后端的图形验证码后台认证登录
|
|
||||||
- [x] 多点登录:通过用户信息一键修改多点登录支持
|
|
||||||
- [x] OAuth20:内置自研 OAuth 2.0 登录集成
|
|
||||||
- [x] 代码生成:后端代码自动生成,支持预览,写入及下载
|
|
||||||
- [x] 定时任务:自动化任务,异步任务,支持函数调用
|
|
||||||
- [x] 插件系统:通过热插拔插件模式告别高耦合集成
|
|
||||||
- [x] 操作日志:系统正常和异常操作的日志记录与查询
|
|
||||||
- [x] 登录日志:用户正常和异常登录的日志记录与查询
|
|
||||||
- [x] 服务监控:服务器硬件设备信息与状态
|
|
||||||
- [x] 接口文档:自动生成在线交互式 API 文档
|
|
||||||
|
|
||||||
## 开发部署
|
|
||||||
|
|
||||||
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||||
|
|
||||||
@@ -98,7 +60,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
|||||||
|
|
||||||
## 互动
|
## 互动
|
||||||
|
|
||||||
[TG / Discord](https://wu-clan.github.io/homepage/)
|
[Discord](https://wu-clan.github.io/homepage/)
|
||||||
|
|
||||||
## 赞助我们
|
## 赞助我们
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
.python-version
|
|
||||||
+16
-13
@@ -1,4 +1,4 @@
|
|||||||
# Env: dev、pro
|
# Env
|
||||||
ENVIRONMENT='dev'
|
ENVIRONMENT='dev'
|
||||||
# Database
|
# Database
|
||||||
DATABASE_TYPE='mysql'
|
DATABASE_TYPE='mysql'
|
||||||
@@ -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'
|
||||||
# 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'
|
|
||||||
# Task
|
|
||||||
# Celery
|
# Celery
|
||||||
CELERY_BROKER_REDIS_DATABASE=1
|
CELERY_BROKER_REDIS_DATABASE=1
|
||||||
CELERY_BACKEND_REDIS_DATABASE=2
|
|
||||||
# Rabbitmq
|
# Rabbitmq
|
||||||
RABBITMQ_HOST='127.0.0.1'
|
CELERY_RABBITMQ_HOST='127.0.0.1'
|
||||||
RABBITMQ_PORT=5672
|
CELERY_RABBITMQ_PORT=5672
|
||||||
RABBITMQ_USERNAME='guest'
|
CELERY_RABBITMQ_USERNAME='guest'
|
||||||
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
-7
@@ -1,12 +1,6 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.env
|
.env
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
.mypy_cache/
|
|
||||||
log/
|
|
||||||
alembic/versions/
|
alembic/versions/
|
||||||
static/media/
|
static/media/
|
||||||
.ruff_cache/
|
*.log
|
||||||
.pytest_cache/
|
|
||||||
.python-version
|
|
||||||
celerybeat-schedule.*
|
celerybeat-schedule.*
|
||||||
|
|||||||
@@ -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
|
|
||||||
+28
-24
@@ -1,64 +1,68 @@
|
|||||||
# FBA Project - Backend
|
# FastAPI Best Architecture - Backend
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Due to Docker context limitations, you can't build an image in this directory
|
|
||||||
|
|
||||||
1. Make sure you're at the root of the project
|
1. Make sure you're at the root of the project
|
||||||
2. Run the following Docker command to build container:
|
2. Run the following Docker command to build container:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build -f backend/backend.dockerfile -t fba_backend_independent .
|
docker build -f Dockerfile -t fba_backend_independent .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start container
|
3. Start container
|
||||||
|
|
||||||
|
Native boot needs to change `127.0.0.1` in `.env` to `host.docker.internal`
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d fba_backend_independent -p 8000:8000 --name fba_app
|
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Prerequisites
|
1. Prerequisites
|
||||||
|
|
||||||
You'll need the following prerequisites:
|
- Python >= 3.10
|
||||||
- Any python version between Python >= 3.10
|
|
||||||
- Git
|
- Git
|
||||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
- Fork this repository to your GitHub account
|
- Fork this repository to your GitHub account
|
||||||
|
|
||||||
2. Installation and setup
|
2. Installation and setup
|
||||||
|
|
||||||
Clone your fork and cd into the repo directory
|
Clone your forked repository:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/<your username>/fastapi_best_architecture.git
|
git clone https://github.com/<your account>/fastapi_best_architecture.git
|
||||||
|
|
||||||
cd fastapi_best_architecture/backend
|
|
||||||
|
|
||||||
uv venv
|
|
||||||
|
|
||||||
uv pip install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Checkout a new branch and make your changes
|
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
|
```shell
|
||||||
# Checkout a new branch and make your changes
|
|
||||||
git checkout -b your-new-feature-branch
|
git checkout -b your-new-feature-branch
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run linting
|
4. Format and Lint
|
||||||
|
|
||||||
|
Auto-formatting and lint via `pre-commit`
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Run automated code formatting and linting
|
|
||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Commit and push your changes
|
5. Commit and push
|
||||||
|
|
||||||
Commit your changes, push your branch to GitHub, and create a pull request.
|
Commit your changes and push your branch to the GitHub.
|
||||||
|
|
||||||
|
6. PR
|
||||||
|
|
||||||
|
Create a PR via GitHub
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@
|
|||||||
>
|
>
|
||||||
> It is recommended to execute under the backend directory, and chmod authorization may be required
|
> 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
|
- `pre_start.sh`: Perform automatic database migration
|
||||||
|
|
||||||
- `celery-start.sh`: For celery docker script, implementation is not recommended
|
- `celery-start.sh`: For celery docker script, implementation is not recommended
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -1,2 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
from backend.common.i18n import i18n
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
__version__ = '1.8.2'
|
||||||
|
|
||||||
|
|
||||||
|
# 初始化 i18n
|
||||||
|
i18n.load_locales()
|
||||||
|
|||||||
+11
-18
@@ -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,23 +8,16 @@ 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.app import get_app_models
|
||||||
|
|
||||||
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
|
from backend.plugin.tools import get_plugin_models
|
||||||
|
|
||||||
# import your new model here
|
# import models
|
||||||
from backend.app.admin.model import * # noqa: F401
|
for cls in get_app_models() + get_plugin_models():
|
||||||
from backend.app.generator.model import * # noqa: F401
|
|
||||||
|
|
||||||
# import plugin model
|
|
||||||
for cls in get_plugin_models():
|
|
||||||
class_name = cls.__name__
|
class_name = cls.__name__
|
||||||
if class_name in globals():
|
if class_name not in globals():
|
||||||
print(f'\nWarning: Class "{class_name}" already exists in global namespace.')
|
|
||||||
else:
|
|
||||||
globals()[class_name] = cls
|
globals()[class_name] = cls
|
||||||
|
|
||||||
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
|
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
|
||||||
@@ -47,10 +37,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 +72,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.
|
||||||
|
|||||||
+29
-2
@@ -1,2 +1,29 @@
|
|||||||
#!/usr/bin/env python3
|
import os.path
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
from backend.core.path_conf import BASE_PATH
|
||||||
|
from backend.utils.import_parse import get_model_objects
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_models() -> list[type]:
|
||||||
|
"""获取 app 所有模型类"""
|
||||||
|
app_path = BASE_PATH / 'app'
|
||||||
|
list_dirs = os.listdir(app_path)
|
||||||
|
|
||||||
|
apps = [d for d in list_dirs if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__']
|
||||||
|
|
||||||
|
objs = []
|
||||||
|
|
||||||
|
for app in apps:
|
||||||
|
module_path = f'backend.app.{app}.model'
|
||||||
|
obj = get_model_objects(module_path)
|
||||||
|
if obj:
|
||||||
|
objs.extend(obj)
|
||||||
|
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
|
# import all app models for auto create db tables
|
||||||
|
for cls in get_app_models():
|
||||||
|
class_name = cls.__name__
|
||||||
|
if class_name not in globals():
|
||||||
|
globals()[class_name] = cls
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
#!/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
|
||||||
from backend.app.admin.api.v1.log import router as log_router
|
from backend.app.admin.api.v1.log import router as log_router
|
||||||
from backend.app.admin.api.v1.monitor import router as monitor_router
|
from backend.app.admin.api.v1.monitor import router as monitor_router
|
||||||
from backend.app.admin.api.v1.oauth2 import router as oauth2_router
|
|
||||||
from backend.app.admin.api.v1.sys import router as sys_router
|
from backend.app.admin.api.v1.sys import router as sys_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)
|
||||||
|
|
||||||
v1.include_router(auth_router)
|
v1.include_router(auth_router)
|
||||||
v1.include_router(oauth2_router)
|
|
||||||
v1.include_router(sys_router)
|
v1.include_router(sys_router)
|
||||||
v1.include_router(log_router)
|
v1.include_router(log_router)
|
||||||
v1.include_router(monitor_router)
|
v1.include_router(monitor_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
|
||||||
@@ -12,14 +10,17 @@ 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.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()]
|
||||||
return GetSwaggerToken(access_token=token, user=user) # type: ignore
|
) -> GetSwaggerToken:
|
||||||
|
token, user = await auth_service.swagger_login(db=db, obj=obj)
|
||||||
|
return GetSwaggerToken(access_token=token, user=user)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
@@ -28,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, response: Response) -> 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='用户登出', dependencies=[DependsJwtAuth])
|
@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,34 @@
|
|||||||
#!/usr/bin/env python3
|
from uuid import uuid4
|
||||||
# -*- 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.conf import admin_settings
|
|
||||||
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.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
|
|
||||||
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() -> ResponseSchemaModel[GetCaptchaDetail]:
|
||||||
"""
|
"""
|
||||||
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是IO密集型任务,使用线程池尽量减少性能损耗
|
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是IO密集型任务,使用线程池尽量减少性能损耗
|
||||||
"""
|
"""
|
||||||
img_type: str = 'base64'
|
img_type: str = 'base64'
|
||||||
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
|
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
|
||||||
ip = request.state.ip
|
uuid = str(uuid4())
|
||||||
await redis_client.set(
|
await redis_client.set(
|
||||||
f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}',
|
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{uuid}',
|
||||||
code,
|
code,
|
||||||
ex=admin_settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
|
ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
|
||||||
)
|
)
|
||||||
data = GetCaptchaDetail(image_type=img_type, image=img)
|
data = GetCaptchaDetail(uuid=uuid, img_type=img_type, image=img)
|
||||||
return response_base.success(data=data)
|
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,50 +1,48 @@
|
|||||||
#!/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()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取登录日志',
|
summary='分页获取登录日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_login_logs(
|
async def get_login_logs_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
username: Annotated[str | None, Query()] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
ip: Annotated[str | None, Query()] = 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)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除登录日志',
|
summary='批量删除登录日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('log:login:del')),
|
Depends(RequestPermission('log:login:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> 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(...)]) -> ResponseMode
|
|||||||
'/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,50 +1,48 @@
|
|||||||
#!/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()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取操作日志',
|
summary='分页获取操作日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_opera_logs(
|
async def get_opera_logs_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
username: Annotated[str | None, Query()] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
ip: Annotated[str | None, Query()] = 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)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除操作日志',
|
summary='批量删除操作日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('log:opera:del')),
|
Depends(RequestPermission('log:opera:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> 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(...)]) -> ResponseMode
|
|||||||
'/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,7 +1,6 @@
|
|||||||
#!/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.redis import router as redis_router
|
from backend.app.admin.api.v1.monitor.redis import router as redis_router
|
||||||
from backend.app.admin.api.v1.monitor.server import router as server_router
|
from backend.app.admin.api.v1.monitor.server import router as server_router
|
||||||
|
|
||||||
@@ -9,3 +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='/sessions', tags=['会话监控'])
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Path, Query
|
||||||
|
|
||||||
|
from backend.app.admin.schema.token import GetTokenDetail
|
||||||
|
from backend.common.enums import StatusType
|
||||||
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
|
from backend.common.security.jwt import DependsJwtAuth, DependsSuperUser, jwt_decode, revoke_token
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.database.redis import redis_client
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('', summary='获取在线用户', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_sessions(
|
||||||
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
|
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
||||||
|
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
||||||
|
online_clients = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
|
||||||
|
data: list[GetTokenDetail] = []
|
||||||
|
|
||||||
|
def append_token_detail() -> None:
|
||||||
|
data.append(
|
||||||
|
token_detail.model_copy(
|
||||||
|
update={
|
||||||
|
'username': extra_info.get('username', '未知'),
|
||||||
|
'nickname': extra_info.get('nickname', '未知'),
|
||||||
|
'ip': extra_info.get('ip', '未知'),
|
||||||
|
'os': extra_info.get('os', '未知'),
|
||||||
|
'browser': extra_info.get('browser', '未知'),
|
||||||
|
'device': extra_info.get('device', '未知'),
|
||||||
|
'last_login_time': extra_info.get('last_login_time', '未知'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in token_keys:
|
||||||
|
token = await redis_client.get(key)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
user_id = token_payload.id
|
||||||
|
session_uuid = token_payload.session_uuid
|
||||||
|
token_detail = GetTokenDetail(
|
||||||
|
id=user_id,
|
||||||
|
session_uuid=session_uuid,
|
||||||
|
username='未知',
|
||||||
|
nickname='未知',
|
||||||
|
ip='未知',
|
||||||
|
os='未知',
|
||||||
|
browser='未知',
|
||||||
|
device='未知',
|
||||||
|
status=StatusType.enable if session_uuid in online_clients else StatusType.disable,
|
||||||
|
last_login_time='未知',
|
||||||
|
expire_time=token_payload.expire_time,
|
||||||
|
)
|
||||||
|
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||||
|
if extra_info:
|
||||||
|
extra_info = json.loads(extra_info)
|
||||||
|
# 排除 swagger 登录生成的 token
|
||||||
|
if extra_info.get('swagger') is None:
|
||||||
|
if username is not None:
|
||||||
|
if username == extra_info.get('username'):
|
||||||
|
append_token_detail()
|
||||||
|
else:
|
||||||
|
append_token_detail()
|
||||||
|
else:
|
||||||
|
data.append(token_detail)
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
'/{pk}',
|
||||||
|
summary='强制下线',
|
||||||
|
dependencies=[DependsSuperUser],
|
||||||
|
)
|
||||||
|
async def delete_session(
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
|
session_uuid: Annotated[str, Query(description='会话 UUID')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
await revoke_token(pk, session_uuid)
|
||||||
|
return response_base.success()
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
#!/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 = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()}
|
data = {
|
||||||
|
'info': await redis_info.get_info(),
|
||||||
|
'stats': await redis_info.get_stats(),
|
||||||
|
}
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|||||||
@@ -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,11 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from backend.app.admin.api.v1.oauth2.github import router as github_router
|
|
||||||
from backend.app.admin.api.v1.oauth2.linux_do import router as linux_do_router
|
|
||||||
|
|
||||||
router = APIRouter(prefix='/oauth2')
|
|
||||||
|
|
||||||
router.include_router(github_router, prefix='/github', tags=['GitHub OAuth2'])
|
|
||||||
router.include_router(linux_do_router, prefix='/linux-do', tags=['LinuxDo OAuth2'])
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, Request, Response
|
|
||||||
from fastapi_limiter.depends import RateLimiter
|
|
||||||
from fastapi_oauth20 import FastAPIOAuth20, LinuxDoOAuth20
|
|
||||||
from starlette.responses import RedirectResponse
|
|
||||||
|
|
||||||
from backend.app.admin.conf import admin_settings
|
|
||||||
from backend.app.admin.service.oauth2_service import oauth2_service
|
|
||||||
from backend.common.enums import UserSocialType
|
|
||||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
_linux_do_client = LinuxDoOAuth20(
|
|
||||||
admin_settings.OAUTH2_LINUX_DO_CLIENT_ID,
|
|
||||||
admin_settings.OAUTH2_LINUX_DO_CLIENT_SECRET,
|
|
||||||
)
|
|
||||||
_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取 Linux Do 授权链接')
|
|
||||||
async def linux_do_auth2() -> ResponseSchemaModel[str]:
|
|
||||||
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
|
||||||
return response_base.success(data=auth_url)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
'/callback',
|
|
||||||
summary='Linux Do 授权自动重定向',
|
|
||||||
description='Linux Do 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户',
|
|
||||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
|
||||||
)
|
|
||||||
async def linux_do_login(
|
|
||||||
request: Request,
|
|
||||||
response: Response,
|
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
oauth2: FastAPIOAuth20 = Depends(_linux_do_oauth2),
|
|
||||||
):
|
|
||||||
token, _state = oauth2
|
|
||||||
access_token = token['access_token']
|
|
||||||
user = await _linux_do_client.get_userinfo(access_token)
|
|
||||||
data = await oauth2_service.create_with_login(
|
|
||||||
request=request,
|
|
||||||
response=response,
|
|
||||||
background_tasks=background_tasks,
|
|
||||||
user=user,
|
|
||||||
social=UserSocialType.linuxdo,
|
|
||||||
)
|
|
||||||
return RedirectResponse(url=f'{admin_settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}')
|
|
||||||
@@ -1,27 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from backend.app.admin.api.v1.sys.config import router as config_router
|
|
||||||
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.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.dict_data import router as dict_data_router
|
from backend.app.admin.api.v1.sys.files import router as file_router
|
||||||
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_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.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.token import router as token_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')
|
||||||
|
|
||||||
router.include_router(config_router, prefix='/configs', tags=['系统配置'])
|
|
||||||
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
|
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
|
||||||
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
|
|
||||||
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
|
|
||||||
router.include_router(menu_router, prefix='/menus', tags=['系统菜单'])
|
router.include_router(menu_router, prefix='/menus', tags=['系统菜单'])
|
||||||
router.include_router(role_router, prefix='/roles', tags=['系统角色'])
|
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(token_router, prefix='/tokens', 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='/plugins', tags=['系统插件'])
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
|
||||||
|
|
||||||
from backend.app.admin.schema.config import (
|
|
||||||
CreateConfigParam,
|
|
||||||
GetConfigDetail,
|
|
||||||
SaveBuiltInConfigParam,
|
|
||||||
UpdateConfigParam,
|
|
||||||
)
|
|
||||||
from backend.app.admin.service.config_service import config_service
|
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.common.security.rbac import DependsRBAC
|
|
||||||
from backend.database.db import CurrentSession
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/website', summary='获取网站配置信息', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_website_config() -> ResponseSchemaModel[GetConfigDetail]:
|
|
||||||
config = await config_service.get_built_in_config('website')
|
|
||||||
return response_base.success(data=config)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'/website',
|
|
||||||
summary='保存网站配置信息',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:config:website:add')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def save_website_config(objs: list[SaveBuiltInConfigParam]) -> ResponseModel:
|
|
||||||
await config_service.save_built_in_config(objs, 'website')
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/protocol', summary='获取用户协议', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_protocol_config() -> ResponseSchemaModel[GetConfigDetail]:
|
|
||||||
config = await config_service.get_built_in_config('protocol')
|
|
||||||
return response_base.success(data=config)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'/protocol',
|
|
||||||
summary='保存用户协议',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:config:protocol:add')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def save_protocol_config(objs: list[SaveBuiltInConfigParam]) -> ResponseModel:
|
|
||||||
await config_service.save_built_in_config(objs, 'protocol')
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/policy', summary='获取用户政策', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_policy_config() -> ResponseSchemaModel[GetConfigDetail]:
|
|
||||||
config = await config_service.get_built_in_config('policy')
|
|
||||||
return response_base.success(data=config)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'/policy',
|
|
||||||
summary='保存用户政策',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:config:policy:add')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def save_policy_config(objs: list[SaveBuiltInConfigParam]) -> ResponseModel:
|
|
||||||
await config_service.save_built_in_config(objs, 'policy')
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取系统参数配置详情', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_config(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetConfigDetail]:
|
|
||||||
config = await config_service.get(pk)
|
|
||||||
return response_base.success(data=config)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
'',
|
|
||||||
summary='(模糊条件)分页获取所有系统参数配置',
|
|
||||||
dependencies=[
|
|
||||||
DependsJwtAuth,
|
|
||||||
DependsPagination,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_pagination_config(
|
|
||||||
db: CurrentSession,
|
|
||||||
name: Annotated[str | None, Query()] = None,
|
|
||||||
type: Annotated[str | None, Query()] = None,
|
|
||||||
) -> ResponseSchemaModel[PageData[GetConfigDetail]]:
|
|
||||||
config_select = await config_service.get_select(name=name, type=type)
|
|
||||||
page_data = await paging_data(db, config_select)
|
|
||||||
return response_base.success(data=page_data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'',
|
|
||||||
summary='创建系统参数配置',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:config:add')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def create_config(obj: CreateConfigParam) -> ResponseModel:
|
|
||||||
await config_service.create(obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
'/{pk}',
|
|
||||||
summary='更新系统参数配置',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:config:edit')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -> ResponseModel:
|
|
||||||
count = await config_service.update(pk=pk, obj=obj)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
'',
|
|
||||||
summary='(批量)删除系统参数配置',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:config:del')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def delete_config(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
|
||||||
count = await config_service.delete(pk=pk)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
@@ -1,87 +1,97 @@
|
|||||||
#!/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, Depends, Path, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.data_rule import CreateDataRuleParam, GetDataRuleDetail, UpdateDataRuleParam
|
from backend.app.admin.schema.data_rule import (
|
||||||
|
CreateDataRuleParam,
|
||||||
|
DeleteDataRuleParam,
|
||||||
|
GetDataRuleColumnDetail,
|
||||||
|
GetDataRuleDetail,
|
||||||
|
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
|
||||||
from backend.utils.serializers import select_as_dict, select_list_serialize
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/models', summary='获取支持过滤的数据库模型', dependencies=[DependsJwtAuth])
|
@router.get('/models', summary='获取数据规则可用模型', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
||||||
models = await data_rule_service.get_models()
|
models = await data_rule_service.get_models()
|
||||||
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(model: Annotated[str, Path()]) -> ResponseSchemaModel[list[str]]:
|
async def get_data_rule_model_columns(
|
||||||
|
model: Annotated[str, Path(description='模型名称')],
|
||||||
|
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
||||||
models = await data_rule_service.get_columns(model=model)
|
models = await data_rule_service.get_columns(model=model)
|
||||||
return response_base.success(data=models)
|
return response_base.success(data=models)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_data_rule() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
async def get_all_data_rules(db: CurrentSession) -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
||||||
data_rules = await data_rule_service.get_all()
|
data = await data_rule_service.get_all(db=db)
|
||||||
data = select_list_serialize(data_rules)
|
|
||||||
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(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDataRuleDetail]:
|
async def get_data_rule(
|
||||||
data_rule = await data_rule_service.get(pk=pk)
|
db: CurrentSession,
|
||||||
data = GetDataRuleDetail(**select_as_dict(data_rule))
|
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||||
|
) -> ResponseSchemaModel[GetDataRuleDetail]:
|
||||||
|
data = await data_rule_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有数据权限规则',
|
summary='分页获取所有数据规则',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_data_rule(
|
async def get_data_rules_paginated(
|
||||||
db: CurrentSession, name: Annotated[str | None, Query()] = 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)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'',
|
'',
|
||||||
summary='创建数据权限规则',
|
summary='创建数据规则',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('data:rule:add')),
|
Depends(RequestPermission('data:rule:add')),
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}',
|
'/{pk}',
|
||||||
summary='更新数据权限规则',
|
summary='更新数据规则',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('data:rule:edit')),
|
Depends(RequestPermission('data:rule:edit')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRuleParam) -> ResponseModel:
|
async def update_data_rule(
|
||||||
count = await data_rule_service.update(pk=pk, obj=obj)
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||||
|
obj: UpdateDataRuleParam,
|
||||||
|
) -> ResponseModel:
|
||||||
|
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()
|
||||||
@@ -89,14 +99,14 @@ async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRulePar
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除数据权限规则',
|
summary='批量删除数据规则',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('data:rule:del')),
|
Depends(RequestPermission('data:rule:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_data_rule(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_data_rules(db: CurrentSessionTransaction, obj: DeleteDataRuleParam) -> ResponseModel:
|
||||||
count = await data_rule_service.delete(request=request, 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()
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
|
from backend.app.admin.schema.data_scope import (
|
||||||
|
CreateDataScopeParam,
|
||||||
|
DeleteDataScopeParam,
|
||||||
|
GetDataScopeDetail,
|
||||||
|
GetDataScopeWithRelationDetail,
|
||||||
|
UpdateDataScopeParam,
|
||||||
|
UpdateDataScopeRuleParam,
|
||||||
|
)
|
||||||
|
from backend.app.admin.service.data_scope_service import data_scope_service
|
||||||
|
from backend.common.pagination import DependsPagination, PageData
|
||||||
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
|
from backend.common.security.permission import RequestPermission
|
||||||
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
|
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])
|
||||||
|
async def get_data_scope(
|
||||||
|
db: CurrentSession,
|
||||||
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
) -> ResponseSchemaModel[GetDataScopeDetail]:
|
||||||
|
data = await data_scope_service.get(db=db, pk=pk)
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{pk}/rules', summary='获取数据范围所有规则', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_data_scope_rules(
|
||||||
|
db: CurrentSession,
|
||||||
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
) -> ResponseSchemaModel[GetDataScopeWithRelationDetail]:
|
||||||
|
data = await data_scope_service.get_rules(db=db, pk=pk)
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'',
|
||||||
|
summary='分页获取所有数据范围',
|
||||||
|
dependencies=[
|
||||||
|
DependsJwtAuth,
|
||||||
|
DependsPagination,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def get_data_scopes_paginated(
|
||||||
|
db: CurrentSession,
|
||||||
|
name: Annotated[str | None, Query(description='范围名称')] = None,
|
||||||
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
|
) -> ResponseSchemaModel[PageData[GetDataScopeDetail]]:
|
||||||
|
page_data = await data_scope_service.get_list(db=db, name=name, status=status)
|
||||||
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'',
|
||||||
|
summary='创建数据范围',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('data:scope:add')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def create_data_scope(db: CurrentSessionTransaction, obj: CreateDataScopeParam) -> ResponseModel:
|
||||||
|
await data_scope_service.create(db=db, obj=obj)
|
||||||
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
'/{pk}',
|
||||||
|
summary='更新数据范围',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('data:scope:edit')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def update_data_scope(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
obj: UpdateDataScopeParam,
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await data_scope_service.update(db=db, pk=pk, obj=obj)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
'/{pk}/rules',
|
||||||
|
summary='更新数据范围规则',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('data:scope:rule:edit')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def update_data_scope_rules(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
rule_ids: UpdateDataScopeRuleParam,
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await data_scope_service.update_data_scope_rule(db=db, pk=pk, rule_ids=rule_ids)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
'',
|
||||||
|
summary='批量删除数据范围',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('data:scope:del')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def delete_data_scopes(db: CurrentSessionTransaction, obj: DeleteDataScopeParam) -> ResponseModel:
|
||||||
|
count = await data_scope_service.delete(db=db, obj=obj)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
@@ -1,35 +1,36 @@
|
|||||||
#!/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, Request
|
||||||
|
|
||||||
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, UpdateDeptParam
|
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, GetDeptTree, UpdateDeptParam
|
||||||
from backend.app.admin.service.dept_service import dept_service
|
from backend.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 RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.utils.serializers import select_as_dict
|
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(...)]) -> ResponseSchemaModel[GetDeptDetail]:
|
async def get_dept(
|
||||||
dept = await dept_service.get(pk=pk)
|
db: CurrentSession, pk: Annotated[int, Path(description='部门 ID')]
|
||||||
data = GetDeptDetail(**select_as_dict(dept))
|
) -> 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_tree(
|
async def get_dept_tree(
|
||||||
name: Annotated[str | None, Query()] = None,
|
db: CurrentSession,
|
||||||
leader: Annotated[str | None, Query()] = None,
|
request: Request,
|
||||||
phone: Annotated[str | None, Query()] = None,
|
name: Annotated[str | None, Query(description='部门名称')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
leader: Annotated[str | None, Query(description='部门负责人')] = None,
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
phone: Annotated[str | None, Query(description='联系电话')] = None,
|
||||||
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status)
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
|
) -> ResponseSchemaModel[list[GetDeptTree]]:
|
||||||
|
dept = await dept_service.get_tree(db=db, request=request, name=name, leader=leader, phone=phone, status=status)
|
||||||
return response_base.success(data=dept)
|
return response_base.success(data=dept)
|
||||||
|
|
||||||
|
|
||||||
@@ -41,8 +42,8 @@ async def get_all_depts_tree(
|
|||||||
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()
|
||||||
|
|
||||||
|
|
||||||
@@ -54,8 +55,10 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_dept(pk: Annotated[int, Path(...)], 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()
|
||||||
@@ -69,8 +72,8 @@ async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> Re
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_dept(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def delete_dept(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
|
||||||
count = await dept_service.delete(request=request, 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()
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
|
||||||
|
|
||||||
from backend.app.admin.schema.dict_data import CreateDictDataParam, GetDictDataDetail, UpdateDictDataParam
|
|
||||||
from backend.app.admin.service.dict_data_service import dict_data_service
|
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.common.security.rbac import DependsRBAC
|
|
||||||
from backend.database.db import CurrentSession
|
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDictDataDetail]:
|
|
||||||
dict_data = await dict_data_service.get(pk=pk)
|
|
||||||
data = GetDictDataDetail(**select_as_dict(dict_data))
|
|
||||||
return response_base.success(data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
'',
|
|
||||||
summary='(模糊条件)分页获取所有字典',
|
|
||||||
dependencies=[
|
|
||||||
DependsJwtAuth,
|
|
||||||
DependsPagination,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_pagination_dict_datas(
|
|
||||||
db: CurrentSession,
|
|
||||||
label: Annotated[str | None, Query()] = None,
|
|
||||||
value: Annotated[str | None, Query()] = None,
|
|
||||||
status: Annotated[int | None, Query()] = None,
|
|
||||||
) -> ResponseSchemaModel[PageData[GetDictDataDetail]]:
|
|
||||||
dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status)
|
|
||||||
page_data = await paging_data(db, dict_data_select)
|
|
||||||
return response_base.success(data=page_data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'',
|
|
||||||
summary='创建字典',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:dict:data:add')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel:
|
|
||||||
await dict_data_service.create(obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
'/{pk}',
|
|
||||||
summary='更新字典',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:dict:data:edit')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataParam) -> ResponseModel:
|
|
||||||
count = await dict_data_service.update(pk=pk, obj=obj)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
'',
|
|
||||||
summary='(批量)删除字典',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:dict:data:del')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def delete_dict_data(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
|
||||||
count = await dict_data_service.delete(pk=pk)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
|
||||||
|
|
||||||
from backend.app.admin.schema.dict_type import CreateDictTypeParam, GetDictTypeDetail, UpdateDictTypeParam
|
|
||||||
from backend.app.admin.service.dict_type_service import dict_type_service
|
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.common.security.rbac import DependsRBAC
|
|
||||||
from backend.database.db import CurrentSession
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
'',
|
|
||||||
summary='(模糊条件)分页获取所有字典类型',
|
|
||||||
dependencies=[
|
|
||||||
DependsJwtAuth,
|
|
||||||
DependsPagination,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def get_pagination_dict_types(
|
|
||||||
db: CurrentSession,
|
|
||||||
name: Annotated[str | None, Query()] = None,
|
|
||||||
code: Annotated[str | None, Query()] = None,
|
|
||||||
status: Annotated[int | None, Query()] = None,
|
|
||||||
) -> ResponseSchemaModel[PageData[GetDictTypeDetail]]:
|
|
||||||
dict_type_select = await dict_type_service.get_select(name=name, code=code, status=status)
|
|
||||||
page_data = await paging_data(db, dict_type_select)
|
|
||||||
return response_base.success(data=page_data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'',
|
|
||||||
summary='创建字典类型',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:dict:type:add')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def create_dict_type(obj: CreateDictTypeParam) -> ResponseModel:
|
|
||||||
await dict_type_service.create(obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
'/{pk}',
|
|
||||||
summary='更新字典类型',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:dict:type:edit')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypeParam) -> ResponseModel:
|
|
||||||
count = await dict_type_service.update(pk=pk, obj=obj)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
'',
|
|
||||||
summary='(批量)删除字典类型',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:dict:type:del')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def delete_dict_type(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
|
||||||
count = await dict_type_service.delete(pk=pk)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
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,38 +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.utils.serializers import select_as_dict
|
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/sidebar', summary='获取用户菜单展示树', dependencies=[DependsJwtAuth])
|
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='已适配 vben admin v5', dependencies=[DependsJwtAuth])
|
||||||
async def get_user_sidebar_tree(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
async def get_user_sidebar(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||||
menu = await menu_service.get_user_menu_tree(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(...)]) -> ResponseSchemaModel[GetMenuDetail]:
|
async def get_menu(
|
||||||
menu = await menu_service.get(pk=pk)
|
db: CurrentSession, pk: Annotated[int, Path(description='菜单 ID')]
|
||||||
data = GetMenuDetail(**select_as_dict(menu))
|
) -> 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(
|
||||||
title: Annotated[str | None, Query()] = None, status: Annotated[int | None, Query()] = None
|
db: CurrentSession,
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
title: Annotated[str | None, Query(description='菜单标题')] = None,
|
||||||
menu = await menu_service.get_menu_tree(title=title, status=status)
|
status: Annotated[int | None, Query(description='状体')] = None,
|
||||||
|
) -> ResponseSchemaModel[list[GetMenuTree]]:
|
||||||
|
menu = await menu_service.get_tree(db=db, title=title, status=status)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,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()
|
||||||
|
|
||||||
|
|
||||||
@@ -57,8 +58,10 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_menu(pk: Annotated[int, Path(...)], 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()
|
||||||
@@ -72,8 +75,8 @@ async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> Re
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_menu(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def delete_menu(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
|
||||||
count = await menu_service.delete(request=request, 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()
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
||||||
|
from fastapi.params import Query
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
|
||||||
|
from backend.app.admin.service.plugin_service import plugin_service
|
||||||
|
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.security.jwt import DependsJwtAuth
|
||||||
|
from backend.common.security.permission import RequestPermission
|
||||||
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('', summary='获取所有插件', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_all_plugins() -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||||
|
plugins = await plugin_service.get_all()
|
||||||
|
return response_base.success(data=plugins)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/changed', summary='是否存在插件变更', dependencies=[DependsJwtAuth])
|
||||||
|
async def plugin_changed() -> ResponseSchemaModel[bool]:
|
||||||
|
plugins = await plugin_service.changed()
|
||||||
|
return response_base.success(data=bool(plugins))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'',
|
||||||
|
summary='安装插件',
|
||||||
|
description='使用插件 zip 压缩包或 git 仓库地址进行安装',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:plugin:install')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def install_plugin(
|
||||||
|
type: Annotated[PluginType, Query(description='插件类型')],
|
||||||
|
file: Annotated[UploadFile | None, File()] = None,
|
||||||
|
repo_url: Annotated[str | None, Query(description='插件 git 仓库地址')] = None,
|
||||||
|
) -> ResponseModel:
|
||||||
|
plugin_name = await plugin_service.install(type=type, file=file, repo_url=repo_url)
|
||||||
|
return response_base.success(
|
||||||
|
res=CustomResponse(
|
||||||
|
code=200,
|
||||||
|
msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
'/{plugin}',
|
||||||
|
summary='卸载插件',
|
||||||
|
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:plugin:uninstall')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
|
await plugin_service.uninstall(plugin=plugin)
|
||||||
|
return response_base.success(
|
||||||
|
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
'/{plugin}/status',
|
||||||
|
summary='更新插件状态',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:plugin:edit')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def update_plugin_status(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
|
await plugin_service.update_status(plugin=plugin)
|
||||||
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{plugin}', summary='下载插件', dependencies=[DependsJwtAuth])
|
||||||
|
async def download_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
|
||||||
|
bio = await plugin_service.build(plugin=plugin)
|
||||||
|
return StreamingResponse(
|
||||||
|
bio,
|
||||||
|
media_type='application/x-zip-compressed',
|
||||||
|
headers={'Content-Disposition': f'attachment; filename={plugin}.zip'},
|
||||||
|
)
|
||||||
@@ -1,78 +1,73 @@
|
|||||||
#!/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 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,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
UpdateRoleParam,
|
UpdateRoleParam,
|
||||||
UpdateRoleRuleParam,
|
UpdateRoleScopeParam,
|
||||||
)
|
)
|
||||||
from backend.app.admin.service.data_rule_service import data_rule_service
|
|
||||||
from backend.app.admin.service.menu_service import menu_service
|
|
||||||
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
|
||||||
from backend.utils.serializers import select_as_dict, select_list_serialize
|
|
||||||
|
|
||||||
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]]:
|
||||||
roles = await role_service.get_all()
|
data = await role_service.get_all(db=db)
|
||||||
data = select_list_serialize(roles)
|
|
||||||
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(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
async def get_role_menu_tree(
|
||||||
roles = await role_service.get_by_user(pk=pk)
|
db: CurrentSession,
|
||||||
data = select_list_serialize(roles)
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
return response_base.success(data=data)
|
) -> ResponseSchemaModel[list[GetMenuTree] | None]:
|
||||||
|
menu = await role_service.get_menu_tree(db=db, pk=pk)
|
||||||
|
|
||||||
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_role_all_menus(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
|
||||||
menu = await menu_service.get_role_menu_tree(pk=pk)
|
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
|
||||||
async def get_role_all_rules(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[int]]:
|
async def get_role_scopes(
|
||||||
rule = await data_rule_service.get_role_rules(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(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetRoleDetail]:
|
async def get_role(
|
||||||
role = await role_service.get(pk=pk)
|
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
|
||||||
data = GetRoleDetail(**select_as_dict(role))
|
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
|
||||||
|
data = await role_service.get(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有角色',
|
summary='分页获取所有角色',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_roles(
|
async def get_roles_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[str | None, Query()] = None,
|
name: Annotated[str | None, Query(description='角色名称')] = None,
|
||||||
status: Annotated[int | None, Query()] = 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)
|
||||||
|
|
||||||
|
|
||||||
@@ -84,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()
|
||||||
|
|
||||||
|
|
||||||
@@ -97,15 +92,17 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role(pk: Annotated[int, Path(...)], 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')),
|
||||||
@@ -113,26 +110,30 @@ async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> Re
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_menus(
|
async def update_role_menus(
|
||||||
request: Request, pk: Annotated[int, Path(...)], menu_ids: UpdateRoleMenuParam
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
menu_ids: UpdateRoleMenuParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_menu(request=request, 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}/rule',
|
'/{pk}/scopes',
|
||||||
summary='更新角色数据权限规则',
|
summary='更新角色数据范围',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:rule:edit')),
|
Depends(RequestPermission('sys:role:scope:edit')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_rules(
|
async def update_role_scopes(
|
||||||
request: Request, pk: Annotated[int, Path(...)], rule_ids: UpdateRoleRuleParam
|
db: CurrentSessionTransaction,
|
||||||
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
scope_ids: UpdateRoleScopeParam,
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_rule(request=request, pk=pk, rule_ids=rule_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()
|
||||||
@@ -140,14 +141,14 @@ async def update_role_rules(
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除角色',
|
summary='批量删除角色',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:del')),
|
Depends(RequestPermission('sys:role:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_role(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_roles(db: CurrentSessionTransaction, obj: DeleteRoleParam) -> ResponseModel:
|
||||||
count = await role_service.delete(request=request, 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,84 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import json
|
|
||||||
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
|
||||||
|
|
||||||
from backend.app.admin.schema.token import GetTokenDetail, KickOutToken
|
|
||||||
from backend.common.enums import StatusType
|
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
|
||||||
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, superuser_verify
|
|
||||||
from backend.common.security.permission import RequestPermission
|
|
||||||
from backend.common.security.rbac import DependsRBAC
|
|
||||||
from backend.core.conf import settings
|
|
||||||
from backend.database.redis import redis_client
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_tokens(username: Annotated[str | None, Query()] = None) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
|
||||||
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
|
||||||
token_online = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
|
|
||||||
data = []
|
|
||||||
for key in token_keys:
|
|
||||||
token = await redis_client.get(key)
|
|
||||||
token_payload = jwt_decode(token)
|
|
||||||
session_uuid = token_payload.session_uuid
|
|
||||||
token_detail = GetTokenDetail(
|
|
||||||
id=token_payload.id,
|
|
||||||
session_uuid=session_uuid,
|
|
||||||
username='未知',
|
|
||||||
nickname='未知',
|
|
||||||
ip='未知',
|
|
||||||
os='未知',
|
|
||||||
browser='未知',
|
|
||||||
device='未知',
|
|
||||||
status=StatusType.disable if session_uuid not in token_online else StatusType.enable,
|
|
||||||
last_login_time='未知',
|
|
||||||
expire_time=token_payload.expire_time,
|
|
||||||
)
|
|
||||||
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
|
|
||||||
if extra_info:
|
|
||||||
|
|
||||||
def append_token_detail():
|
|
||||||
data.append(
|
|
||||||
token_detail.model_copy(
|
|
||||||
update={
|
|
||||||
'username': extra_info.get('username'),
|
|
||||||
'nickname': extra_info.get('nickname'),
|
|
||||||
'ip': extra_info.get('ip'),
|
|
||||||
'os': extra_info.get('os'),
|
|
||||||
'browser': extra_info.get('browser'),
|
|
||||||
'device': extra_info.get('device'),
|
|
||||||
'last_login_time': extra_info.get('last_login_time'),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
extra_info = json.loads(extra_info)
|
|
||||||
if extra_info.get('login_type') != 'swagger':
|
|
||||||
if username:
|
|
||||||
if username == extra_info.get('username'):
|
|
||||||
append_token_detail()
|
|
||||||
else:
|
|
||||||
append_token_detail()
|
|
||||||
else:
|
|
||||||
data.append(token_detail)
|
|
||||||
return response_base.success(data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
'/{pk}',
|
|
||||||
summary='踢下线',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:token:kick')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def kick_out(request: Request, pk: Annotated[int, Path(...)], session_uuid: KickOutToken) -> ResponseModel:
|
|
||||||
superuser_verify(request)
|
|
||||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{pk}:{session_uuid}')
|
|
||||||
return response_base.success()
|
|
||||||
@@ -1,28 +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,160 +1,172 @@
|
|||||||
#!/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,
|
||||||
AvatarParam,
|
GetCurrentUserInfoWithRelationDetail,
|
||||||
GetCurrentUserInfoDetail,
|
GetUserInfoWithRelationDetail,
|
||||||
GetUserInfoDetail,
|
|
||||||
RegisterUserParam,
|
|
||||||
ResetPasswordParam,
|
ResetPasswordParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
UpdateUserRoleParam,
|
|
||||||
)
|
)
|
||||||
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
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post('/register', summary='注册用户')
|
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def register_user(obj: RegisterUserParam) -> ResponseModel:
|
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
|
||||||
await user_service.register(obj=obj)
|
data = request.user.model_dump()
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
|
||||||
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoDetail]:
|
|
||||||
await user_service.add(request=request, obj=obj)
|
|
||||||
current_user = await user_service.get_userinfo(username=obj.username)
|
|
||||||
data = GetUserInfoDetail(**select_as_dict(current_user))
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseModel:
|
async def get_userinfo(
|
||||||
count = await user_service.pwd_reset(request=request, obj=obj)
|
db: CurrentSession,
|
||||||
if count > 0:
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
return response_base.success()
|
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||||
return response_base.fail()
|
data = await user_service.get_userinfo(db=db, pk=pk)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'})
|
|
||||||
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoDetail]:
|
|
||||||
data = GetCurrentUserInfoDetail(**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}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||||
async def get_user(username: Annotated[str, Path(...)]) -> ResponseSchemaModel[GetUserInfoDetail]:
|
async def get_user_roles(
|
||||||
current_user = await user_service.get_userinfo(username=username)
|
db: CurrentSession, pk: Annotated[int, Path(description='用户 ID')]
|
||||||
data = GetUserInfoDetail(**select_as_dict(current_user))
|
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||||
|
data = await user_service.get_roles(db=db, pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
|
||||||
async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel:
|
|
||||||
count = await user_service.update(request=request, username=username, obj=obj)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
'/{username}/role',
|
|
||||||
summary='更新用户角色',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:user:role:edit')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def update_user_role(
|
|
||||||
request: Request, username: Annotated[str, Path(...)], obj: UpdateUserRoleParam
|
|
||||||
) -> ResponseModel:
|
|
||||||
await user_service.update_roles(request=request, username=username, obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtAuth])
|
|
||||||
async def update_avatar(request: Request, username: Annotated[str, Path(...)], avatar: AvatarParam) -> ResponseModel:
|
|
||||||
count = await user_service.update_avatar(request=request, username=username, avatar=avatar)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有用户',
|
summary='分页获取所有用户',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_users(
|
async def get_users_paginated(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
dept: Annotated[int | None, Query()] = None,
|
dept: Annotated[int | None, Query(description='部门 ID')] = None,
|
||||||
username: Annotated[str | None, Query()] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
phone: Annotated[str | None, Query()] = None,
|
phone: Annotated[str | None, Query(description='手机号')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetUserInfoDetail]]:
|
) -> 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(...)]) -> 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(...)]) -> 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(...)]) -> 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, request=request, 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(...)]) -> 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, request=request, nickname=nickname)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user_avatar(
|
||||||
|
db: CurrentSessionTransaction,
|
||||||
|
request: Request,
|
||||||
|
avatar: Annotated[str, Body(embed=True, description='用户头像地址')],
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update_avatar(db=db, request=request, avatar=avatar)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/me/email', summary='更新当前用户邮箱', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user_email(
|
||||||
|
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, request=request, 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(...)]) -> 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,43 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
||||||
|
|
||||||
from backend.core.path_conf import BasePath
|
|
||||||
|
|
||||||
|
|
||||||
class AdminSettings(BaseSettings):
|
|
||||||
"""Admin Settings"""
|
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
|
|
||||||
|
|
||||||
# OAuth2:https://github.com/fastapi-practices/fastapi_oauth20
|
|
||||||
# GitHub
|
|
||||||
OAUTH2_GITHUB_CLIENT_ID: str
|
|
||||||
OAUTH2_GITHUB_CLIENT_SECRET: str
|
|
||||||
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
|
|
||||||
|
|
||||||
# Linux Do
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_ID: str
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_SECRET: str
|
|
||||||
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
|
|
||||||
|
|
||||||
# Front-end redirect address
|
|
||||||
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
|
|
||||||
|
|
||||||
# Captcha
|
|
||||||
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
|
|
||||||
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒
|
|
||||||
|
|
||||||
# Config
|
|
||||||
CONFIG_BUILT_IN_TYPES: list = ['website', 'protocol', 'policy']
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def get_admin_settings() -> AdminSettings:
|
|
||||||
"""获取 admin 配置"""
|
|
||||||
return AdminSettings()
|
|
||||||
|
|
||||||
|
|
||||||
admin_settings = get_admin_settings()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.conf import admin_settings
|
|
||||||
from backend.app.admin.model import Config
|
|
||||||
from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDConfig(CRUDPlus[Config]):
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> Config | None:
|
|
||||||
"""
|
|
||||||
获取系统参数配置
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model_by_column(db, id=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES)
|
|
||||||
|
|
||||||
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
|
|
||||||
"""
|
|
||||||
通过 type 获取内置系统配置
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param type:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_models(db, type=type)
|
|
||||||
|
|
||||||
async def get_by_key_and_type(self, db: AsyncSession, key: str, type: str) -> Config | None:
|
|
||||||
"""
|
|
||||||
通过 name 和 type 获取内置系统配置
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param key:
|
|
||||||
:param type:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model_by_column(db, key=key, type=type)
|
|
||||||
|
|
||||||
async def get_by_key(self, db: AsyncSession, key: str, built_in: bool = False) -> Config | None:
|
|
||||||
"""
|
|
||||||
通过 key 获取系统配置参数
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param key:
|
|
||||||
:param built_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
filters = {'key': key}
|
|
||||||
if not built_in:
|
|
||||||
filters.update({'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES})
|
|
||||||
return await self.select_model_by_column(db, **filters)
|
|
||||||
|
|
||||||
async def get_list(self, name: str = None, type: str = None) -> Select:
|
|
||||||
"""
|
|
||||||
获取系统参数配置列表
|
|
||||||
|
|
||||||
:param name:
|
|
||||||
:param type:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
filters = {'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES}
|
|
||||||
if name is not None:
|
|
||||||
filters.update(name__like=f'%{name}%')
|
|
||||||
if type is not None:
|
|
||||||
filters.update(type__like=f'%{type}%')
|
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateConfigParam) -> None:
|
|
||||||
"""
|
|
||||||
创建 Config
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
await self.create_model(db, obj_in)
|
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateConfigParam) -> int:
|
|
||||||
"""
|
|
||||||
更新 Config
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.update_model(db, pk, obj_in)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
|
||||||
"""
|
|
||||||
删除 Config
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.delete_model_by_column(
|
|
||||||
db, allow_multiple=True, id__in=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
config_dao: CRUDConfig = CRUDConfig(Config)
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select, 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
|
||||||
@@ -12,79 +9,81 @@ from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRu
|
|||||||
|
|
||||||
|
|
||||||
class CRUDDataRule(CRUDPlus[DataRule]):
|
class CRUDDataRule(CRUDPlus[DataRule]):
|
||||||
|
"""数据规则数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
|
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
|
||||||
"""
|
"""
|
||||||
获取数据权限规则
|
获取规则详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 规则 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
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: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(noload(self.model.roles)).order_by(desc(self.model.created_time))
|
filters = {}
|
||||||
where_list = []
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
where_list.append(self.model.name.like(f'%{name}%'))
|
filters['name__like'] = f'%{name}%'
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(*where_list)
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str):
|
return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
|
||||||
|
|
||||||
|
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取数据权限规则
|
通过名称获取规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name)
|
return await self.select_model_by_column(db, name=name)
|
||||||
|
|
||||||
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
|
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
|
||||||
"""
|
"""
|
||||||
获取所有数据权限规则
|
获取所有规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDataRuleParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDataRuleParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建数据权限规则
|
创建规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建规则参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDataRuleParam) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据权限规则
|
更新规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 规则 ID
|
||||||
:param obj_in:
|
:param obj: 更新规则参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in)
|
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:
|
: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)
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import Select, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
|
from backend.app.admin.model import DataRule, DataScope
|
||||||
|
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDDataScope(CRUDPlus[DataScope]):
|
||||||
|
"""数据范围数据库操作类"""
|
||||||
|
|
||||||
|
async def get(self, db: AsyncSession, pk: int) -> DataScope | None:
|
||||||
|
"""
|
||||||
|
获取数据范围详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
|
async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None:
|
||||||
|
"""
|
||||||
|
通过名称获取数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param name: 范围名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_model_by_column(db, name=name)
|
||||||
|
|
||||||
|
async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope:
|
||||||
|
"""
|
||||||
|
获取数据范围关联数据
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_model(db, pk, load_strategies=['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 status: 范围状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
filters['name__like'] = f'%{name}%'
|
||||||
|
if status is not None:
|
||||||
|
filters['status'] = status
|
||||||
|
|
||||||
|
return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters)
|
||||||
|
|
||||||
|
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
||||||
|
"""
|
||||||
|
创建数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 创建数据范围参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int:
|
||||||
|
"""
|
||||||
|
更新数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:param obj: 更新数据范围参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
|
async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
|
||||||
|
"""
|
||||||
|
更新数据范围规则
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:param rule_ids: 数据规则 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
current_data_scope = await self.get_with_relation(db, pk)
|
||||||
|
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
|
||||||
|
rules = await db.execute(stmt)
|
||||||
|
current_data_scope.rules = rules.scalars().all()
|
||||||
|
return len(current_data_scope.rules)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
data_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
|
||||||
@@ -1,117 +1,121 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
from fastapi import Request
|
||||||
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 Dept, User
|
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.security.permission import filter_data_permission
|
||||||
|
|
||||||
|
|
||||||
class CRUDDept(CRUDPlus[Dept]):
|
class CRUDDept(CRUDPlus[Dept]):
|
||||||
|
"""部门数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
获取部门
|
获取部门详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_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=0)
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取 API
|
通过名称获取部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
: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=0)
|
||||||
|
|
||||||
async def get_all(
|
async def get_all(
|
||||||
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: int = None
|
self,
|
||||||
|
request: Request,
|
||||||
|
db: AsyncSession,
|
||||||
|
name: str | None,
|
||||||
|
leader: str | None,
|
||||||
|
phone: str | None,
|
||||||
|
status: int | None,
|
||||||
) -> Sequence[Dept]:
|
) -> Sequence[Dept]:
|
||||||
"""
|
"""
|
||||||
获取所有部门
|
获取所有部门
|
||||||
|
|
||||||
:param db:
|
:param request: FastAPI 请求对象
|
||||||
:param name:
|
:param db: 数据库会话
|
||||||
:param leader:
|
:param name: 部门名称
|
||||||
:param phone:
|
:param leader: 负责人
|
||||||
:param status:
|
:param phone: 联系电话
|
||||||
|
:param status: 部门状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {'del_flag__eq': 0}
|
filters = {'del_flag': 0}
|
||||||
if name is not None:
|
|
||||||
filters.update(name__like=f'%{name}%')
|
|
||||||
if leader is not None:
|
|
||||||
filters.update(leader__like=f'%{leader}%')
|
|
||||||
if phone is not None:
|
|
||||||
filters.update(phone__startswith=phone)
|
|
||||||
if status is not None:
|
|
||||||
filters.update(status=status)
|
|
||||||
return await self.select_models_order(db, sort_columns='sort', **filters)
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDeptParam) -> None:
|
if name is not None:
|
||||||
|
filters['name__like'] = f'%{name}%'
|
||||||
|
if leader is not None:
|
||||||
|
filters['leader__like'] = f'%{leader}%'
|
||||||
|
if phone is not None:
|
||||||
|
filters['phone__startswith'] = phone
|
||||||
|
if status is not None:
|
||||||
|
filters['status'] = status
|
||||||
|
|
||||||
|
data_filtered = await filter_data_permission(db, request)
|
||||||
|
return await self.select_models_order(db, 'sort', 'desc', data_filtered, **filters)
|
||||||
|
|
||||||
|
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建部门
|
创建部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建部门参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, dept_id: int, obj_in: UpdateDeptParam) -> int:
|
async def update(self, db: AsyncSession, dept_id: int, obj: UpdateDeptParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新部门
|
更新部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:param obj_in:
|
:param obj: 更新部门参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, dept_id, obj_in)
|
return await self.update_model(db, dept_id, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, dept_id: int) -> int:
|
async def delete(self, db: AsyncSession, dept_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除部门
|
删除部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
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) -> list[User]:
|
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
获取关联
|
获取部门及关联数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
|
return await self.select_model(db, dept_id, load_strategies=['users'])
|
||||||
result = await db.execute(stmt)
|
|
||||||
user_relation = result.scalars().first()
|
|
||||||
return user_relation.users
|
|
||||||
|
|
||||||
async def get_children(self, db: AsyncSession, dept_id: int) -> list[Dept]:
|
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
|
||||||
"""
|
"""
|
||||||
获取子部门
|
获取子部门列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == dept_id)
|
return await self.select_models(db, parent_id=dept_id, del_flag=0)
|
||||||
result = await db.execute(stmt)
|
|
||||||
dept = result.scalars().first()
|
|
||||||
return dept.children
|
|
||||||
|
|
||||||
|
|
||||||
dept_dao: CRUDDept = CRUDDept(Dept)
|
dept_dao: CRUDDept = CRUDDept(Dept)
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
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 DictData
|
|
||||||
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDDictData(CRUDPlus[DictData]):
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> DictData | None:
|
|
||||||
"""
|
|
||||||
获取字典数据
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model(db, pk)
|
|
||||||
|
|
||||||
async def get_list(self, label: str = None, value: str = None, status: int = None) -> Select:
|
|
||||||
"""
|
|
||||||
获取所有字典数据
|
|
||||||
|
|
||||||
:param label:
|
|
||||||
:param value:
|
|
||||||
:param status:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
|
|
||||||
where_list = []
|
|
||||||
if label is not None:
|
|
||||||
where_list.append(self.model.label.like(f'%{label}%'))
|
|
||||||
if value is not None:
|
|
||||||
where_list.append(self.model.value.like(f'%{value}%'))
|
|
||||||
if status is not None:
|
|
||||||
where_list.append(self.model.status == status)
|
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(and_(*where_list))
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
|
|
||||||
"""
|
|
||||||
通过 label 获取字典数据
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param label:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model_by_column(db, label=label)
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDictDataParam) -> None:
|
|
||||||
"""
|
|
||||||
创建数据字典
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
await self.create_model(db, obj_in)
|
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictDataParam) -> int:
|
|
||||||
"""
|
|
||||||
更新数据字典
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.update_model(db, pk, obj_in)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
|
||||||
"""
|
|
||||||
删除字典数据
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
|
|
||||||
"""
|
|
||||||
获取字典数据和类型
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk)
|
|
||||||
dict_data = await db.execute(stmt)
|
|
||||||
return dict_data.scalars().first()
|
|
||||||
|
|
||||||
|
|
||||||
dict_data_dao: CRUDDictData = CRUDDictData(DictData)
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.model import DictType
|
|
||||||
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDDictType(CRUDPlus[DictType]):
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> DictType | None:
|
|
||||||
"""
|
|
||||||
获取字典类型
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model(db, pk)
|
|
||||||
|
|
||||||
async def get_list(self, *, name: str = None, code: str = None, status: int = None) -> Select:
|
|
||||||
"""
|
|
||||||
获取所有字典类型
|
|
||||||
|
|
||||||
:param name:
|
|
||||||
:param code:
|
|
||||||
:param status:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
filters = {}
|
|
||||||
if name is not None:
|
|
||||||
filters.update(name__like=f'%{name}%')
|
|
||||||
if code is not None:
|
|
||||||
filters.update(code__like=f'%{code}%')
|
|
||||||
if status is not None:
|
|
||||||
filters.update(status=status)
|
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
|
||||||
|
|
||||||
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
|
|
||||||
"""
|
|
||||||
通过 code 获取字典类型
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param code:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model_by_column(db, code=code)
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDictTypeParam) -> None:
|
|
||||||
"""
|
|
||||||
创建字典类型
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
await self.create_model(db, obj_in)
|
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictTypeParam) -> int:
|
|
||||||
"""
|
|
||||||
更新字典类型
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.update_model(db, pk, obj_in)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
|
||||||
"""
|
|
||||||
删除字典类型
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
|
||||||
|
|
||||||
|
|
||||||
dict_type_dao: CRUDDictType = CRUDDictType(DictType)
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -9,52 +8,57 @@ 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 = None, status: int | None = None, ip: str | None = None) -> Select:
|
"""登录日志数据库操作类"""
|
||||||
"""
|
|
||||||
获取登录日志列表
|
|
||||||
|
|
||||||
:param username:
|
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
||||||
:param status:
|
"""
|
||||||
:param ip:
|
获取登录日志列表查询表达式
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:param status: 登录状态
|
||||||
|
:param ip: IP 地址
|
||||||
: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_in: CreateLoginLogParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建登录日志
|
创建登录日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建登录日志参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in, 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:
|
: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,9 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import and_, asc, select
|
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
|
||||||
@@ -11,101 +8,101 @@ from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
|||||||
|
|
||||||
|
|
||||||
class CRUDMenu(CRUDPlus[Menu]):
|
class CRUDMenu(CRUDPlus[Menu]):
|
||||||
async def get(self, db, menu_id: int) -> Menu | None:
|
"""菜单数据库操作类"""
|
||||||
"""
|
|
||||||
获取菜单
|
|
||||||
|
|
||||||
:param db:
|
async def get(self, db: AsyncSession, menu_id: int) -> Menu | None:
|
||||||
:param menu_id:
|
"""
|
||||||
|
获取菜单详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, menu_id)
|
return await self.select_model(db, menu_id)
|
||||||
|
|
||||||
async def get_by_title(self, db, title: str) -> Menu | None:
|
async def get_by_title(self, db: AsyncSession, title: str) -> Menu | None:
|
||||||
"""
|
"""
|
||||||
通过 title 获取菜单
|
通过标题获取菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param title:
|
:param title: 菜单标题
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, title=title, menu_type__ne=2)
|
return await self.select_model_by_column(db, title=title, type__ne=2)
|
||||||
|
|
||||||
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
|
async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
|
||||||
"""
|
"""
|
||||||
获取所有菜单
|
获取菜单列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param title:
|
:param title: 菜单标题
|
||||||
:param status:
|
:param status: 菜单状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if title is not None:
|
if title is not None:
|
||||||
filters.update(title=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_role_menus(self, db, superuser: bool, menu_ids: list[int]) -> 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:
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).order_by(asc(self.model.sort))
|
filters = {'type__in': [0, 1, 3, 4]}
|
||||||
where_list = [self.model.menu_type.in_([0, 1])]
|
|
||||||
if not superuser:
|
|
||||||
where_list.append(self.model.id.in_(menu_ids))
|
|
||||||
stmt = stmt.where(and_(*where_list))
|
|
||||||
menu = await db.execute(stmt)
|
|
||||||
return menu.scalars().all()
|
|
||||||
|
|
||||||
async def create(self, db, obj_in: CreateMenuParam) -> None:
|
if menu_ids:
|
||||||
|
filters['id__in'] = menu_ids
|
||||||
|
|
||||||
|
return await self.select_models_order(db, 'sort', 'asc', **filters)
|
||||||
|
|
||||||
|
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建菜单
|
创建菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建菜单参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db, menu_id: int, obj_in: UpdateMenuParam) -> int:
|
async def update(self, db: AsyncSession, menu_id: int, obj: UpdateMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新菜单
|
更新菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param menu_id:
|
:param menu_id: 菜单 ID
|
||||||
:param obj_in:
|
:param obj: 更新菜单参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, menu_id, obj_in)
|
return await self.update_model(db, menu_id, obj)
|
||||||
|
|
||||||
async def delete(self, db, menu_id: int) -> int:
|
async def delete(self, db: AsyncSession, menu_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除菜单
|
删除菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param menu_id:
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, menu_id)
|
return await self.delete_model(db, menu_id)
|
||||||
|
|
||||||
async def get_children(self, db, menu_id: int) -> list[Menu]:
|
async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None]:
|
||||||
"""
|
"""
|
||||||
获取子菜单
|
获取子菜单列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param menu_id:
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
|
menu = await self.select_model(db, menu_id, load_strategies=['children'])
|
||||||
result = await db.execute(stmt)
|
|
||||||
menu = result.scalars().first()
|
|
||||||
return menu.children
|
return menu.children
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -9,52 +8,67 @@ 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 = None, status: int | None = None, ip: str | None = None) -> Select:
|
"""操作日志数据库操作类"""
|
||||||
"""
|
|
||||||
获取操作日志列表
|
|
||||||
|
|
||||||
:param username:
|
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
||||||
:param status:
|
"""
|
||||||
:param ip:
|
获取操作日志列表查询表达式
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:param status: 操作状态
|
||||||
|
:param ip: IP 地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if username is not None:
|
if username is not None:
|
||||||
filters.update(username=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=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_in: CreateOperaLogParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建操作日志
|
创建操作日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 操作日志创建参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
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:
|
: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 db: 数据库会话
|
||||||
|
:param pks: 操作日志 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_all(db: AsyncSession) -> None:
|
||||||
|
"""
|
||||||
|
删除所有日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
await db.execute(sa_delete(OperaLog))
|
||||||
|
|
||||||
|
|
||||||
opera_log_dao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog)
|
opera_log_dao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog)
|
||||||
|
|||||||
@@ -1,162 +1,146 @@
|
|||||||
#!/usr/bin/env python3
|
from collections.abc import Sequence
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select, desc, select
|
from sqlalchemy import Select, select
|
||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.app.admin.model import DataRule, Menu, Role, User
|
from backend.app.admin.model import DataScope, Menu, Role
|
||||||
from backend.app.admin.schema.role import (
|
from backend.app.admin.schema.role import (
|
||||||
CreateRoleParam,
|
CreateRoleParam,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
UpdateRoleParam,
|
UpdateRoleParam,
|
||||||
UpdateRoleRuleParam,
|
UpdateRoleScopeParam,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CRUDRole(CRUDPlus[Role]):
|
class CRUDRole(CRUDPlus[Role]):
|
||||||
async def get(self, db, role_id: int) -> Role | None:
|
"""角色数据库操作类"""
|
||||||
"""
|
|
||||||
获取角色
|
|
||||||
|
|
||||||
:param db:
|
async def get(self, db: AsyncSession, role_id: int) -> Role | None:
|
||||||
:param role_id:
|
"""
|
||||||
|
获取角色详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param role_id: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, role_id)
|
return await self.select_model(db, role_id)
|
||||||
|
|
||||||
async def get_with_relation(self, db, role_id: int) -> Role | None:
|
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
|
||||||
"""
|
"""
|
||||||
获取角色和菜单
|
获取角色及关联数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
return await self.select_model(db, role_id, load_strategies=['menus', 'scopes'])
|
||||||
select(self.model)
|
|
||||||
.options(selectinload(self.model.menus), selectinload(self.model.rules))
|
|
||||||
.where(self.model.id == role_id)
|
|
||||||
)
|
|
||||||
role = await db.execute(stmt)
|
|
||||||
return role.scalars().first()
|
|
||||||
|
|
||||||
async def get_all(self, db) -> Sequence[Role]:
|
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
|
||||||
"""
|
"""
|
||||||
获取所有角色
|
获取所有角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def get_by_user(self, db, user_id: int) -> Sequence[Role]:
|
async def get_select(self, name: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取用户所有角色
|
获取角色列表查询表达式
|
||||||
|
|
||||||
:param db:
|
:param name: 角色名称
|
||||||
:param user_id:
|
:param status: 角色状态
|
||||||
:return:
|
: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:
|
filters = {}
|
||||||
"""
|
|
||||||
获取角色列表
|
|
||||||
|
|
||||||
:param name:
|
if name is not None:
|
||||||
:param status:
|
filters['name__like'] = f'%{name}%'
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stmt = (
|
|
||||||
select(self.model)
|
|
||||||
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.rules))
|
|
||||||
.order_by(desc(self.model.created_time))
|
|
||||||
)
|
|
||||||
where_list = []
|
|
||||||
if name:
|
|
||||||
where_list.append(self.model.name.like(f'%{name}%'))
|
|
||||||
if status is not None:
|
if status is not None:
|
||||||
where_list.append(self.model.status == status)
|
filters['status'] = status
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(*where_list)
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_by_name(self, db, name: str) -> Role | None:
|
return await self.select_order(
|
||||||
|
'id',
|
||||||
|
load_strategies={
|
||||||
|
'users': 'noload',
|
||||||
|
'menus': 'noload',
|
||||||
|
'scopes': 'noload',
|
||||||
|
},
|
||||||
|
**filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取角色
|
通过名称获取角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 角色名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name)
|
return await self.select_model_by_column(db, name=name)
|
||||||
|
|
||||||
async def create(self, db, obj_in: CreateRoleParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateRoleParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建角色
|
创建角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建角色参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db, role_id: int, obj_in: UpdateRoleParam) -> int:
|
async def update(self, db: AsyncSession, role_id: int, obj: UpdateRoleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色
|
更新角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:param obj_in:
|
:param obj: 更新角色参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, role_id, obj_in)
|
return await self.update_model(db, role_id, obj)
|
||||||
|
|
||||||
async def update_menus(self, db, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色菜单
|
更新角色菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:param menu_ids:
|
:param menu_ids: 菜单 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_role = await self.get_with_relation(db, role_id)
|
current_role = await self.get_with_relation(db, role_id)
|
||||||
# 更新菜单
|
|
||||||
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
|
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
|
||||||
menus = await db.execute(stmt)
|
menus = await db.execute(stmt)
|
||||||
current_role.menus = menus.scalars().all()
|
current_role.menus = menus.scalars().all()
|
||||||
return len(current_role.menus)
|
return len(current_role.menus)
|
||||||
|
|
||||||
async def update_rules(self, db, role_id: int, rule_ids: UpdateRoleRuleParam) -> int:
|
async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色数据权限
|
更新角色数据范围
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:param rule_ids:
|
:param scope_ids: 权限范围 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_role = await self.get_with_relation(db, role_id)
|
current_role = await self.get_with_relation(db, role_id)
|
||||||
# 更新数据权限
|
stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
|
||||||
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
|
scopes = await db.execute(stmt)
|
||||||
rules = await db.execute(stmt)
|
current_role.scopes = scopes.scalars().all()
|
||||||
current_role.rules = rules.scalars().all()
|
return len(current_role.scopes)
|
||||||
return len(current_role.rules)
|
|
||||||
|
|
||||||
async def delete(self, db, role_id: list[int]) -> int:
|
async def delete(self, db: AsyncSession, role_ids: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除角色
|
批量删除角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_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)
|
||||||
|
|||||||
+153
-184
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
from sqlalchemy import and_, desc, select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy.orm import noload, selectinload
|
||||||
from sqlalchemy.sql import Select
|
from sqlalchemy.sql import Select
|
||||||
@@ -10,83 +8,63 @@ from sqlalchemy_crud_plus import CRUDPlus
|
|||||||
|
|
||||||
from backend.app.admin.model import Dept, Role, User
|
from backend.app.admin.model import Dept, Role, User
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
|
AddOAuth2UserParam,
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
AvatarParam,
|
|
||||||
RegisterUserParam,
|
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
UpdateUserRoleParam,
|
|
||||||
)
|
)
|
||||||
from backend.common.security.jwt import get_hash_password
|
from backend.common.security.jwt import get_hash_password
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
class CRUDUser(CRUDPlus[User]):
|
class CRUDUser(CRUDPlus[User]):
|
||||||
|
"""用户数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, user_id: int) -> User | None:
|
async def get(self, db: AsyncSession, user_id: int) -> User | None:
|
||||||
"""
|
"""
|
||||||
获取用户
|
获取用户详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, user_id)
|
return await self.select_model(db, user_id)
|
||||||
|
|
||||||
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
|
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
通过 username 获取用户
|
通过用户名获取用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, username=username)
|
return await self.select_model_by_column(db, username=username)
|
||||||
|
|
||||||
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
|
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
通过 nickname 获取用户
|
通过昵称获取用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param nickname:
|
:param nickname: 用户昵称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
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 update_login_time(self, db: AsyncSession, username: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户登录时间
|
更新用户最后登录时间
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool = False) -> None:
|
|
||||||
"""
|
|
||||||
创建用户
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param obj:
|
|
||||||
:param social: 社交用户,适配 oauth 2.0
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not social:
|
|
||||||
salt = bcrypt.gensalt()
|
|
||||||
obj.password = get_hash_password(obj.password, salt)
|
|
||||||
dict_obj = obj.model_dump()
|
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
后台添加用户
|
添加用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj:
|
:param obj: 添加用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
@@ -94,227 +72,218 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
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)
|
||||||
role_list = []
|
|
||||||
for role_id in obj.roles:
|
stmt = select(Role).where(Role.id.in_(obj.roles))
|
||||||
role_list.append(await db.get(Role, role_id))
|
roles = await db.execute(stmt)
|
||||||
new_user.roles.extend(role_list)
|
new_user.roles = roles.scalars().all()
|
||||||
|
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
|
|
||||||
async def update_userinfo(self, db: AsyncSession, input_user: int, obj: UpdateUserParam) -> int:
|
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)
|
||||||
|
|
||||||
|
stmt = select(Role)
|
||||||
|
role = await db.execute(stmt)
|
||||||
|
new_user.roles = [role.scalars().first()] # 默认绑定第一个角色
|
||||||
|
|
||||||
|
db.add(new_user)
|
||||||
|
|
||||||
|
async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户信息
|
更新用户信息
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param input_user:
|
:param input_user: 用户 ID
|
||||||
:param obj:
|
:param obj: 更新用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, input_user, obj)
|
role_ids = obj.roles
|
||||||
|
del obj.roles
|
||||||
|
count = await self.update_model(db, input_user.id, obj)
|
||||||
|
|
||||||
@staticmethod
|
stmt = select(Role).where(Role.id.in_(role_ids))
|
||||||
async def update_role(db: AsyncSession, input_user: User, obj: UpdateUserRoleParam) -> None:
|
roles = await db.execute(stmt)
|
||||||
|
input_user.roles = roles.scalars().all()
|
||||||
|
return count
|
||||||
|
|
||||||
|
async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户角色
|
更新用户昵称
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param input_user:
|
:param user_id: 用户 ID
|
||||||
:param obj:
|
:param nickname: 用户昵称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 删除用户所有角色
|
return await self.update_model(db, user_id, {'nickname': nickname})
|
||||||
for i in list(input_user.roles):
|
|
||||||
input_user.roles.remove(i)
|
|
||||||
# 添加用户角色
|
|
||||||
role_list = []
|
|
||||||
for role_id in obj.roles:
|
|
||||||
role_list.append(await db.get(Role, role_id))
|
|
||||||
input_user.roles.extend(role_list)
|
|
||||||
|
|
||||||
async def update_avatar(self, db: AsyncSession, input_user: int, avatar: AvatarParam) -> int:
|
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户头像
|
更新用户头像
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param input_user:
|
:param user_id: 用户 ID
|
||||||
:param avatar:
|
:param avatar: 头像地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, input_user, {'avatar': avatar.url})
|
return await self.update_model(db, user_id, {'avatar': avatar})
|
||||||
|
|
||||||
|
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
|
||||||
|
"""
|
||||||
|
更新用户邮箱
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_id: 用户 ID
|
||||||
|
:param email: 邮箱
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.update_model(db, user_id, {'email': email})
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除用户
|
删除用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, user_id)
|
return await self.delete_model(db, user_id)
|
||||||
|
|
||||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
检查邮箱是否存在
|
检查邮箱是否已被绑定
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param email:
|
:param email: 电子邮箱
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, email=email)
|
return await self.select_model_by_column(db, email=email)
|
||||||
|
|
||||||
async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int:
|
async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
|
||||||
"""
|
"""
|
||||||
重置用户密码
|
重置用户密码
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
: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})
|
||||||
|
|
||||||
async def get_list(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select:
|
async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取用户列表
|
获取用户列表查询表达式
|
||||||
|
|
||||||
:param dept:
|
:param dept: 部门 ID
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:param phone:
|
:param phone: 电话号码
|
||||||
:param status:
|
:param status: 用户状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
filters = {}
|
||||||
select(self.model)
|
|
||||||
.options(
|
|
||||||
selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)),
|
|
||||||
noload(self.model.socials),
|
|
||||||
selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.rules)),
|
|
||||||
)
|
|
||||||
.order_by(desc(self.model.join_time))
|
|
||||||
)
|
|
||||||
where_list = []
|
|
||||||
if dept:
|
if dept:
|
||||||
where_list.append(self.model.dept_id == dept)
|
filters['dept_id'] = dept
|
||||||
if username:
|
if username:
|
||||||
where_list.append(self.model.username.like(f'%{username}%'))
|
filters['username__like'] = f'%{username}%'
|
||||||
if phone:
|
if phone:
|
||||||
where_list.append(self.model.phone.like(f'%{phone}%'))
|
filters['phone__like'] = f'%{phone}%'
|
||||||
if status is not None:
|
if status is not None:
|
||||||
where_list.append(self.model.status == status)
|
filters['status'] = status
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(and_(*where_list))
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
async def get_super(self, db: AsyncSession, user_id: int) -> bool:
|
return await self.select_order(
|
||||||
|
'id',
|
||||||
|
'desc',
|
||||||
|
load_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)),
|
||||||
|
],
|
||||||
|
**filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_super(self, db: AsyncSession, user_id: int, *, is_super: bool) -> int:
|
||||||
"""
|
"""
|
||||||
获取用户超级管理员状态
|
设置用户超级管理员状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
|
:param is_super: 是否超级管理员
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
user = await self.get(db, user_id)
|
return await self.update_model(db, user_id, {'is_superuser': is_super})
|
||||||
return user.is_superuser
|
|
||||||
|
|
||||||
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
|
async def set_staff(self, db: AsyncSession, user_id: int, *, is_staff: bool) -> int:
|
||||||
"""
|
"""
|
||||||
获取用户后台登录状态
|
设置用户后台登录状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
|
:param is_staff: 是否可登录后台
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
user = await self.get(db, user_id)
|
return await self.update_model(db, user_id, {'is_staff': is_staff})
|
||||||
return user.is_staff
|
|
||||||
|
|
||||||
async def get_status(self, db: AsyncSession, user_id: int) -> int:
|
async def set_status(self, db: AsyncSession, user_id: int, status: int) -> int:
|
||||||
"""
|
|
||||||
获取用户状态
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param user_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:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
user = await self.get(db, user_id)
|
|
||||||
return user.is_multi_login
|
|
||||||
|
|
||||||
async def set_super(self, db: AsyncSession, user_id: int, _super: bool) -> int:
|
|
||||||
"""
|
|
||||||
设置用户超级管理员
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param user_id:
|
|
||||||
:param _super:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.update_model(db, user_id, {'is_superuser': _super})
|
|
||||||
|
|
||||||
async def set_staff(self, db: AsyncSession, user_id: int, staff: bool) -> int:
|
|
||||||
"""
|
|
||||||
设置用户后台登录
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param user_id:
|
|
||||||
:param staff:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.update_model(db, user_id, {'is_staff': staff})
|
|
||||||
|
|
||||||
async def set_status(self, db: AsyncSession, user_id: int, status: bool) -> int:
|
|
||||||
"""
|
"""
|
||||||
设置用户状态
|
设置用户状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param status:
|
:param status: 状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
设置用户多点登录
|
设置用户多端登录状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param multi_login:
|
:param multi_login: 是否允许多端登录
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
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(self, db: AsyncSession, *, user_id: int = None, username: str = None) -> User | None:
|
async def get_with_relation(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
user_id: int | None = None,
|
||||||
|
username: str | None = None,
|
||||||
|
) -> User | None:
|
||||||
"""
|
"""
|
||||||
获取用户和(部门,角色,菜单,规则)
|
获取用户关联信息
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
: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.rules),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
filters = []
|
|
||||||
if user_id:
|
if user_id:
|
||||||
filters.append(self.model.id == user_id)
|
filters['id'] = user_id
|
||||||
if username:
|
if username:
|
||||||
filters.append(self.model.username == username)
|
filters['username'] = username
|
||||||
user = await db.execute(stmt.where(*filters))
|
|
||||||
return user.scalars().first()
|
return await self.select_model_by_column(
|
||||||
|
db,
|
||||||
|
load_options=[selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes))],
|
||||||
|
load_strategies=['dept'],
|
||||||
|
**filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
user_dao: CRUDUser = CRUDUser(User)
|
user_dao: CRUDUser = CRUDUser(User)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
|
||||||
|
|
||||||
from backend.app.admin.model import UserSocial
|
|
||||||
from backend.app.admin.schema.user_social import CreateUserSocialParam
|
|
||||||
from backend.common.enums import UserSocialType
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDOUserSocial(CRUDPlus[UserSocial]):
|
|
||||||
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
|
|
||||||
"""
|
|
||||||
获取用户社交账号绑定
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param pk:
|
|
||||||
:param source:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.select_model_by_column(db, user_id=pk, source=source)
|
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateUserSocialParam) -> None:
|
|
||||||
"""
|
|
||||||
创建用户社交账号绑定
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param obj_in:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
await self.create_model(db, obj_in)
|
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, social_id: int) -> int:
|
|
||||||
"""
|
|
||||||
删除用户社交账号绑定
|
|
||||||
|
|
||||||
:param db:
|
|
||||||
:param social_id:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await self.delete_model(db, social_id)
|
|
||||||
|
|
||||||
|
|
||||||
user_social_dao: CRUDOUserSocial = CRUDOUserSocial(UserSocial)
|
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
#!/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.config import Config
|
from backend.app.admin.model.dept import Dept as Dept
|
||||||
from backend.app.admin.model.data_rule import DataRule
|
from backend.app.admin.model.login_log import LoginLog as LoginLog
|
||||||
from backend.app.admin.model.dept import Dept
|
from backend.app.admin.model.menu import Menu as Menu
|
||||||
from backend.app.admin.model.dict_data import DictData
|
from backend.app.admin.model.opera_log import OperaLog as OperaLog
|
||||||
from backend.app.admin.model.dict_type import DictType
|
from backend.app.admin.model.role import Role as Role
|
||||||
from backend.app.admin.model.login_log import LoginLog
|
from backend.app.admin.model.user import User as User
|
||||||
from backend.app.admin.model.menu import Menu
|
|
||||||
from backend.app.admin.model.opera_log import OperaLog
|
|
||||||
from backend.app.admin.model.role import Role
|
|
||||||
from backend.app.admin.model.user import User
|
|
||||||
from backend.app.admin.model.user_social import UserSocial
|
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
#!/usr/bin/env python3
|
from __future__ import annotations
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_rule
|
from backend.app.admin.model.m2m import sys_data_scope_rule
|
||||||
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):
|
||||||
"""数据权限规则表"""
|
"""数据规则表"""
|
||||||
|
|
||||||
__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(255), unique=True, comment='规则名称')
|
name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
|
||||||
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型类')
|
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
|
||||||
column: Mapped[str] = mapped_column(String(20), comment='数据库字段')
|
column: Mapped[str] = mapped_column(String(20), 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(String(255), comment='规则值')
|
||||||
|
|
||||||
# 角色规则多对多
|
# 数据范围规则多对多
|
||||||
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules') # noqa: F821
|
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules')
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from backend.app.admin.model.m2m import sys_data_scope_rule, sys_role_data_scope
|
||||||
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import DataRule, Role
|
||||||
|
|
||||||
|
|
||||||
|
class DataScope(Base):
|
||||||
|
"""数据范围表"""
|
||||||
|
|
||||||
|
__tablename__ = 'sys_data_scope'
|
||||||
|
|
||||||
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
|
||||||
|
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
||||||
|
|
||||||
|
# 数据范围规则多对多
|
||||||
|
rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='scopes')
|
||||||
|
|
||||||
|
# 角色数据范围多对多
|
||||||
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes')
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
from __future__ import annotations
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from sqlalchemy import Boolean, ForeignKey, String
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
|
||||||
from sqlalchemy.dialects.postgresql import INTEGER
|
from sqlalchemy.dialects.postgresql import INTEGER
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
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):
|
||||||
"""部门表"""
|
"""部门表"""
|
||||||
@@ -22,15 +25,21 @@ class Dept(Base):
|
|||||||
email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(String(50), 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(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='删除标志(0删除 1存在)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='删除标志(0删除 1存在)',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 父级部门一对多
|
# 父级部门一对多
|
||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
|
BigInteger,
|
||||||
|
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||||
|
default=None,
|
||||||
|
index=True,
|
||||||
|
comment='父部门ID',
|
||||||
)
|
)
|
||||||
parent: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='children', remote_side=[id])
|
parent: Mapped[Dept | None] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||||
children: Mapped[list['Dept'] | None] = relationship(init=False, back_populates='parent')
|
children: Mapped[list[Dept] | None] = relationship(init=False, back_populates='parent')
|
||||||
|
|
||||||
# 部门用户一对多
|
# 部门用户一对多
|
||||||
users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821
|
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import ForeignKey, String
|
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.common.model import Base, id_key
|
|
||||||
|
|
||||||
|
|
||||||
class DictData(Base):
|
|
||||||
"""字典数据"""
|
|
||||||
|
|
||||||
__tablename__ = 'sys_dict_data'
|
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
|
||||||
label: Mapped[str] = mapped_column(String(32), unique=True, comment='字典标签')
|
|
||||||
value: Mapped[str] = mapped_column(String(32), unique=True, comment='字典值')
|
|
||||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
|
||||||
remark: Mapped[str | None] = mapped_column(
|
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 字典类型一对多
|
|
||||||
type_id: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey('sys_dict_type.id', ondelete='CASCADE'), default=0, comment='字典类型关联ID'
|
|
||||||
)
|
|
||||||
type: Mapped['DictType'] = relationship(init=False, back_populates='datas') # noqa: F821
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
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, id_key
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +27,10 @@ class LoginLog(DataClassBase):
|
|||||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
||||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
||||||
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
||||||
login_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), comment='登录时间')
|
login_time: Mapped[datetime] = mapped_column(TimeZone, comment='登录时间')
|
||||||
created_time: Mapped[datetime] = mapped_column(
|
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,53 @@
|
|||||||
#!/usr/bin/env python3
|
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
||||||
# -*- 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(
|
sys_user_role = 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'),
|
Column('id', 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'),
|
Column('user_id', BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
|
||||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sys_role_menu = Table(
|
sys_role_menu = 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'),
|
Column('id', 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'),
|
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||||
Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
|
Column('menu_id', BigInteger, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sys_role_data_rule = Table(
|
sys_role_data_scope = Table(
|
||||||
'sys_role_data_rule',
|
'sys_role_data_scope',
|
||||||
MappedBase.metadata,
|
MappedBase.metadata,
|
||||||
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
Column('id', 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'),
|
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
|
||||||
Column(
|
Column(
|
||||||
'data_rule_id',
|
'data_scope_id',
|
||||||
Integer,
|
BigInteger,
|
||||||
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
|
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
comment='数据权限规则ID',
|
comment='数据范围 ID',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
sys_data_scope_rule = Table(
|
||||||
|
'sys_data_scope_rule',
|
||||||
|
MappedBase.metadata,
|
||||||
|
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||||
|
Column(
|
||||||
|
'data_scope_id',
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||||
|
primary_key=True,
|
||||||
|
comment='数据范围 ID',
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
'data_rule_id',
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
|
||||||
|
primary_key=True,
|
||||||
|
comment='数据规则 ID',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
from __future__ import annotations
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, ForeignKey, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
@@ -10,6 +10,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
from backend.app.admin.model.m2m import sys_role_menu
|
from backend.app.admin.model.m2m import sys_role_menu
|
||||||
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 Role
|
||||||
|
|
||||||
|
|
||||||
class Menu(Base):
|
class Menu(Base):
|
||||||
"""菜单表"""
|
"""菜单表"""
|
||||||
@@ -19,25 +22,36 @@ class Menu(Base):
|
|||||||
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(String(50), comment='菜单标题')
|
||||||
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
||||||
|
path: Mapped[str | None] = mapped_column(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(String(100), default=None, comment='菜单图标')
|
||||||
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址')
|
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||||
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
|
|
||||||
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
||||||
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
perms: Mapped[str | None] = mapped_column(String(100), 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(
|
||||||
|
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||||
|
default=None,
|
||||||
|
comment='外链地址',
|
||||||
|
)
|
||||||
remark: Mapped[str | None] = mapped_column(
|
remark: Mapped[str | None] = mapped_column(
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||||
|
default=None,
|
||||||
|
comment='备注',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 父级菜单一对多
|
# 父级菜单一对多
|
||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
|
BigInteger,
|
||||||
|
ForeignKey('sys_menu.id', ondelete='SET NULL'),
|
||||||
|
default=None,
|
||||||
|
index=True,
|
||||||
|
comment='父菜单ID',
|
||||||
)
|
)
|
||||||
parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id])
|
parent: Mapped[Menu | None] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||||
children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent')
|
children: Mapped[list[Menu] | None] = relationship(init=False, back_populates='parent')
|
||||||
|
|
||||||
# 菜单角色多对多
|
# 菜单角色多对多
|
||||||
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_menu, back_populates='menus') # noqa: F821
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.dialects.mysql import JSON, LONGTEXT, TEXT
|
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, id_key
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ class OperaLog(DataClassBase):
|
|||||||
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
||||||
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
||||||
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
||||||
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
|
user_agent: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='请求头')
|
||||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
||||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
||||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
||||||
@@ -34,7 +33,10 @@ class OperaLog(DataClassBase):
|
|||||||
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
|
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
|
||||||
msg: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
msg: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
||||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms)')
|
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,13 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
from __future__ import annotations
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import String
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import INTEGER, TEXT
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_rule, sys_role_menu, sys_user_role
|
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import DataScope, Menu, User
|
||||||
|
|
||||||
|
|
||||||
class Role(Base):
|
class Role(Base):
|
||||||
"""角色表"""
|
"""角色表"""
|
||||||
@@ -17,15 +22,22 @@ class Role(Base):
|
|||||||
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(String(20), unique=True, comment='角色名称')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
||||||
|
is_filter_scopes: Mapped[bool] = mapped_column(
|
||||||
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=True,
|
||||||
|
comment='过滤数据权限(0否 1是)',
|
||||||
|
)
|
||||||
remark: Mapped[str | None] = mapped_column(
|
remark: Mapped[str | None] = mapped_column(
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||||
|
default=None,
|
||||||
|
comment='备注',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 角色用户多对多
|
# 角色用户多对多
|
||||||
users: Mapped[list['User']] = relationship(init=False, secondary=sys_user_role, back_populates='roles') # noqa: F821
|
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') # noqa: F821
|
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
|
||||||
|
|
||||||
# 角色数据权限规则多对多
|
# 角色数据范围多对多
|
||||||
rules: Mapped[list['DataRule']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles') # noqa: F821
|
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='roles')
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
from __future__ import annotations
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from sqlalchemy import VARBINARY, Boolean, DateTime, ForeignKey, String
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import VARBINARY, Boolean, ForeignKey, String
|
||||||
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
|
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_user_role
|
from backend.app.admin.model.m2m import sys_user_role
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, TimeZone, id_key
|
||||||
from backend.database.db import uuid4_str
|
from backend.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):
|
||||||
"""用户表"""
|
"""用户表"""
|
||||||
@@ -21,37 +24,43 @@ class User(Base):
|
|||||||
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(String(50), 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(String(20), unique=True, index=True, comment='用户名')
|
||||||
nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称')
|
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
||||||
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
||||||
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
||||||
email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
|
||||||
|
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
||||||
|
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||||
|
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||||
is_superuser: Mapped[bool] = mapped_column(
|
is_superuser: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='超级权限(0否 1是)',
|
||||||
)
|
)
|
||||||
is_staff: Mapped[bool] = mapped_column(
|
is_staff: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
|
default=False,
|
||||||
|
comment='后台管理登陆(0否 1是)',
|
||||||
)
|
)
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='用户账号状态(0停用 1正常)')
|
|
||||||
is_multi_login: Mapped[bool] = mapped_column(
|
is_multi_login: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||||
)
|
default=False,
|
||||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
comment='是否重复登陆(0否 1是)',
|
||||||
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='注册时间'
|
|
||||||
)
|
)
|
||||||
|
join_time: Mapped[datetime] = mapped_column(TimeZone, 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='上次登录',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 部门用户一对多
|
# 部门用户一对多
|
||||||
dept_id: Mapped[int | None] = mapped_column(
|
dept_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
|
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||||
|
default=None,
|
||||||
|
comment='部门关联ID',
|
||||||
)
|
)
|
||||||
dept: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='users') # noqa: F821
|
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||||
|
|
||||||
# 用户社交信息一对多
|
|
||||||
socials: Mapped[list['UserSocial']] = relationship(init=False, back_populates='user') # noqa: F821
|
|
||||||
|
|
||||||
# 用户角色多对多
|
# 用户角色多对多
|
||||||
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_user_role, back_populates='users') # noqa: F821
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.common.model import Base, id_key
|
|
||||||
|
|
||||||
|
|
||||||
class UserSocial(Base):
|
|
||||||
"""用户社交表(OAuth2)"""
|
|
||||||
|
|
||||||
__tablename__ = 'sys_user_social'
|
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
|
||||||
source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
|
|
||||||
open_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 open id')
|
|
||||||
uid: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 ID')
|
|
||||||
union_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 union id')
|
|
||||||
scope: Mapped[str | None] = mapped_column(String(120), default=None, comment='第三方用户授予的权限')
|
|
||||||
code: Mapped[str | None] = mapped_column(String(50), default=None, comment='用户的授权 code')
|
|
||||||
|
|
||||||
# 用户社交信息一对多
|
|
||||||
user_id: Mapped[int | None] = mapped_column(
|
|
||||||
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
|
|
||||||
)
|
|
||||||
user: Mapped[Union['User', None]] = relationship(init=False, back_populates='socials') # noqa: F821
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#!/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
|
||||||
|
|
||||||
|
|
||||||
class GetCaptchaDetail(SchemaBase):
|
class GetCaptchaDetail(SchemaBase):
|
||||||
image_type: str = Field(description='图片类型')
|
"""验证码详情"""
|
||||||
|
|
||||||
|
uuid: str = Field(description='图片唯一标识')
|
||||||
|
img_type: str = Field(description='图片类型')
|
||||||
image: str = Field(description='图片内容')
|
image: str = Field(description='图片内容')
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
|
||||||
|
|
||||||
|
|
||||||
class SaveBuiltInConfigParam(SchemaBase):
|
|
||||||
name: str
|
|
||||||
key: str
|
|
||||||
value: str
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigSchemaBase(SchemaBase):
|
|
||||||
name: str
|
|
||||||
type: str | None
|
|
||||||
key: str
|
|
||||||
value: str
|
|
||||||
is_frontend: bool
|
|
||||||
remark: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class CreateConfigParam(ConfigSchemaBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateConfigParam(ConfigSchemaBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GetConfigDetail(ConfigSchemaBase):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
id: int
|
|
||||||
created_time: datetime
|
|
||||||
updated_time: datetime | None = None
|
|
||||||
@@ -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
|
||||||
@@ -9,28 +7,42 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class DataRuleSchemaBase(SchemaBase):
|
class DataRuleSchemaBase(SchemaBase):
|
||||||
name: str
|
"""数据规则基础模型"""
|
||||||
model: str
|
|
||||||
column: str
|
name: str = Field(description='规则名称')
|
||||||
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR)
|
model: str = Field(description='模型名称')
|
||||||
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq)
|
column: str = Field(description='字段名称')
|
||||||
value: str
|
operator: RoleDataRuleOperatorType = Field(description='操作符(AND/OR)')
|
||||||
|
expression: RoleDataRuleExpressionType = Field(description='表达式类型')
|
||||||
|
value: str = Field(description='规则值')
|
||||||
|
|
||||||
|
|
||||||
class CreateDataRuleParam(DataRuleSchemaBase):
|
class CreateDataRuleParam(DataRuleSchemaBase):
|
||||||
pass
|
"""创建数据规则参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateDataRuleParam(DataRuleSchemaBase):
|
class UpdateDataRuleParam(DataRuleSchemaBase):
|
||||||
pass
|
"""更新数据规则参数"""
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='规则 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.name)
|
class GetDataRuleColumnDetail(SchemaBase):
|
||||||
|
"""数据规则可用模型字段详情"""
|
||||||
|
|
||||||
|
key: str = Field(description='字段名')
|
||||||
|
comment: str = Field(description='字段评论')
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
|
from backend.app.admin.schema.data_rule import GetDataRuleDetail
|
||||||
|
from backend.common.enums import StatusType
|
||||||
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
|
class DataScopeBase(SchemaBase):
|
||||||
|
"""数据范围基础模型"""
|
||||||
|
|
||||||
|
name: str = Field(description='名称')
|
||||||
|
status: StatusType = Field(description='状态')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDataScopeParam(DataScopeBase):
|
||||||
|
"""创建数据范围参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDataScopeParam(DataScopeBase):
|
||||||
|
"""更新数据范围参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDataScopeRuleParam(SchemaBase):
|
||||||
|
"""更新数据范围规则参数"""
|
||||||
|
|
||||||
|
rules: list[int] = Field(description='数据规则 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDataScopeParam(SchemaBase):
|
||||||
|
"""删除数据范围参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='数据范围 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class GetDataScopeDetail(DataScopeBase):
|
||||||
|
"""数据范围详情"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int = Field(description='数据范围 ID')
|
||||||
|
created_time: datetime = Field(description='创建时间')
|
||||||
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|
||||||
|
|
||||||
|
class GetDataScopeWithRelationDetail(GetDataScopeDetail):
|
||||||
|
"""数据范围关联详情"""
|
||||||
|
|
||||||
|
rules: list[GetDataRuleDetail] = 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
|
||||||
@@ -9,27 +7,37 @@ from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class DeptSchemaBase(SchemaBase):
|
class DeptSchemaBase(SchemaBase):
|
||||||
name: str
|
"""部门基础模型"""
|
||||||
parent_id: int | None = Field(default=None, description='部门父级ID')
|
|
||||||
sort: int = Field(default=0, ge=0, description='排序')
|
name: str = Field(description='部门名称')
|
||||||
leader: str | None = None
|
parent_id: int | None = Field(None, description='部门父级 ID')
|
||||||
phone: CustomPhoneNumber | None = None
|
sort: int = Field(0, ge=0, description='排序')
|
||||||
email: CustomEmailStr | None = None
|
leader: str | None = Field(None, description='负责人')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
|
||||||
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
status: StatusType = Field(description='状态')
|
||||||
|
|
||||||
|
|
||||||
class CreateDeptParam(DeptSchemaBase):
|
class CreateDeptParam(DeptSchemaBase):
|
||||||
pass
|
"""创建部门参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateDeptParam(DeptSchemaBase):
|
class UpdateDeptParam(DeptSchemaBase):
|
||||||
pass
|
"""更新部门参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetDeptDetail(DeptSchemaBase):
|
class GetDeptDetail(DeptSchemaBase):
|
||||||
|
"""部门详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='部门 ID')
|
||||||
del_flag: bool
|
del_flag: bool = Field(description='是否删除')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|
||||||
|
|
||||||
|
class GetDeptTree(GetDeptDetail):
|
||||||
|
"""获取部门树"""
|
||||||
|
|
||||||
|
children: list['GetDeptTree'] | None = Field(None, description='子菜单')
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
|
||||||
|
|
||||||
from backend.app.admin.schema.dict_type import GetDictTypeDetail
|
|
||||||
from backend.common.enums import StatusType
|
|
||||||
from backend.common.schema import SchemaBase
|
|
||||||
|
|
||||||
|
|
||||||
class DictDataSchemaBase(SchemaBase):
|
|
||||||
type_id: int
|
|
||||||
label: str
|
|
||||||
value: str
|
|
||||||
sort: int
|
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
|
||||||
remark: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class CreateDictDataParam(DictDataSchemaBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateDictDataParam(DictDataSchemaBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GetDictDataDetail(DictDataSchemaBase):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
id: int
|
|
||||||
type: GetDictTypeDetail | None = None
|
|
||||||
created_time: datetime
|
|
||||||
updated_time: datetime | None = None
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
|
||||||
|
|
||||||
from backend.common.enums import StatusType
|
|
||||||
from backend.common.schema import SchemaBase
|
|
||||||
|
|
||||||
|
|
||||||
class DictTypeSchemaBase(SchemaBase):
|
|
||||||
name: str
|
|
||||||
code: str
|
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
|
||||||
remark: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class CreateDictTypeParam(DictTypeSchemaBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateDictTypeParam(DictTypeSchemaBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GetDictTypeDetail(DictTypeSchemaBase):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
id: int
|
|
||||||
created_time: datetime
|
|
||||||
updated_time: datetime | None = None
|
|
||||||
@@ -1,38 +1,46 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class LoginLogSchemaBase(SchemaBase):
|
class LoginLogSchemaBase(SchemaBase):
|
||||||
user_uuid: str
|
"""登录日志基础模型"""
|
||||||
username: str
|
|
||||||
status: int
|
user_uuid: str = Field(description='用户 UUID')
|
||||||
ip: str
|
username: str = Field(description='用户名')
|
||||||
country: str | None
|
status: int = Field(description='登录状态')
|
||||||
region: str | None
|
ip: str = Field(description='IP 地址')
|
||||||
city: str | None
|
country: str | None = Field(None, description='国家')
|
||||||
user_agent: str
|
region: str | None = Field(None, description='地区')
|
||||||
browser: str | None
|
city: str | None = Field(None, description='城市')
|
||||||
os: str | None
|
user_agent: str = Field(description='用户代理')
|
||||||
device: str | None
|
browser: str | None = Field(None, description='浏览器')
|
||||||
msg: str
|
os: str | None = Field(None, description='操作系统')
|
||||||
login_time: datetime
|
device: str | None = Field(None, description='设备')
|
||||||
|
msg: str = Field(description='消息')
|
||||||
|
login_time: datetime = Field(description='登录时间')
|
||||||
|
|
||||||
|
|
||||||
class CreateLoginLogParam(LoginLogSchemaBase):
|
class CreateLoginLogParam(LoginLogSchemaBase):
|
||||||
pass
|
"""创建登录日志参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateLoginLogParam(LoginLogSchemaBase):
|
class UpdateLoginLogParam(LoginLogSchemaBase):
|
||||||
pass
|
"""更新登录日志参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteLoginLogParam(SchemaBase):
|
||||||
|
"""删除登录日志参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='登录日志 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetLoginLogDetail(LoginLogSchemaBase):
|
class GetLoginLogDetail(LoginLogSchemaBase):
|
||||||
|
"""登录日志详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='日志 ID')
|
||||||
created_time: datetime
|
created_time: datetime = 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
|
||||||
@@ -9,32 +7,43 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class MenuSchemaBase(SchemaBase):
|
class MenuSchemaBase(SchemaBase):
|
||||||
title: str
|
"""菜单基础模型"""
|
||||||
name: str
|
|
||||||
parent_id: int | None = Field(default=None, description='菜单父级ID')
|
title: str = Field(description='菜单标题')
|
||||||
sort: int = Field(default=0, ge=0, description='排序')
|
name: str = Field(description='菜单名称')
|
||||||
icon: str | None = None
|
path: str | None = Field(None, description='路由地址')
|
||||||
path: str | None = None
|
parent_id: int | None = Field(None, description='菜单父级 ID')
|
||||||
menu_type: MenuType = Field(default=MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
|
sort: int = Field(0, ge=0, description='排序')
|
||||||
component: str | None = None
|
icon: str | None = Field(None, description='图标')
|
||||||
perms: str | None = None
|
type: MenuType = Field(description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
component: str | None = Field(None, description='组件路径')
|
||||||
display: StatusType = Field(default=StatusType.enable)
|
perms: str | None = Field(None, description='权限标识')
|
||||||
cache: StatusType = Field(default=StatusType.enable)
|
status: StatusType = Field(description='状态')
|
||||||
remark: str | None = None
|
display: StatusType = Field(description='是否显示')
|
||||||
|
cache: StatusType = Field(description='是否缓存')
|
||||||
|
link: str | None = Field(None, description='外链地址')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateMenuParam(MenuSchemaBase):
|
class CreateMenuParam(MenuSchemaBase):
|
||||||
pass
|
"""创建菜单参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateMenuParam(MenuSchemaBase):
|
class UpdateMenuParam(MenuSchemaBase):
|
||||||
pass
|
"""更新菜单参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetMenuDetail(MenuSchemaBase):
|
class GetMenuDetail(MenuSchemaBase):
|
||||||
|
"""菜单详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='菜单 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|
||||||
|
|
||||||
|
class GetMenuTree(GetMenuDetail):
|
||||||
|
"""获取菜单树"""
|
||||||
|
|
||||||
|
children: list['GetMenuTree'] | None = Field(None, description='子菜单')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
@@ -9,37 +8,47 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class OperaLogSchemaBase(SchemaBase):
|
class OperaLogSchemaBase(SchemaBase):
|
||||||
trace_id: str
|
"""操作日志基础模型"""
|
||||||
username: str | None = None
|
|
||||||
method: str
|
trace_id: str = Field(description='追踪 ID')
|
||||||
title: str
|
username: str | None = Field(None, description='用户名')
|
||||||
path: str
|
method: str = Field(description='请求方法')
|
||||||
ip: str
|
title: str = Field(description='操作标题')
|
||||||
country: str | None = None
|
path: str = Field(description='请求路径')
|
||||||
region: str | None = None
|
ip: str = Field(description='IP 地址')
|
||||||
city: str | None = None
|
country: str | None = Field(None, description='国家')
|
||||||
user_agent: str
|
region: str | None = Field(None, description='地区')
|
||||||
os: str | None = None
|
city: str | None = Field(None, description='城市')
|
||||||
browser: str | None = None
|
user_agent: str = Field(description='用户代理')
|
||||||
device: str | None = None
|
os: str | None = Field(None, description='操作系统')
|
||||||
args: dict | None = None
|
browser: str | None = Field(None, description='浏览器')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
device: str | None = Field(None, description='设备')
|
||||||
code: str
|
args: dict[str, Any] | None = Field(None, description='请求参数')
|
||||||
msg: str | None = None
|
status: StatusType = Field(description='状态')
|
||||||
cost_time: float
|
code: str = Field(description='状态码')
|
||||||
opera_time: datetime
|
msg: str | None = Field(None, description='消息')
|
||||||
|
cost_time: float = Field(description='耗时')
|
||||||
|
opera_time: datetime = Field(description='操作时间')
|
||||||
|
|
||||||
|
|
||||||
class CreateOperaLogParam(OperaLogSchemaBase):
|
class CreateOperaLogParam(OperaLogSchemaBase):
|
||||||
pass
|
"""创建操作日志参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateOperaLogParam(OperaLogSchemaBase):
|
class UpdateOperaLogParam(OperaLogSchemaBase):
|
||||||
pass
|
"""更新操作日志参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteOperaLogParam(SchemaBase):
|
||||||
|
"""删除操作日志参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='操作日志 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetOperaLogDetail(OperaLogSchemaBase):
|
class GetOperaLogDetail(OperaLogSchemaBase):
|
||||||
|
"""操作日志详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='日志 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
|
|||||||
@@ -1,42 +1,60 @@
|
|||||||
#!/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_rule import GetDataRuleDetail
|
from backend.app.admin.schema.data_scope import GetDataScopeDetail
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class RoleSchemaBase(SchemaBase):
|
class RoleSchemaBase(SchemaBase):
|
||||||
name: str
|
"""角色基础模型"""
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
|
||||||
remark: str | None = None
|
name: str = Field(description='角色名称')
|
||||||
|
status: StatusType = Field(description='状态')
|
||||||
|
is_filter_scopes: bool = Field(True, description='过滤数据权限')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateRoleParam(RoleSchemaBase):
|
class CreateRoleParam(RoleSchemaBase):
|
||||||
pass
|
"""创建角色参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleParam(RoleSchemaBase):
|
class UpdateRoleParam(RoleSchemaBase):
|
||||||
pass
|
"""更新角色参数"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRoleParam(SchemaBase):
|
||||||
|
"""删除角色参数"""
|
||||||
|
|
||||||
|
pks: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleMenuParam(SchemaBase):
|
class UpdateRoleMenuParam(SchemaBase):
|
||||||
menus: list[int]
|
"""更新角色菜单参数"""
|
||||||
|
|
||||||
|
menus: list[int] = Field(description='菜单 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleRuleParam(SchemaBase):
|
class UpdateRoleScopeParam(SchemaBase):
|
||||||
rules: list[int]
|
"""更新角色数据范围参数"""
|
||||||
|
|
||||||
|
scopes: list[int] = Field(description='数据范围 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetRoleDetail(RoleSchemaBase):
|
class GetRoleDetail(RoleSchemaBase):
|
||||||
|
"""角色详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='角色 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
menus: list[GetMenuDetail | None] = []
|
|
||||||
rules: list[GetDataRuleDetail | None] = []
|
|
||||||
|
class GetRoleWithRelationDetail(GetRoleDetail):
|
||||||
|
"""角色关联详情"""
|
||||||
|
|
||||||
|
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
|
||||||
|
scopes: list[GetDataScopeDetail | None] = Field([], description='数据范围列表')
|
||||||
|
|||||||
@@ -1,45 +1,49 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from backend.app.admin.schema.user import GetUserInfoNoRelationDetail
|
from pydantic import Field
|
||||||
|
|
||||||
|
from backend.app.admin.schema.user import GetUserInfoDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class GetSwaggerToken(SchemaBase):
|
class GetSwaggerToken(SchemaBase):
|
||||||
access_token: str
|
"""Swagger 认证令牌"""
|
||||||
token_type: str = 'Bearer'
|
|
||||||
user: GetUserInfoNoRelationDetail
|
access_token: str = Field(description='访问令牌')
|
||||||
|
token_type: str = Field('Bearer', description='令牌类型')
|
||||||
|
user: GetUserInfoDetail = Field(description='用户信息')
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenBase(SchemaBase):
|
class AccessTokenBase(SchemaBase):
|
||||||
access_token: str
|
"""访问令牌基础模型"""
|
||||||
access_token_expire_time: datetime
|
|
||||||
session_uuid: str
|
access_token: str = Field(description='访问令牌')
|
||||||
|
access_token_expire_time: datetime = Field(description='令牌过期时间')
|
||||||
|
session_uuid: str = Field(description='会话 UUID')
|
||||||
|
|
||||||
|
|
||||||
class GetNewToken(AccessTokenBase):
|
class GetNewToken(AccessTokenBase):
|
||||||
pass
|
"""获取新令牌"""
|
||||||
|
|
||||||
|
|
||||||
class GetLoginToken(AccessTokenBase):
|
class GetLoginToken(AccessTokenBase):
|
||||||
user: GetUserInfoNoRelationDetail
|
"""获取登录令牌"""
|
||||||
|
|
||||||
|
user: GetUserInfoDetail = Field(description='用户信息')
|
||||||
class KickOutToken(SchemaBase):
|
|
||||||
session_uuid: str
|
|
||||||
|
|
||||||
|
|
||||||
class GetTokenDetail(SchemaBase):
|
class GetTokenDetail(SchemaBase):
|
||||||
id: int
|
"""令牌详情"""
|
||||||
session_uuid: str
|
|
||||||
username: str
|
id: int = Field(description='用户 ID')
|
||||||
nickname: str
|
session_uuid: str = Field(description='会话 UUID')
|
||||||
ip: str
|
username: str = Field(description='用户名')
|
||||||
os: str
|
nickname: str = Field(description='昵称')
|
||||||
browser: str
|
ip: str = Field(description='IP 地址')
|
||||||
device: str
|
os: str = Field(description='操作系统')
|
||||||
status: StatusType
|
browser: str = Field(description='浏览器')
|
||||||
last_login_time: str
|
device: str = Field(description='设备')
|
||||||
expire_time: datetime
|
status: StatusType = Field(description='状态')
|
||||||
|
last_login_time: str = Field(description='最后登录时间')
|
||||||
|
expire_time: datetime = Field(description='过期时间')
|
||||||
|
|||||||
@@ -1,90 +1,110 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
from pydantic import ConfigDict, Field, HttpUrl, model_validator
|
||||||
from typing_extensions import Self
|
from 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 GetRoleDetail
|
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
|
||||||
|
|
||||||
|
|
||||||
class AuthSchemaBase(SchemaBase):
|
class AuthSchemaBase(SchemaBase):
|
||||||
username: str
|
"""用户认证基础模型"""
|
||||||
password: str | None
|
|
||||||
|
username: str = Field(description='用户名')
|
||||||
|
password: str = Field(description='密码')
|
||||||
|
|
||||||
|
|
||||||
class AuthLoginParam(AuthSchemaBase):
|
class AuthLoginParam(AuthSchemaBase):
|
||||||
captcha: str
|
"""用户登录参数"""
|
||||||
|
|
||||||
|
uuid: str = Field(description='验证码 UUID')
|
||||||
class RegisterUserParam(AuthSchemaBase):
|
captcha: str = Field(description='验证码')
|
||||||
nickname: str | None = None
|
|
||||||
email: EmailStr = Field(examples=['user@example.com'])
|
|
||||||
|
|
||||||
|
|
||||||
class AddUserParam(AuthSchemaBase):
|
class AddUserParam(AuthSchemaBase):
|
||||||
dept_id: int
|
"""添加用户参数"""
|
||||||
roles: list[int]
|
|
||||||
nickname: str | None = None
|
nickname: str | None = Field(None, description='昵称')
|
||||||
email: EmailStr = Field(examples=['user@example.com'])
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
phone: CustomPhoneNumber | None = Field(None, description='手机号码')
|
||||||
|
dept_id: int = Field(description='部门 ID')
|
||||||
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
|
class AddOAuth2UserParam(AuthSchemaBase):
|
||||||
|
"""添加 OAuth2 用户参数"""
|
||||||
|
|
||||||
|
password: str | None = Field(None, description='密码')
|
||||||
|
nickname: str | None = Field(None, description='昵称')
|
||||||
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordParam(SchemaBase):
|
||||||
|
"""重置密码参数"""
|
||||||
|
|
||||||
|
old_password: str = Field(description='旧密码')
|
||||||
|
new_password: str = Field(description='新密码')
|
||||||
|
confirm_password: str = Field(description='确认密码')
|
||||||
|
|
||||||
|
|
||||||
class UserInfoSchemaBase(SchemaBase):
|
class UserInfoSchemaBase(SchemaBase):
|
||||||
dept_id: int | None = None
|
"""用户信息基础模型"""
|
||||||
username: str
|
|
||||||
nickname: str
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
email: EmailStr = Field(examples=['user@example.com'])
|
username: str = Field(description='用户名')
|
||||||
phone: CustomPhoneNumber | None = None
|
nickname: str = Field(description='昵称')
|
||||||
|
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||||
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserParam(UserInfoSchemaBase):
|
class UpdateUserParam(UserInfoSchemaBase):
|
||||||
pass
|
"""更新用户参数"""
|
||||||
|
|
||||||
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserRoleParam(SchemaBase):
|
class GetUserInfoDetail(UserInfoSchemaBase):
|
||||||
roles: list[int]
|
"""用户信息详情"""
|
||||||
|
|
||||||
|
|
||||||
class AvatarParam(SchemaBase):
|
|
||||||
url: HttpUrl = Field(description='头像 http 地址')
|
|
||||||
|
|
||||||
|
|
||||||
class GetUserInfoNoRelationDetail(UserInfoSchemaBase):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
dept_id: int | None = None
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
id: int
|
id: int = Field(description='用户 ID')
|
||||||
uuid: str
|
uuid: str = Field(description='用户 UUID')
|
||||||
avatar: str | None = None
|
status: StatusType = Field(description='状态')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
is_superuser: bool = Field(description='是否超级管理员')
|
||||||
is_superuser: bool
|
is_staff: bool = Field(description='是否管理员')
|
||||||
is_staff: bool
|
is_multi_login: bool = Field(description='是否允许多端登录')
|
||||||
is_multi_login: bool
|
join_time: datetime = Field(description='加入时间')
|
||||||
join_time: datetime = None
|
last_login_time: datetime | None = Field(None, description='最后登录时间')
|
||||||
last_login_time: datetime | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class GetUserInfoDetail(GetUserInfoNoRelationDetail):
|
class GetUserInfoWithRelationDetail(GetUserInfoDetail):
|
||||||
|
"""用户信息关联详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
dept: GetDeptDetail | None = None
|
dept: GetDeptDetail | None = Field(None, description='部门信息')
|
||||||
roles: list[GetRoleDetail]
|
roles: list[GetRoleWithRelationDetail] = Field(description='角色列表')
|
||||||
|
|
||||||
|
|
||||||
class GetCurrentUserInfoDetail(GetUserInfoDetail):
|
class GetCurrentUserInfoWithRelationDetail(GetUserInfoWithRelationDetail):
|
||||||
|
"""当前用户信息关联详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
dept: str | None = None
|
dept: str | None = Field(None, description='部门名称')
|
||||||
roles: list[str]
|
roles: list[str] = Field(description='角色名称列表')
|
||||||
|
|
||||||
@model_validator(mode='before')
|
@model_validator(mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def handel(cls, data: Any) -> Self:
|
def handel(cls, data: Any) -> Self:
|
||||||
"""处理部门和角色"""
|
"""处理部门和角色数据"""
|
||||||
dept = data['dept']
|
dept = data['dept']
|
||||||
if dept:
|
if dept:
|
||||||
data['dept'] = dept['name']
|
data['dept'] = dept['name']
|
||||||
@@ -92,13 +112,3 @@ class GetCurrentUserInfoDetail(GetUserInfoDetail):
|
|||||||
if roles:
|
if roles:
|
||||||
data['roles'] = [role['name'] for role in roles]
|
data['roles'] = [role['name'] for role in roles]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class CurrentUserIns(GetUserInfoDetail):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordParam(SchemaBase):
|
|
||||||
old_password: str
|
|
||||||
new_password: str
|
|
||||||
confirm_password: str
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from backend.common.enums import UserSocialType
|
|
||||||
from backend.common.schema import SchemaBase
|
|
||||||
|
|
||||||
|
|
||||||
class UserSocialSchemaBase(SchemaBase):
|
|
||||||
source: UserSocialType
|
|
||||||
open_id: str | None = None
|
|
||||||
uid: str | None = None
|
|
||||||
union_id: str | None = None
|
|
||||||
scope: str | None = None
|
|
||||||
code: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class CreateUserSocialParam(UserSocialSchemaBase):
|
|
||||||
user_id: int
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserSocialParam(SchemaBase):
|
|
||||||
pass
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#!/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.conf import admin_settings
|
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.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 (
|
||||||
@@ -24,166 +24,237 @@ from backend.common.security.jwt import (
|
|||||||
password_verify,
|
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.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
|
"""认证服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
|
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
|
||||||
|
"""
|
||||||
|
验证用户名和密码
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param username: 用户名
|
||||||
|
:param password: 密码
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
user = await user_dao.get_by_username(db, username)
|
user = await user_dao.get_by_username(db, username)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户名或密码有误')
|
raise errors.NotFoundError(msg='用户名或密码有误')
|
||||||
elif not password_verify(password, user.password):
|
|
||||||
|
if user.password is None:
|
||||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||||
elif not user.status:
|
if not password_verify(password, user.password):
|
||||||
|
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||||
|
|
||||||
|
if not user.status:
|
||||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
async def swagger_login(self, *, db: AsyncSession, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
user = await self.user_verify(db, obj.username, obj.password)
|
Swagger 文档登录
|
||||||
await user_dao.update_login_time(db, obj.username)
|
|
||||||
a_token = await create_access_token(
|
:param db: 数据库会话
|
||||||
str(user.id),
|
:param obj: 登录凭证
|
||||||
user.is_multi_login,
|
:return:
|
||||||
# extra info
|
"""
|
||||||
login_type='swagger',
|
user = await self.user_verify(db, obj.username, obj.password)
|
||||||
)
|
await user_dao.update_login_time(db, obj.username)
|
||||||
return a_token.access_token, user
|
access_token = await create_access_token(
|
||||||
|
user.id,
|
||||||
|
multi_login=user.is_multi_login,
|
||||||
|
# extra info
|
||||||
|
swagger=True,
|
||||||
|
)
|
||||||
|
return access_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:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
user = None
|
用户登录
|
||||||
try:
|
|
||||||
user = await self.user_verify(db, obj.username, obj.password)
|
|
||||||
captcha_code = await redis_client.get(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
|
||||||
if not captcha_code:
|
|
||||||
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
|
|
||||||
if captcha_code.lower() != obj.captcha.lower():
|
|
||||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
|
||||||
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
|
||||||
await user_dao.update_login_time(db, obj.username)
|
|
||||||
await db.refresh(user)
|
|
||||||
a_token = await create_access_token(
|
|
||||||
str(user.id),
|
|
||||||
user.is_multi_login,
|
|
||||||
# extra info
|
|
||||||
username=user.username,
|
|
||||||
nickname=user.nickname,
|
|
||||||
last_login_time=timezone.t_str(user.last_login_time),
|
|
||||||
ip=request.state.ip,
|
|
||||||
os=request.state.os,
|
|
||||||
browser=request.state.browser,
|
|
||||||
device=request.state.device,
|
|
||||||
)
|
|
||||||
r_token = await create_refresh_token(str(user.id), user.is_multi_login)
|
|
||||||
response.set_cookie(
|
|
||||||
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
|
||||||
value=r_token.refresh_token,
|
|
||||||
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
|
||||||
expires=timezone.f_utc(r_token.refresh_token_expire_time),
|
|
||||||
httponly=True,
|
|
||||||
)
|
|
||||||
except errors.NotFoundError as e:
|
|
||||||
log.error('登陆错误: 用户名不存在')
|
|
||||||
raise errors.NotFoundError(msg=e.msg)
|
|
||||||
except (errors.AuthorizationError, errors.CustomError) as e:
|
|
||||||
if not user:
|
|
||||||
log.error('登陆错误: 用户密码有误')
|
|
||||||
task = BackgroundTask(
|
|
||||||
login_log_service.create,
|
|
||||||
**dict(
|
|
||||||
db=db,
|
|
||||||
request=request,
|
|
||||||
user_uuid=user.uuid if user else uuid4_str(),
|
|
||||||
username=obj.username,
|
|
||||||
login_time=timezone.now(),
|
|
||||||
status=LoginLogStatusType.fail.value,
|
|
||||||
msg=e.msg,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
raise errors.AuthorizationError(msg=e.msg, background=task)
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f'登陆错误: {e}')
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
background_tasks.add_task(
|
|
||||||
login_log_service.create,
|
|
||||||
**dict(
|
|
||||||
db=db,
|
|
||||||
request=request,
|
|
||||||
user_uuid=user.uuid,
|
|
||||||
username=obj.username,
|
|
||||||
login_time=timezone.now(),
|
|
||||||
status=LoginLogStatusType.success.value,
|
|
||||||
msg='登录成功',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
data = GetLoginToken(
|
|
||||||
access_token=a_token.access_token,
|
|
||||||
access_token_expire_time=a_token.access_token_expire_time,
|
|
||||||
session_uuid=a_token.session_uuid,
|
|
||||||
user=user, # type: ignore
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
:param db: 数据库会话
|
||||||
async def new_token(*, request: Request) -> GetNewToken:
|
:param request: 请求对象
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
:param response: 响应对象
|
||||||
if not refresh_token:
|
:param obj: 登录参数
|
||||||
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
|
:param background_tasks: 后台任务
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
user = None
|
||||||
try:
|
try:
|
||||||
user_id = jwt_decode(refresh_token).id
|
user = await self.user_verify(db, obj.username, obj.password)
|
||||||
except Exception:
|
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{obj.uuid}')
|
||||||
raise errors.TokenError(msg='Refresh Token 无效')
|
if not captcha_code:
|
||||||
async with async_db_session() as db:
|
raise errors.RequestError(msg=t('error.captcha.expired'))
|
||||||
user = await user_dao.get(db, user_id)
|
if captcha_code.lower() != obj.captcha.lower():
|
||||||
if not user:
|
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||||
raise errors.NotFoundError(msg='用户名或密码有误')
|
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{obj.uuid}')
|
||||||
elif not user.status:
|
await user_dao.update_login_time(db, obj.username)
|
||||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
await db.refresh(user)
|
||||||
new_token = await create_new_token(
|
access_token = await create_access_token(
|
||||||
user_id=str(user.id),
|
user.id,
|
||||||
refresh_token=refresh_token,
|
|
||||||
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=ctx.device,
|
||||||
)
|
)
|
||||||
data = GetNewToken(
|
refresh_token = await create_refresh_token(
|
||||||
access_token=new_token.new_access_token,
|
access_token.session_uuid,
|
||||||
access_token_expire_time=new_token.new_access_token_expire_time,
|
user.id,
|
||||||
session_uuid=new_token.session_uuid,
|
multi_login=user.is_multi_login,
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
||||||
|
value=refresh_token.refresh_token,
|
||||||
|
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||||
|
expires=timezone.to_utc(refresh_token.refresh_token_expire_time),
|
||||||
|
httponly=True,
|
||||||
|
)
|
||||||
|
except errors.NotFoundError as e:
|
||||||
|
log.error('登陆错误: 用户名不存在')
|
||||||
|
raise errors.NotFoundError(msg=e.msg)
|
||||||
|
except (errors.RequestError, errors.CustomError) as e:
|
||||||
|
if not user:
|
||||||
|
log.error('登陆错误: 用户密码有误')
|
||||||
|
task = BackgroundTask(
|
||||||
|
login_log_service.create,
|
||||||
|
db=db,
|
||||||
|
user_uuid=user.uuid if user else uuid4_str(),
|
||||||
|
username=obj.username,
|
||||||
|
login_time=timezone.now(),
|
||||||
|
status=LoginLogStatusType.fail.value,
|
||||||
|
msg=e.msg,
|
||||||
|
)
|
||||||
|
raise errors.RequestError(code=e.code, msg=e.msg, background=task)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f'登陆错误: {e}')
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
background_tasks.add_task(
|
||||||
|
login_log_service.create,
|
||||||
|
db=db,
|
||||||
|
user_uuid=user.uuid,
|
||||||
|
username=obj.username,
|
||||||
|
login_time=timezone.now(),
|
||||||
|
status=LoginLogStatusType.success.value,
|
||||||
|
msg=t('success.login.success'),
|
||||||
|
)
|
||||||
|
data = GetLoginToken(
|
||||||
|
access_token=access_token.access_token,
|
||||||
|
access_token_expire_time=access_token.access_token_expire_time,
|
||||||
|
session_uuid=access_token.session_uuid,
|
||||||
|
user=user, # type: ignore
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def logout(*, request: Request, response: Response) -> None:
|
async def get_codes(*, db: AsyncSession, request: Request) -> list[str]:
|
||||||
token = get_token(request)
|
"""
|
||||||
token_payload = jwt_decode(token)
|
获取用户权限码
|
||||||
user_id = token_payload.id
|
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
:param db: 数据库会话
|
||||||
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
:param request: FastAPI 请求对象
|
||||||
if request.user.is_multi_login:
|
:return:
|
||||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
|
"""
|
||||||
if refresh_token:
|
codes = set()
|
||||||
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
|
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:
|
else:
|
||||||
key_prefix = [
|
roles = request.user.roles
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:',
|
if roles:
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:',
|
for role in roles:
|
||||||
]
|
for menu in role.menus:
|
||||||
for prefix in key_prefix:
|
if menu.perms:
|
||||||
await redis_client.delete_prefix(prefix)
|
codes.add(*menu.perms.split(','))
|
||||||
|
|
||||||
|
return list(codes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def refresh_token(*, db: AsyncSession, request: Request) -> GetNewToken:
|
||||||
|
"""
|
||||||
|
刷新令牌
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
if not refresh_token:
|
||||||
|
raise errors.RequestError(msg='Refresh Token 已过期,请重新登录')
|
||||||
|
token_payload = jwt_decode(refresh_token)
|
||||||
|
|
||||||
|
user = await user_dao.get(db, token_payload.id)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
if not user.status:
|
||||||
|
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||||
|
if not user.is_multi_login and await redis_client.keys(match=f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
|
||||||
|
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
|
||||||
|
new_token = await create_new_token(
|
||||||
|
refresh_token,
|
||||||
|
token_payload.session_uuid,
|
||||||
|
user.id,
|
||||||
|
multi_login=user.is_multi_login,
|
||||||
|
# extra info
|
||||||
|
username=user.username,
|
||||||
|
nickname=user.nickname,
|
||||||
|
last_login_time=timezone.to_str(user.last_login_time),
|
||||||
|
ip=ctx.ip,
|
||||||
|
os=ctx.os,
|
||||||
|
browser=ctx.browser,
|
||||||
|
device_type=ctx.device,
|
||||||
|
)
|
||||||
|
data = GetNewToken(
|
||||||
|
access_token=new_token.new_access_token,
|
||||||
|
access_token_expire_time=new_token.new_access_token_expire_time,
|
||||||
|
session_uuid=new_token.session_uuid,
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def logout(*, request: Request, response: Response) -> None:
|
||||||
|
"""
|
||||||
|
用户登出
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param response: FastAPI 响应对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
user_id = token_payload.id
|
||||||
|
session_uuid = token_payload.session_uuid
|
||||||
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
except errors.TokenError:
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
|
||||||
|
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||||
|
await redis_client.delete(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||||
|
if refresh_token:
|
||||||
|
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
|
||||||
|
|
||||||
|
|
||||||
auth_service: AuthService = AuthService()
|
auth_service: AuthService = AuthService()
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from sqlalchemy import Select
|
|
||||||
|
|
||||||
from backend.app.admin.conf import admin_settings
|
|
||||||
from backend.app.admin.crud.crud_config import config_dao
|
|
||||||
from backend.app.admin.model import Config
|
|
||||||
from backend.app.admin.schema.config import (
|
|
||||||
CreateConfigParam,
|
|
||||||
SaveBuiltInConfigParam,
|
|
||||||
UpdateConfigParam,
|
|
||||||
)
|
|
||||||
from backend.common.exception import errors
|
|
||||||
from backend.database.db import async_db_session
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigService:
|
|
||||||
@staticmethod
|
|
||||||
async def get_built_in_config(type: str) -> Sequence[Config]:
|
|
||||||
async with async_db_session() as db:
|
|
||||||
return await config_dao.get_by_type(db, type)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def save_built_in_config(objs: list[SaveBuiltInConfigParam], type: str) -> None:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
for obj in objs:
|
|
||||||
config = await config_dao.get_by_key_and_type(db, obj.key, type)
|
|
||||||
if config is None:
|
|
||||||
if await config_dao.get_by_key(db, obj.key, built_in=True):
|
|
||||||
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
|
|
||||||
await config_dao.create_model(db, obj, flush=True, type=type)
|
|
||||||
else:
|
|
||||||
await config_dao.update_model(db, config.id, obj, type=type)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get(pk) -> Config | dict:
|
|
||||||
async with async_db_session() as db:
|
|
||||||
config = await config_dao.get(db, pk)
|
|
||||||
if not config:
|
|
||||||
raise errors.NotFoundError(msg='参数配置不存在')
|
|
||||||
return config
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_select(*, name: str = None, type: str = None) -> Select:
|
|
||||||
return await config_dao.get_list(name=name, type=type)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def create(*, obj: CreateConfigParam) -> None:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
if obj.type in admin_settings.CONFIG_BUILT_IN_TYPES:
|
|
||||||
raise errors.ForbiddenError(msg='非法类型参数')
|
|
||||||
config = await config_dao.get_by_key(db, obj.key)
|
|
||||||
if config:
|
|
||||||
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
|
|
||||||
await config_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update(*, pk: int, obj: UpdateConfigParam) -> int:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
config = await config_dao.get(db, pk)
|
|
||||||
if not config:
|
|
||||||
raise errors.NotFoundError(msg='参数配置不存在')
|
|
||||||
count = await config_dao.update(db, pk, obj)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete(*, pk: list[int]) -> int:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
count = await config_dao.delete(db, pk)
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
config_service: ConfigService = ConfigService()
|
|
||||||
@@ -1,89 +1,131 @@
|
|||||||
#!/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.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import Select
|
|
||||||
|
|
||||||
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.crud.crud_role import role_dao
|
|
||||||
from backend.app.admin.model import DataRule
|
from backend.app.admin.model import DataRule
|
||||||
from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRuleParam
|
from backend.app.admin.schema.data_rule import (
|
||||||
|
CreateDataRuleParam,
|
||||||
|
DeleteDataRuleParam,
|
||||||
|
GetDataRuleColumnDetail,
|
||||||
|
UpdateDataRuleParam,
|
||||||
|
)
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
|
from backend.common.pagination import paging_data
|
||||||
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.utils.import_parse import dynamic_import_data_model
|
from backend.utils.import_parse import dynamic_import_data_model
|
||||||
|
|
||||||
|
|
||||||
class DataRuleService:
|
class DataRuleService:
|
||||||
@staticmethod
|
"""数据规则服务类"""
|
||||||
async def get(*, pk: int) -> DataRule:
|
|
||||||
async with async_db_session() as db:
|
|
||||||
data_rule = await data_rule_dao.get(db, pk)
|
|
||||||
if not data_rule:
|
|
||||||
raise errors.NotFoundError(msg='数据规则不存在')
|
|
||||||
return data_rule
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_role_rules(*, pk: int) -> list[int]:
|
async def get(*, db: AsyncSession, pk: int) -> DataRule:
|
||||||
async with async_db_session() as db:
|
"""
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
获取数据规则详情
|
||||||
if not role:
|
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
:param db: 数据库会话
|
||||||
rule_ids = [rule.id for rule in role.rules]
|
:param pk: 规则 ID
|
||||||
return rule_ids
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
|
if not data_rule:
|
||||||
|
raise errors.NotFoundError(msg='数据规则不存在')
|
||||||
|
return data_rule
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_models() -> list[str]:
|
async def get_models() -> list[str]:
|
||||||
|
"""获取所有数据规则可用模型"""
|
||||||
return list(settings.DATA_PERMISSION_MODELS.keys())
|
return list(settings.DATA_PERMISSION_MODELS.keys())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_columns(model: str) -> list[str]:
|
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
|
||||||
|
"""
|
||||||
|
获取数据规则可用模型的字段列表
|
||||||
|
|
||||||
|
:param model: 模型名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if model not in settings.DATA_PERMISSION_MODELS:
|
if model not in settings.DATA_PERMISSION_MODELS:
|
||||||
raise errors.NotFoundError(msg='数据模型不存在')
|
raise errors.NotFoundError(msg='数据规则可用模型不存在')
|
||||||
try:
|
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
|
||||||
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
raise errors.ServerError(msg=f'数据模型 {model} 动态导入失败,请联系系统超级管理员')
|
|
||||||
model_columns = [
|
model_columns = [
|
||||||
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
|
GetDataRuleColumnDetail(key=column.key, comment=column.comment)
|
||||||
|
for column in model_ins.__table__.columns
|
||||||
|
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]:
|
||||||
return await data_rule_dao.get_list(name=name)
|
"""
|
||||||
|
获取数据规则列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param name: 规则名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
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:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
data_rule = await data_rule_dao.get_by_name(db, obj.name)
|
创建数据规则
|
||||||
if data_rule:
|
|
||||||
raise errors.ForbiddenError(msg='数据权限规则已存在')
|
:param db: 数据库会话
|
||||||
await data_rule_dao.create(db, obj)
|
:param obj: 规则创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data_rule = await data_rule_dao.get_by_name(db, obj.name)
|
||||||
|
if data_rule:
|
||||||
|
raise errors.ConflictError(msg='数据规则已存在')
|
||||||
|
await data_rule_dao.create(db, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
data_rule = await data_rule_dao.get(db, pk)
|
更新数据规则
|
||||||
if not data_rule:
|
|
||||||
raise errors.NotFoundError(msg='数据权限规则不存在')
|
:param db: 数据库会话
|
||||||
count = await data_rule_dao.update(db, pk, obj)
|
:param pk: 规则 ID
|
||||||
return count
|
:param obj: 规则更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
|
if not data_rule:
|
||||||
|
raise errors.NotFoundError(msg='数据规则不存在')
|
||||||
|
if data_rule.name != obj.name and await data_rule_dao.get_by_name(db, obj.name):
|
||||||
|
raise errors.ConflictError(msg='数据规则已存在')
|
||||||
|
count = await data_rule_dao.update(db, pk, obj)
|
||||||
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, request: Request, pk: list[int]) -> int:
|
async def delete(*, db: AsyncSession, obj: DeleteDataRuleParam) -> int:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
count = await data_rule_dao.delete(db, pk)
|
批量删除数据规则
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
|
||||||
return count
|
:param db: 数据库会话
|
||||||
|
:param obj: 规则 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
count = await data_rule_dao.delete(db, obj.pks)
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
data_rule_service: DataRuleService = DataRuleService()
|
data_rule_service: DataRuleService = DataRuleService()
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
||||||
|
from backend.app.admin.model import DataScope
|
||||||
|
from backend.app.admin.schema.data_scope import (
|
||||||
|
CreateDataScopeParam,
|
||||||
|
DeleteDataScopeParam,
|
||||||
|
UpdateDataScopeParam,
|
||||||
|
UpdateDataScopeRuleParam,
|
||||||
|
)
|
||||||
|
from backend.common.exception import errors
|
||||||
|
from backend.common.pagination import paging_data
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.database.redis import redis_client
|
||||||
|
|
||||||
|
|
||||||
|
class DataScopeService:
|
||||||
|
"""数据范围服务类"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get(*, db: AsyncSession, pk: int) -> DataScope:
|
||||||
|
"""
|
||||||
|
获取数据范围详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_scope = await data_scope_dao.get(db, pk)
|
||||||
|
if not data_scope:
|
||||||
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
|
return data_scope
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_all(*, 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
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_scope = await data_scope_dao.get_with_relation(db, pk)
|
||||||
|
if not data_scope:
|
||||||
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
|
return data_scope
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取数据范围列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param name: 范围名称
|
||||||
|
:param status: 范围状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data_scope_select = await data_scope_dao.get_select(name, status)
|
||||||
|
return await paging_data(db, data_scope_select)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create(*, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
||||||
|
"""
|
||||||
|
创建数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 数据范围参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data_scope = await data_scope_dao.get_by_name(db, obj.name)
|
||||||
|
if data_scope:
|
||||||
|
raise errors.ConflictError(msg='数据范围已存在')
|
||||||
|
await data_scope_dao.create(db, obj)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int:
|
||||||
|
"""
|
||||||
|
更新数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:param obj: 数据范围更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
data_scope = await data_scope_dao.get(db, pk)
|
||||||
|
if not data_scope:
|
||||||
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
|
if data_scope.name != obj.name and await data_scope_dao.get_by_name(db, obj.name):
|
||||||
|
raise errors.ConflictError(msg='数据范围已存在')
|
||||||
|
count = await data_scope_dao.update(db, pk, obj)
|
||||||
|
for role in await data_scope.awaitable_attrs.roles:
|
||||||
|
for user in await role.awaitable_attrs.users:
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_data_scope_rule(*, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
|
||||||
|
"""
|
||||||
|
更新数据范围规则
|
||||||
|
|
||||||
|
:param pk: 范围 ID
|
||||||
|
:param rule_ids: 规则 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
count = await data_scope_dao.update_rules(db, pk, rule_ids)
|
||||||
|
return count
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete(*, db: AsyncSession, obj: DeleteDataScopeParam) -> int:
|
||||||
|
"""
|
||||||
|
批量删除数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 范围 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
count = await data_scope_dao.delete(db, obj.pks)
|
||||||
|
for pk in obj.pks:
|
||||||
|
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()
|
||||||
@@ -1,79 +1,122 @@
|
|||||||
#!/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_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
|
||||||
|
|
||||||
|
|
||||||
class DeptService:
|
class DeptService:
|
||||||
@staticmethod
|
"""部门服务类"""
|
||||||
async def get(*, pk: int) -> Dept:
|
|
||||||
async with async_db_session() as db:
|
|
||||||
dept = await dept_dao.get(db, pk)
|
|
||||||
if not dept:
|
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
|
||||||
return dept
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_dept_tree(
|
async def get(*, db: AsyncSession, pk: int) -> Dept:
|
||||||
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None
|
"""
|
||||||
|
获取部门详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 部门 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
dept = await dept_dao.get(db, pk)
|
||||||
|
if not dept:
|
||||||
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
|
return dept
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_tree(
|
||||||
|
*,
|
||||||
|
db: AsyncSession,
|
||||||
|
request: Request,
|
||||||
|
name: str | None,
|
||||||
|
leader: str | None,
|
||||||
|
phone: str | None,
|
||||||
|
status: int | None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
async with async_db_session() as db:
|
"""
|
||||||
dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status)
|
获取部门树形结构
|
||||||
tree_data = get_tree_data(dept_select)
|
|
||||||
return tree_data
|
:param db: 数据库会话
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param name: 部门名称
|
||||||
|
:param leader: 部门负责人
|
||||||
|
:param phone: 联系电话
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
dept_select = await dept_dao.get_all(request, db, name, leader, phone, status)
|
||||||
|
tree_data = get_tree_data(dept_select)
|
||||||
|
return tree_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDeptParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateDeptParam) -> None:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
dept = await dept_dao.get_by_name(db, obj.name)
|
创建部门
|
||||||
if dept:
|
|
||||||
raise errors.ForbiddenError(msg='部门名称已存在')
|
:param db: 数据库会话
|
||||||
if obj.parent_id:
|
:param obj: 部门创建参数
|
||||||
parent_dept = await dept_dao.get(db, obj.parent_id)
|
:return:
|
||||||
if not parent_dept:
|
"""
|
||||||
raise errors.NotFoundError(msg='父级部门不存在')
|
dept = await dept_dao.get_by_name(db, obj.name)
|
||||||
await dept_dao.create(db, obj)
|
if dept:
|
||||||
|
raise errors.ConflictError(msg='部门名称已存在')
|
||||||
|
if obj.parent_id:
|
||||||
|
parent_dept = await dept_dao.get(db, obj.parent_id)
|
||||||
|
if not parent_dept:
|
||||||
|
raise errors.NotFoundError(msg='父级部门不存在')
|
||||||
|
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:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
dept = await dept_dao.get(db, pk)
|
更新部门
|
||||||
if not dept:
|
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
:param db: 数据库会话
|
||||||
if dept.name != obj.name:
|
:param pk: 部门 ID
|
||||||
if await dept_dao.get_by_name(db, obj.name):
|
:param obj: 部门更新参数
|
||||||
raise errors.ForbiddenError(msg='部门名称已存在')
|
:return:
|
||||||
if obj.parent_id:
|
"""
|
||||||
parent_dept = await dept_dao.get(db, obj.parent_id)
|
dept = await dept_dao.get(db, pk)
|
||||||
if not parent_dept:
|
if not dept:
|
||||||
raise errors.NotFoundError(msg='父级部门不存在')
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
if obj.parent_id == dept.id:
|
if dept.name != obj.name and await dept_dao.get_by_name(db, obj.name):
|
||||||
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
raise errors.ConflictError(msg='部门名称已存在')
|
||||||
count = await dept_dao.update(db, pk, obj)
|
if obj.parent_id:
|
||||||
return count
|
parent_dept = await dept_dao.get(db, obj.parent_id)
|
||||||
|
if not parent_dept:
|
||||||
|
raise errors.NotFoundError(msg='父级部门不存在')
|
||||||
|
if obj.parent_id == dept.id:
|
||||||
|
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
||||||
|
count = await dept_dao.update(db, pk, obj)
|
||||||
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, request: Request, pk: int) -> int:
|
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
dept_user = await dept_dao.get_with_relation(db, pk)
|
删除部门
|
||||||
if dept_user:
|
|
||||||
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
|
:param db: 数据库会话
|
||||||
children = await dept_dao.get_children(db, pk)
|
:param pk: 部门 ID
|
||||||
if children:
|
:return:
|
||||||
raise errors.ForbiddenError(msg='部门下存在子部门,无法删除')
|
"""
|
||||||
count = await dept_dao.delete(db, pk)
|
dept = await dept_dao.get_with_relation(db, pk)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
if dept.users:
|
||||||
return count
|
raise errors.ConflictError(msg='部门下存在用户,无法删除')
|
||||||
|
children = await dept_dao.get_children(db, pk)
|
||||||
|
if children:
|
||||||
|
raise errors.ConflictError(msg='部门下存在子部门,无法删除')
|
||||||
|
count = await dept_dao.delete(db, pk)
|
||||||
|
for user in dept.users:
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
dept_service: DeptService = DeptService()
|
dept_service: DeptService = DeptService()
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_dict_data import dict_data_dao
|
|
||||||
from backend.app.admin.crud.crud_dict_type import dict_type_dao
|
|
||||||
from backend.app.admin.model import DictData
|
|
||||||
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
|
|
||||||
from backend.common.exception import errors
|
|
||||||
from backend.database.db import async_db_session
|
|
||||||
|
|
||||||
|
|
||||||
class DictDataService:
|
|
||||||
@staticmethod
|
|
||||||
async def get(*, pk: int) -> DictData:
|
|
||||||
async with async_db_session() as db:
|
|
||||||
dict_data = await dict_data_dao.get_with_relation(db, pk)
|
|
||||||
if not dict_data:
|
|
||||||
raise errors.NotFoundError(msg='字典数据不存在')
|
|
||||||
return dict_data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_select(*, label: str = None, value: str = None, status: int = None) -> Select:
|
|
||||||
return await dict_data_dao.get_list(label=label, value=value, status=status)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def create(*, obj: CreateDictDataParam) -> None:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
dict_data = await dict_data_dao.get_by_label(db, obj.label)
|
|
||||||
if dict_data:
|
|
||||||
raise errors.ForbiddenError(msg='字典数据已存在')
|
|
||||||
dict_type = await dict_type_dao.get(db, obj.type_id)
|
|
||||||
if not dict_type:
|
|
||||||
raise errors.NotFoundError(msg='字典类型不存在')
|
|
||||||
await dict_data_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update(*, pk: int, obj: UpdateDictDataParam) -> int:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
dict_data = await dict_data_dao.get(db, pk)
|
|
||||||
if not dict_data:
|
|
||||||
raise errors.NotFoundError(msg='字典数据不存在')
|
|
||||||
if dict_data.label != obj.label:
|
|
||||||
if await dict_data_dao.get_by_label(db, obj.label):
|
|
||||||
raise errors.ForbiddenError(msg='字典数据已存在')
|
|
||||||
dict_type = await dict_type_dao.get(db, obj.type_id)
|
|
||||||
if not dict_type:
|
|
||||||
raise errors.NotFoundError(msg='字典类型不存在')
|
|
||||||
count = await dict_data_dao.update(db, pk, obj)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete(*, pk: list[int]) -> int:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
count = await dict_data_dao.delete(db, pk)
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
dict_data_service: DictDataService = DictDataService()
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sqlalchemy import Select
|
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_dict_type import dict_type_dao
|
|
||||||
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
|
|
||||||
from backend.common.exception import errors
|
|
||||||
from backend.database.db import async_db_session
|
|
||||||
|
|
||||||
|
|
||||||
class DictTypeService:
|
|
||||||
@staticmethod
|
|
||||||
async def get_select(*, name: str = None, code: str = None, status: int = None) -> Select:
|
|
||||||
return await dict_type_dao.get_list(name=name, code=code, status=status)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def create(*, obj: CreateDictTypeParam) -> None:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
dict_type = await dict_type_dao.get_by_code(db, obj.code)
|
|
||||||
if dict_type:
|
|
||||||
raise errors.ForbiddenError(msg='字典类型已存在')
|
|
||||||
await dict_type_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update(*, pk: int, obj: UpdateDictTypeParam) -> int:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
dict_type = await dict_type_dao.get(db, pk)
|
|
||||||
if not dict_type:
|
|
||||||
raise errors.NotFoundError(msg='字典类型不存在')
|
|
||||||
if dict_type.code != obj.code:
|
|
||||||
if await dict_type_dao.get_by_code(db, obj.code):
|
|
||||||
raise errors.ForbiddenError(msg='字典类型已存在')
|
|
||||||
count = await dict_type_dao.update(db, pk, obj)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete(*, pk: list[int]) -> int:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
count = await dict_type_dao.delete(db, pk)
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
dict_type_service: DictTypeService = DictTypeService()
|
|
||||||
@@ -1,64 +1,89 @@
|
|||||||
#!/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.database.db import async_db_session
|
from backend.common.pagination import paging_data
|
||||||
|
|
||||||
|
|
||||||
class LoginLogService:
|
class LoginLogService:
|
||||||
|
"""登录日志服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, username: str, status: int, ip: str) -> Select:
|
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
|
||||||
return await login_log_dao.get_list(username=username, status=status, ip=ip)
|
"""
|
||||||
|
获取登录日志列表
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param username: 用户名
|
||||||
|
:param status: 状态
|
||||||
|
:param ip: IP 地址
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
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,
|
db: AsyncSession,
|
||||||
request: Request,
|
|
||||||
user_uuid: str,
|
user_uuid: str,
|
||||||
username: str,
|
username: str,
|
||||||
login_time: datetime,
|
login_time: datetime,
|
||||||
status: int,
|
status: int,
|
||||||
msg: str,
|
msg: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
创建登录日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param user_uuid: 用户 UUID
|
||||||
|
:param username: 用户名
|
||||||
|
:param login_time: 登录时间
|
||||||
|
:param status: 状态
|
||||||
|
:param msg: 消息
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
obj_in = CreateLoginLogParam(
|
obj = CreateLoginLogParam(
|
||||||
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_in)
|
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:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
count = await login_log_dao.delete(db, pk)
|
批量删除登录日志
|
||||||
return count
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 日志 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
count = await login_log_dao.delete(db, obj.pks)
|
||||||
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete_all() -> int:
|
async def delete_all(*, db: AsyncSession) -> None:
|
||||||
async with async_db_session.begin() as db:
|
"""清空所有登录日志"""
|
||||||
count = await login_log_dao.delete_all(db)
|
await login_log_dao.delete_all(db)
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
login_log_service: LoginLogService = LoginLogService()
|
login_log_service: LoginLogService = LoginLogService()
|
||||||
|
|||||||
@@ -1,99 +1,139 @@
|
|||||||
#!/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.crud.crud_role import role_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.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, get_vben5_tree_data
|
||||||
|
|
||||||
|
|
||||||
class MenuService:
|
class MenuService:
|
||||||
@staticmethod
|
"""菜单服务类"""
|
||||||
async def get(*, pk: int) -> Menu:
|
|
||||||
async with async_db_session() as db:
|
|
||||||
menu = await menu_dao.get(db, menu_id=pk)
|
|
||||||
if not menu:
|
|
||||||
raise errors.NotFoundError(msg='菜单不存在')
|
|
||||||
return menu
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]:
|
async def get(*, db: AsyncSession, pk: int) -> Menu:
|
||||||
async with async_db_session() as db:
|
"""
|
||||||
menu_select = await menu_dao.get_all(db, title=title, status=status)
|
获取菜单详情
|
||||||
menu_tree = get_tree_data(menu_select)
|
|
||||||
return menu_tree
|
:param db: 数据库会话
|
||||||
|
:param pk: 菜单 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
menu = await menu_dao.get(db, menu_id=pk)
|
||||||
|
if not menu:
|
||||||
|
raise errors.NotFoundError(msg='菜单不存在')
|
||||||
|
return menu
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_role_menu_tree(*, pk: int) -> list[dict[str, Any]]:
|
async def get_tree(*, db: AsyncSession, title: str | None, status: int | None) -> list[dict[str, Any]]:
|
||||||
async with async_db_session() as db:
|
"""
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
获取菜单树形结构
|
||||||
if not role:
|
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
:param db: 数据库会话
|
||||||
menu_ids = [menu.id for menu in role.menus]
|
:param title: 菜单标题
|
||||||
menu_select = await menu_dao.get_role_menus(db, False, menu_ids)
|
:param status: 状态
|
||||||
menu_tree = get_tree_data(menu_select)
|
:return:
|
||||||
return menu_tree
|
"""
|
||||||
|
|
||||||
|
menu_data = await menu_dao.get_all(db, title=title, status=status)
|
||||||
|
menu_tree = get_tree_data(menu_data)
|
||||||
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
|
async def get_sidebar(*, db: AsyncSession, request: Request) -> list[dict[str, Any] | None]:
|
||||||
async with async_db_session() as db:
|
"""
|
||||||
|
获取用户的菜单侧边栏
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
if request.user.is_superuser:
|
||||||
|
menu_data = await menu_dao.get_sidebar(db, None)
|
||||||
|
else:
|
||||||
roles = request.user.roles
|
roles = request.user.roles
|
||||||
menu_ids = []
|
menu_ids = set()
|
||||||
menu_tree = []
|
|
||||||
if roles:
|
if roles:
|
||||||
for role in roles:
|
for role in roles:
|
||||||
menu_ids.extend([menu.id for menu in role.menus])
|
menu_ids.update(menu.id for menu in role.menus)
|
||||||
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
|
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
|
||||||
menu_tree = get_tree_data(menu_select)
|
menu_tree = get_vben5_tree_data(menu_data)
|
||||||
return menu_tree
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateMenuParam) -> None:
|
async def create(*, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
title = await menu_dao.get_by_title(db, obj.title)
|
创建菜单
|
||||||
if title:
|
|
||||||
raise errors.ForbiddenError(msg='菜单标题已存在')
|
:param db: 数据库会话
|
||||||
if obj.parent_id:
|
:param obj: 菜单创建参数
|
||||||
parent_menu = await menu_dao.get(db, obj.parent_id)
|
:return:
|
||||||
if not parent_menu:
|
"""
|
||||||
raise errors.NotFoundError(msg='父级菜单不存在')
|
|
||||||
await menu_dao.create(db, obj)
|
title = await menu_dao.get_by_title(db, obj.title)
|
||||||
|
if title:
|
||||||
|
raise errors.ConflictError(msg='菜单标题已存在')
|
||||||
|
if obj.parent_id:
|
||||||
|
parent_menu = await menu_dao.get(db, obj.parent_id)
|
||||||
|
if not parent_menu:
|
||||||
|
raise errors.NotFoundError(msg='父级菜单不存在')
|
||||||
|
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:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
menu = await menu_dao.get(db, pk)
|
更新菜单
|
||||||
if not menu:
|
|
||||||
raise errors.NotFoundError(msg='菜单不存在')
|
:param db: 数据库会话
|
||||||
if menu.title != obj.title:
|
:param pk: 菜单 ID
|
||||||
if await menu_dao.get_by_title(db, obj.title):
|
:param obj: 菜单更新参数
|
||||||
raise errors.ForbiddenError(msg='菜单标题已存在')
|
:return:
|
||||||
if obj.parent_id:
|
"""
|
||||||
parent_menu = await menu_dao.get(db, obj.parent_id)
|
|
||||||
if not parent_menu:
|
menu = await menu_dao.get(db, pk)
|
||||||
raise errors.NotFoundError(msg='父级菜单不存在')
|
if not menu:
|
||||||
if obj.parent_id == menu.id:
|
raise errors.NotFoundError(msg='菜单不存在')
|
||||||
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
if menu.title != obj.title and await menu_dao.get_by_title(db, obj.title):
|
||||||
count = await menu_dao.update(db, pk, obj)
|
raise errors.ConflictError(msg='菜单标题已存在')
|
||||||
return count
|
if obj.parent_id:
|
||||||
|
parent_menu = await menu_dao.get(db, obj.parent_id)
|
||||||
|
if not parent_menu:
|
||||||
|
raise errors.NotFoundError(msg='父级菜单不存在')
|
||||||
|
if obj.parent_id == menu.id:
|
||||||
|
raise errors.ForbiddenError(msg='禁止关联自身为父级')
|
||||||
|
count = await menu_dao.update(db, pk, obj)
|
||||||
|
for role in await menu.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 delete(*, request: Request, pk: int) -> int:
|
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||||
async with async_db_session.begin() as db:
|
"""
|
||||||
children = await menu_dao.get_children(db, pk)
|
删除菜单
|
||||||
if children:
|
|
||||||
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
|
:param db: 数据库会话
|
||||||
count = await menu_dao.delete(db, pk)
|
:param pk: 菜单 ID
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
:return:
|
||||||
return count
|
"""
|
||||||
|
|
||||||
|
children = await menu_dao.get_children(db, pk)
|
||||||
|
if children:
|
||||||
|
raise errors.ConflictError(msg='菜单下存在子菜单,无法删除')
|
||||||
|
menu = await menu_dao.get(db, pk)
|
||||||
|
count = await menu_dao.delete(db, pk)
|
||||||
|
if menu:
|
||||||
|
for role in await menu.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
|
||||||
|
|
||||||
|
|
||||||
menu_service: MenuService = MenuService()
|
menu_service: MenuService = MenuService()
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fast_captcha import text_captcha
|
|
||||||
from fastapi import BackgroundTasks, Request, Response
|
|
||||||
|
|
||||||
from backend.app.admin.conf import admin_settings
|
|
||||||
from backend.app.admin.crud.crud_user import user_dao
|
|
||||||
from backend.app.admin.crud.crud_user_social import user_social_dao
|
|
||||||
from backend.app.admin.schema.token import GetLoginToken
|
|
||||||
from backend.app.admin.schema.user import RegisterUserParam
|
|
||||||
from backend.app.admin.schema.user_social import CreateUserSocialParam
|
|
||||||
from backend.app.admin.service.login_log_service import login_log_service
|
|
||||||
from backend.common.enums import LoginLogStatusType, UserSocialType
|
|
||||||
from backend.common.exception.errors import AuthorizationError
|
|
||||||
from backend.common.security import jwt
|
|
||||||
from backend.core.conf import settings
|
|
||||||
from backend.database.db import async_db_session
|
|
||||||
from backend.database.redis import redis_client
|
|
||||||
from backend.utils.timezone import timezone
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Service:
|
|
||||||
@staticmethod
|
|
||||||
async def create_with_login(
|
|
||||||
*,
|
|
||||||
request: Request,
|
|
||||||
response: Response,
|
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
user: dict,
|
|
||||||
social: UserSocialType,
|
|
||||||
) -> GetLoginToken | None:
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
# 获取 OAuth2 平台用户信息
|
|
||||||
social_id = user.get('id')
|
|
||||||
social_username = user.get('username')
|
|
||||||
if social == UserSocialType.github:
|
|
||||||
social_username = user.get('login')
|
|
||||||
social_nickname = user.get('name')
|
|
||||||
social_email = user.get('email')
|
|
||||||
if social == UserSocialType.linuxdo: # 不提供明文邮箱的平台
|
|
||||||
social_email = f'{social_username}@linux.do'
|
|
||||||
if not social_email:
|
|
||||||
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
|
|
||||||
# 创建系统用户
|
|
||||||
sys_user = await user_dao.check_email(db, social_email)
|
|
||||||
if not sys_user:
|
|
||||||
sys_user = await user_dao.get_by_username(db, social_username)
|
|
||||||
if sys_user:
|
|
||||||
social_username = f'{social_username}#{text_captcha(5)}'
|
|
||||||
sys_user = await user_dao.get_by_nickname(db, social_nickname)
|
|
||||||
if sys_user:
|
|
||||||
social_username = f'{social_nickname}#{text_captcha(5)}'
|
|
||||||
new_sys_user = RegisterUserParam(
|
|
||||||
username=social_username, password=None, nickname=social_username, email=social_email
|
|
||||||
)
|
|
||||||
await user_dao.create(db, new_sys_user, social=True)
|
|
||||||
await db.flush()
|
|
||||||
sys_user = await user_dao.check_email(db, social_email)
|
|
||||||
# 绑定社交用户
|
|
||||||
sys_user_id = sys_user.id
|
|
||||||
user_social = await user_social_dao.get(db, sys_user_id, social.value)
|
|
||||||
if not user_social:
|
|
||||||
new_user_social = CreateUserSocialParam(source=social.value, uid=str(social_id), user_id=sys_user_id)
|
|
||||||
await user_social_dao.create(db, new_user_social)
|
|
||||||
# 创建 token
|
|
||||||
access_token = await jwt.create_access_token(
|
|
||||||
str(sys_user_id),
|
|
||||||
sys_user.is_multi_login,
|
|
||||||
# extra info
|
|
||||||
username=sys_user.username,
|
|
||||||
nickname=sys_user.nickname,
|
|
||||||
last_login_time=timezone.t_str(timezone.now()),
|
|
||||||
ip=request.state.ip,
|
|
||||||
os=request.state.os,
|
|
||||||
browser=request.state.browser,
|
|
||||||
device=request.state.device,
|
|
||||||
)
|
|
||||||
refresh_token = await jwt.create_refresh_token(str(sys_user_id), multi_login=sys_user.is_multi_login)
|
|
||||||
await user_dao.update_login_time(db, sys_user.username)
|
|
||||||
await db.refresh(sys_user)
|
|
||||||
login_log = dict(
|
|
||||||
db=db,
|
|
||||||
request=request,
|
|
||||||
user_uuid=sys_user.uuid,
|
|
||||||
username=sys_user.username,
|
|
||||||
login_time=timezone.now(),
|
|
||||||
status=LoginLogStatusType.success.value,
|
|
||||||
msg='登录成功(OAuth2)',
|
|
||||||
)
|
|
||||||
background_tasks.add_task(login_log_service.create, **login_log)
|
|
||||||
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
|
||||||
response.set_cookie(
|
|
||||||
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
|
||||||
value=refresh_token.refresh_token,
|
|
||||||
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
|
|
||||||
expires=timezone.f_utc(refresh_token.refresh_token_expire_time),
|
|
||||||
httponly=True,
|
|
||||||
)
|
|
||||||
data = GetLoginToken(
|
|
||||||
access_token=access_token.access_token,
|
|
||||||
access_token_expire_time=access_token.access_token_expire_time,
|
|
||||||
user=sys_user, # type: ignore
|
|
||||||
session_uuid=access_token.session_uuid,
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
oauth2_service: OAuth2Service = OAuth2Service()
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user