mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Compare commits
439
Commits
| 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 | ||
|
|
1f8ac4953e | ||
|
|
1d981ec159 | ||
|
|
b0f675f697 | ||
|
|
6d44f807a7 | ||
|
|
9602e28429 | ||
|
|
440f324a3d | ||
|
|
5e7a4e1e74 | ||
|
|
4d13424b30 | ||
|
|
860c96d0ee | ||
|
|
d9985f9d4d | ||
|
|
1e0b04006b | ||
|
|
d419343939 | ||
|
|
eb3fd55321 | ||
|
|
4aa4301abd | ||
|
|
1d1a917580 | ||
|
|
0255283577 | ||
|
|
2df1f0f358 | ||
|
|
52816dd4b4 | ||
|
|
20e873b6e5 | ||
|
|
ebe710fac2 | ||
|
|
e588913d54 | ||
|
|
1c46e2480c | ||
|
|
3b65679fb0 | ||
|
|
7553ccf14d | ||
|
|
cf693cdce6 | ||
|
|
3fe0023a0a | ||
|
|
13cee674a7 | ||
|
|
64ee74bb38 | ||
|
|
1e855dbb86 | ||
|
|
e0606d115c | ||
|
|
8b159536ce | ||
|
|
2279ff7c58 | ||
|
|
3f6c43e15d | ||
|
|
6c1f68ec00 | ||
|
|
78dad3021a | ||
|
|
4f59f459a0 | ||
|
|
58b24b62ac | ||
|
|
a9b9b58290 | ||
|
|
f90b4667c3 | ||
|
|
3b2b45d6a5 | ||
|
|
8c04bcb981 | ||
|
|
7c4e288fd6 | ||
|
|
1838b63b11 | ||
|
|
6d5d1a48f3 | ||
|
|
bd45022a59 | ||
|
|
b29a3cbfb7 | ||
|
|
a15693cc26 | ||
|
|
5e60d9c248 | ||
|
|
12d7ec8174 | ||
|
|
601a6514a8 | ||
|
|
fc5a0f9dfa | ||
|
|
576ed77d36 | ||
|
|
06cbe56201 | ||
|
|
87eec419e6 | ||
|
|
0e7cef5b6c | ||
|
|
5c0b107142 | ||
|
|
0cfeeb4e45 | ||
|
|
aabb72138a | ||
|
|
0a1f0a90b2 | ||
|
|
493c2b2545 | ||
|
|
0810647c48 | ||
|
|
a3d079cfdf | ||
|
|
c22d1394e7 | ||
|
|
03add792a1 | ||
|
|
ab4495ce65 | ||
|
|
1d3b0e7d13 | ||
|
|
36a458b957 | ||
|
|
23d9d17d9f | ||
|
|
8da993655b | ||
|
|
6792bf9954 | ||
|
|
6cf30b3c5d | ||
|
|
eb23d1b5f9 | ||
|
|
29ba40ef32 | ||
|
|
02f65abf74 | ||
|
|
f5b7de6910 | ||
|
|
ac8e4a0406 | ||
|
|
b6821612af | ||
|
|
8783d74cc7 | ||
|
|
5a52871e7e | ||
|
|
90b2cca984 | ||
|
|
9c37297858 | ||
|
|
1f95a776f0 | ||
|
|
b13410c7d1 | ||
|
|
9aa2d7b593 | ||
|
|
1d956cace4 | ||
|
|
4f5c5c28e6 | ||
|
|
24528e6050 | ||
|
|
7853233ddb | ||
|
|
51f3011947 | ||
|
|
87432a74c2 | ||
|
|
e19ae09966 | ||
|
|
8765f833d2 | ||
|
|
c22202eac4 | ||
|
|
a698373064 | ||
|
|
b43c08b5d2 | ||
|
|
2002068d81 | ||
|
|
bb0d7f3f56 | ||
|
|
c743182211 | ||
|
|
22586dd947 | ||
|
|
f61dc4fc84 | ||
|
|
144d704a5d | ||
|
|
f30a18844e | ||
|
|
043a622e67 | ||
|
|
a602dff739 | ||
|
|
863dfed4ec | ||
|
|
ceb8fb1cb2 | ||
|
|
21fed49d7b | ||
|
|
c9be643e76 | ||
|
|
7287093772 | ||
|
|
2112523b4c | ||
|
|
34506c3039 | ||
|
|
f94c2d7948 | ||
|
|
294d7ae9e9 | ||
|
|
a0d47733a9 | ||
|
|
581bed2a33 | ||
|
|
f9c4028c60 | ||
|
|
9752ce8e92 | ||
|
|
3cfc95cb02 | ||
|
|
3d12c3270e | ||
|
|
812b8a0fb7 | ||
|
|
e5262f0b32 | ||
|
|
42ee5d7355 | ||
|
|
5a283b1c3c | ||
|
|
f9ec9d5d56 | ||
|
|
3b060c73b2 | ||
|
|
85c16de32b | ||
|
|
9aa74afff1 | ||
|
|
f21af0c3c3 | ||
|
|
af101d57fe | ||
|
|
1ad5482a22 | ||
|
|
805a5a4e96 | ||
|
|
08a8acb9ea | ||
|
|
dd474cdc08 | ||
|
|
84c16bc983 | ||
|
|
2589ddcdf6 | ||
|
|
eb8dfda8fa | ||
|
|
9edb72d065 | ||
|
|
406116a230 | ||
|
|
8a165ec181 | ||
|
|
5e438c685d | ||
|
|
a98621b40d | ||
|
|
01ee7deeff | ||
|
|
3b64aeb97b | ||
|
|
594efb5e96 | ||
|
|
8af4336428 | ||
|
|
115b6f0b30 | ||
|
|
2bad404913 | ||
|
|
7e633dbdd5 | ||
|
|
2ae020e70a | ||
|
|
94cbc629cd | ||
|
|
4f1d1c2094 | ||
|
|
cd96d2f5cd | ||
|
|
576160129a | ||
|
|
754b9d9edc | ||
|
|
b5c656f308 | ||
|
|
a41b1673e5 | ||
|
|
a22aaa0a3e | ||
|
|
5c7d6659cf | ||
|
|
f39d11eb9d | ||
|
|
227221ff67 | ||
|
|
27d229c250 | ||
|
|
754498be0b | ||
|
|
7bfc7908ab |
@@ -0,0 +1,9 @@
|
||||
__pycache__/
|
||||
.git/
|
||||
.github/
|
||||
.idea/
|
||||
.vscode/
|
||||
venv/
|
||||
.venv/
|
||||
.ruff_cache/
|
||||
.pytest_cache/
|
||||
@@ -0,0 +1 @@
|
||||
custom: https://wu-clan.github.io/sponsor/
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Release changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- uses: rhysd/changelog-from-release/action@v3
|
||||
with:
|
||||
file: CHANGELOG.md
|
||||
pull_request: true
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -0,0 +1,34 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
name: lint ${{ matrix.python-version }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --only-group lint
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
chmod 755 backend/scripts/lint.sh
|
||||
./backend/scripts/lint.sh
|
||||
+5
-6
@@ -1,10 +1,9 @@
|
||||
__pycache__/
|
||||
.idea/
|
||||
.env
|
||||
.vscode/
|
||||
.cursor/
|
||||
.DS_Store
|
||||
venv/
|
||||
.mypy_cache/
|
||||
backend/app/log/
|
||||
backend/app/alembic/versions/
|
||||
backend/app/static/media/
|
||||
.venv/
|
||||
.python-version
|
||||
.ruff_cache/
|
||||
.pytest_cache/
|
||||
|
||||
+15
-7
@@ -1,19 +1,27 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: end-of-file-fixer
|
||||
- id: requirements-txt-fixer
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.1.6
|
||||
rev: v0.14.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
args:
|
||||
- '--config'
|
||||
- '.ruff.toml'
|
||||
# https://github.com/astral-sh/ruff-pre-commit/issues/64
|
||||
- '--fix'
|
||||
- '--unsafe-fixes'
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: 0.9.0
|
||||
hooks:
|
||||
- id: uv-lock
|
||||
- id: uv-export
|
||||
args:
|
||||
- '-o'
|
||||
- 'requirements.txt'
|
||||
- '--no-hashes'
|
||||
|
||||
+153
-21
@@ -1,42 +1,174 @@
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
cache-dir = "./.ruff_cache"
|
||||
|
||||
[per-file-ignores]
|
||||
"backend/app/api/v1/*.py" = ["TCH"]
|
||||
"backend/app/models/*.py" = ["TCH003"]
|
||||
"backend/app/**/__init__.py" = ["F401"]
|
||||
"backend/app/tests/*.py" = ["E402"]
|
||||
preview = true
|
||||
fix = true
|
||||
unsafe-fixes = true
|
||||
show-fixes = true
|
||||
required-version = ">=0.13.0"
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
"E",
|
||||
"F",
|
||||
"I",
|
||||
"W505",
|
||||
"PT018",
|
||||
"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",
|
||||
"PGH004",
|
||||
"PLE1142",
|
||||
"RUF100",
|
||||
"F404",
|
||||
"TCH",
|
||||
"UP007"
|
||||
"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]
|
||||
mark-parentheses = false
|
||||
parametrize-names-type = "list"
|
||||
parametrize-values-row-type = "list"
|
||||
parametrize-values-type = "tuple"
|
||||
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}
|
||||
@@ -1,28 +1,37 @@
|
||||
<div align="center">
|
||||
|
||||
<img alt="The logo includes the abstract combination of the three letters FBA, forming a lightning bolt that seems to spread out from the ground" width="320" src="https://wu-clan.github.io/picx-images-hosting/logo/fba.png">
|
||||
|
||||
# FastAPI Best Architecture
|
||||
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
> [!TIP]
|
||||
> You are viewing the pydantic-v1 branch, which is locked and no longer provides updates and fixes
|
||||
Enterprise-level backend architecture solution
|
||||
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
FastAPI framework based on the front-end and back-end separation of the middle and back-end solutions, follow
|
||||
the [pseudo three-tier architecture](#pseudo-three-tier-architecture) design, support for **python3.10** and above
|
||||
versions
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||

|
||||

|
||||

|
||||
[](https://pydantic.dev)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://github.com/astral-sh/uv)
|
||||

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

|
||||
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
Its purpose is to allow you to use it directly as the infrastructure of your new project, this repository as a template
|
||||
library open to any person or enterprise can be used for free!
|
||||
</div>
|
||||
|
||||
**Continuously updated and maintained**
|
||||
## Pseudo 3-tier architecture
|
||||
|
||||
## Pseudo three-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 three-tier architecture is even more fascinating.
|
||||
In python web development, there is no common standard for the concept of 3-tier architecture, so we'll call it a
|
||||
pseudo 3-tier architecture here
|
||||
|
||||
In python web development, there is no common standard for the concept of three-tier architecture, so we'll call it a
|
||||
pseudo three-tier architecture here
|
||||
But please note that we don't have a traditional multi-app structure (django, springBoot...) If you don't like this
|
||||
pattern, use templates to transform it to your heart's content!
|
||||
|
||||
| workflow | java | fastapi_best_architecture |
|
||||
|----------------|----------------|---------------------------|
|
||||
@@ -32,171 +41,16 @@ pseudo three-tier architecture here
|
||||
| data access | dao / mapper | crud |
|
||||
| model | model / entity | model |
|
||||
|
||||
## Online preview
|
||||
## Help
|
||||
|
||||
Unfortunately, we don't have the funds to provide an online preview, you can deploy by checking
|
||||
out [local-development](#local-development), or directly using [Docker](#docker-deploy)
|
||||
to deploy, or at [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui)
|
||||
See a preview of some of the screenshots
|
||||
|
||||
## Features
|
||||
|
||||
- [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] Casbin 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
|
||||
|
||||
1. [x] User management: system user role management, permission assignment
|
||||
2. [x] Department Management: Configure the system organization (company, department, group...)
|
||||
3. [x] Menu Management: Configuration of system menus, user menus, button privilege identification
|
||||
4. [x] Role Management: Assign role menu privileges, assign role routing privileges
|
||||
5. [x] Dictionary Management: Maintain common fixed data or parameters within the system.
|
||||
6. [x] Operation Logs: logging and querying of normal and abnormal system operations.
|
||||
7. [x] Login Authentication: graphical authentication code background authentication login
|
||||
8. [x] Login Logs: Logging and querying of normal and abnormal user logins
|
||||
9. [x] Service Monitoring: server hardware device information and status
|
||||
10. [x] Scheduled tasks: automated tasks, asynchronous tasks, and function invocation are supported
|
||||
11. [x] Interface Documentation: Automatically generate online interactive API interface documentation.
|
||||
|
||||
## Local development
|
||||
|
||||
* Python: 3.10+
|
||||
* Mysql: 8.0+
|
||||
* Redis: The latest stable version is recommended
|
||||
* Nodejs: 14.0+
|
||||
|
||||
### BackEnd
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Create a database `fba`, choose utf8mb4 encoding
|
||||
3. Install and start Redis
|
||||
4. Create a `.env` file in the `backend/app/` directory
|
||||
|
||||
```shell
|
||||
cd backend/app/
|
||||
touch .env
|
||||
```
|
||||
|
||||
5. Copy `.env.example` to `.env`
|
||||
|
||||
```shell
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
6. Database migration [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
|
||||
|
||||
```shell
|
||||
cd backend/app/
|
||||
|
||||
# Generate migration file
|
||||
alembic revision --autogenerate
|
||||
|
||||
# Execute the migration
|
||||
alembic upgrade head
|
||||
```
|
||||
7. Start celery worker and beat
|
||||
|
||||
```shell
|
||||
celery -A tasks worker --loglevel=INFO
|
||||
# Optional, if you don't need to use the scheduled task
|
||||
celery -A tasks beat --loglevel=INFO
|
||||
```
|
||||
|
||||
8. Modify the configuration file as needed
|
||||
9. Execute the `backend/app/main.py` file to start the service
|
||||
10. Browser access: http://127.0.0.1:8000/api/v1/docs
|
||||
|
||||
---
|
||||
|
||||
### Front
|
||||
|
||||
Go to [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) for details
|
||||
|
||||
### Docker deploy
|
||||
|
||||
> [!WARNING]
|
||||
> Default port conflict:8000,3306,6379,5672
|
||||
>
|
||||
> As a best practice, shut down on-premises services before deployment:mysql,redis,rabbitmq...
|
||||
|
||||
1. Go to the directory where the ``docker-compose.yml`` file is located and create the environment variable
|
||||
file ``.env``
|
||||
|
||||
```shell
|
||||
cd deploy/docker-compose/
|
||||
|
||||
cp .env.server ../../backend/app/.env
|
||||
|
||||
# This command is optional
|
||||
cp .env.docker .env
|
||||
```
|
||||
|
||||
2. Modify the configuration file as needed
|
||||
3. Execute the one-click boot command
|
||||
|
||||
```shell
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
4. Wait for the command to complete automatically
|
||||
5. Visit the browser: http://127.0.0.1:8000/api/v1/docs
|
||||
|
||||
## Test data
|
||||
|
||||
Initialize the test data using the `backend/sql/init_test_data.sql` file
|
||||
|
||||
## Development process
|
||||
|
||||
For reference only
|
||||
|
||||
### BackEnd
|
||||
|
||||
1. Define the database model (model) and remember to perform database migration for each change
|
||||
2. Define the data validation model (schema)
|
||||
3. Define routes (router) and views (api)
|
||||
4. Define the business logic (service)
|
||||
5. Write database operations (crud)
|
||||
|
||||
### Front
|
||||
|
||||
Go to [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) for details
|
||||
|
||||
## Test
|
||||
|
||||
Execute unittests via pytest
|
||||
|
||||
1. Create the test database `fba_test`, select utf8mb4 encoding
|
||||
2. Using `backend/sql/create_tables.sql` file to create database tables
|
||||
3. Initialize the test data using the `backend/sql/init_pytest_data.sql` file
|
||||
4. Enter the app directory
|
||||
|
||||
```shell
|
||||
cd backend/app/
|
||||
```
|
||||
|
||||
5. Execute the test command
|
||||
|
||||
```shell
|
||||
pytest -vs --disable-warnings
|
||||
```
|
||||
For more details, please check
|
||||
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||
|
||||
## Contributors
|
||||
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wu-clan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/52145145?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/downdawn" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/41266749?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<a href="https://github.com/fastapi-practices/fastapi_best_architecture/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=fastapi-practices/fastapi_best_architecture"/>
|
||||
</a>
|
||||
|
||||
## Special thanks
|
||||
|
||||
@@ -207,37 +61,18 @@ Execute unittests via pytest
|
||||
- [Ruff](https://beta.ruff.rs/docs/)
|
||||
- ...
|
||||
|
||||
## 互动
|
||||
## Interactivity
|
||||
|
||||
有且仅有当前一个频道,请注意辨别真伪
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://t.me/+ZlPhIFkPp7E4NGI1">直链跳转</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> Telegram </td>
|
||||
</tr>
|
||||
</table>
|
||||
[Discord](https://wu-clan.github.io/homepage/)
|
||||
|
||||
## Sponsor us
|
||||
|
||||
If this program has helped you, you can sponsor us with some coffee beans :coffee:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://github.com/wu-clan/image/blob/master/pay/weixin.jpg?raw=true" width="180px" alt="WeChat"/>
|
||||
<td><img src="https://github.com/wu-clan/image/blob/master/pay/zfb.jpg?raw=true" width="180px" alt="Alipay"/>
|
||||
<td><img src="https://github.com/wu-clan/image/blob/master/pay/ERC20.jpg?raw=true" width="180px" alt="0x40D5e2304b452256afD9CE2d3d5531dc8d293138"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">WeChat Pay</td>
|
||||
<td align="center">Ali Pay</td>
|
||||
<td align="center">ERC20</td>
|
||||
</tr>
|
||||
</table>
|
||||
If this program has helped you, you can sponsor us with some coffee
|
||||
beans: [:coffee: Sponsor :coffee:](https://wu-clan.github.io/sponsor/)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of
|
||||
This project is licensed by the terms of
|
||||
the [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) license
|
||||
|
||||
[](https://starchart.cc/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
+30
-194
@@ -1,18 +1,27 @@
|
||||
# FastAPI 最佳架构
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||
<img alt="Logo 包含了 FBA 三个字母抽象结合,形成了一个类似从地面扩散投影上来的闪电" width="320" src="https://wu-clan.github.io/picx-images-hosting/logo/fba.png">
|
||||
|
||||
> [!TIP]
|
||||
> 你正在查看 pydantic-v1 分支,此分支已被锁定,不再提供更新和修复
|
||||
# FastAPI Best Architecture
|
||||
|
||||
企业级后端架构解决方案
|
||||
|
||||
简体中文 | [English](./README.md)
|
||||
|
||||
基于 FastAPI 框架的前后端分离中后台解决方案,遵循[伪三层架构](#伪三层架构)设计, 支持 **python3.10** 及以上版本
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||

|
||||

|
||||

|
||||
[](https://pydantic.dev)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://github.com/astral-sh/uv)
|
||||

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

|
||||
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
它的目的是让你可以直接用它作为你新项目的基础架构,本仓库作为模板库公开,任何人或企业均可免费使用
|
||||
|
||||
**持续更新维护中**
|
||||
</div>
|
||||
|
||||
## 伪三层架构
|
||||
|
||||
@@ -20,6 +29,8 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
||||
|
||||
在 python web 开发中,三层架构的概念并没有通用标准,所以这里我们称之为伪三层架构
|
||||
|
||||
但请注意,我们并没有传统的多应用程序结构(django、springBoot...),如果您不喜欢这种模式,可以使用模板对其进行随意改造!
|
||||
|
||||
| 工作流程 | java | fastapi_best_architecture |
|
||||
|------|----------------|---------------------------|
|
||||
| 视图 | controller | api |
|
||||
@@ -28,170 +39,15 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
||||
| 数据访问 | dao / mapper | crud |
|
||||
| 模型 | model / entity | model |
|
||||
|
||||
## 在线预览
|
||||
## 帮助
|
||||
|
||||
遗憾的是,我们没有资金提供在线预览,您可以通过查看[本地开发](#本地开发)进行部署,或直接使用 [Docker](#docker-部署)
|
||||
进行部署,或者在 [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui)
|
||||
查看部分截图预览
|
||||
|
||||
## 特征
|
||||
|
||||
- [x] 使用 FastAPI PEP 593 Annotated 参数设计
|
||||
- [x] async/await + asgiref 的全局异步设计
|
||||
- [x] 遵循 Restful API 规范
|
||||
- [x] 全局 SQLAlchemy 2.0 语法
|
||||
- [x] Casbin RBAC 访问控制模型
|
||||
- [x] Celery 异步任务
|
||||
- [x] JWT 中间件白名单认证
|
||||
- [x] 全局自定义时区时间
|
||||
- [x] Docker / Docker-compose 部署
|
||||
- [x] Pytest 单元测试
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. [x] 用户管理:系统用户角色管理,权限分配
|
||||
2. [x] 部门管理:配置系统组织机构(公司、部门、小组...)
|
||||
3. [x] 菜单管理:配置系统菜单,用户菜单,按钮权限标识
|
||||
4. [x] 角色管理:角色菜单权限分配,角色路由权限分配
|
||||
5. [x] 字典管理:维护系统内部常用固定数据或参数
|
||||
6. [x] 操作日志:系统正常操作和异常操作日志记录和查询
|
||||
7. [x] 登录认证:图形验证码后台认证登录
|
||||
8. [x] 登录日志:用户正常登录和异常登录的日志记录与查询
|
||||
9. [x] 服务监控:服务器硬件设备信息与状态
|
||||
10. [x] 定时任务:自动化任务,异步任务,支持函数调用
|
||||
11. [x] 接口文档:自动生成在线交互式 API 接口文档
|
||||
|
||||
## 本地开发
|
||||
|
||||
* Python 3.10+
|
||||
* Mysql 8.0+
|
||||
* Redis 推荐最新稳定版
|
||||
* Nodejs 14.0+
|
||||
|
||||
### 后端
|
||||
|
||||
1. 安装依赖项
|
||||
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. 创建一个数据库 `fba`,选择 utf8mb4 编码
|
||||
3. 安装并启动 Redis
|
||||
4. 在 `backend/app/` 目录下创建一个 `.env` 文件
|
||||
|
||||
```shell
|
||||
cd backend/app/
|
||||
touch .env
|
||||
```
|
||||
|
||||
5. 复制 `.env.example` 到 `.env`
|
||||
|
||||
```shell
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
6. 数据库迁移 [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
|
||||
|
||||
```shell
|
||||
cd backend/app/
|
||||
|
||||
# 生成迁移文件
|
||||
alembic revision --autogenerate
|
||||
|
||||
# 执行迁移
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
7. 启动 celery worker 和 beat
|
||||
|
||||
```shell
|
||||
celery -A tasks worker --loglevel=INFO
|
||||
# 可选,如果您不需要使用计划任务
|
||||
celery -A tasks beat --loglevel=INFO
|
||||
```
|
||||
|
||||
8. 按需修改配置文件
|
||||
9. 执行 `backend/app/main.py` 文件启动服务
|
||||
10. 浏览器访问:http://127.0.0.1:8000/api/v1/docs
|
||||
|
||||
---
|
||||
|
||||
### 前端
|
||||
|
||||
跳转 [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) 查看详情
|
||||
|
||||
### Docker 部署
|
||||
|
||||
> [!WARNING]
|
||||
> 默认端口冲突:8000,3306,6379,5672
|
||||
>
|
||||
> 最佳做法是在部署之前关闭本地服务:mysql,redis,rabbitmq...
|
||||
|
||||
1. 进入 `docker-compose.yml` 文件所在目录,创建环境变量文件`.env`
|
||||
|
||||
```shell
|
||||
cd deploy/docker-compose/
|
||||
|
||||
cp .env.server ../../backend/app/.env
|
||||
|
||||
# 此命令为可选
|
||||
cp .env.docker .env
|
||||
```
|
||||
|
||||
2. 按需修改配置文件
|
||||
3. 执行一键启动命令
|
||||
|
||||
```shell
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
4. 等待命令自动完成
|
||||
5. 浏览器访问:http://127.0.0.1:8000/api/v1/docs
|
||||
|
||||
## 测试数据
|
||||
|
||||
使用 `backend/sql/init_test_data.sql` 文件初始化测试数据
|
||||
|
||||
## 开发流程
|
||||
|
||||
仅供参考
|
||||
|
||||
### 后端:
|
||||
|
||||
1. 定义数据库模型(model),每次变化记得执行数据库迁移
|
||||
2. 定义数据验证模型(schema)
|
||||
3. 定义路由(router)和视图(api)
|
||||
4. 定义业务逻辑(service)
|
||||
5. 编写数据库操作(crud)
|
||||
|
||||
### 前端
|
||||
|
||||
跳转 [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) 查看详情
|
||||
|
||||
## 测试
|
||||
|
||||
通过 pytest 执行单元测试
|
||||
|
||||
1. 创建测试数据库 `fba_test`,选择 utf8mb4 编码
|
||||
2. 使用 `backend/sql/create_tables.sql` 文件创建数据库表
|
||||
3. 使用 `backend/sql/init_pytest_data.sql` 文件初始化测试数据
|
||||
4. 进入app目录
|
||||
|
||||
```shell
|
||||
cd backend/app/
|
||||
```
|
||||
|
||||
5. 执行测试命令
|
||||
|
||||
```shell
|
||||
pytest -vs --disable-warnings
|
||||
```
|
||||
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||
|
||||
## 贡献者
|
||||
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wu-clan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/52145145?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/downdawn" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/41266749?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<a href="https://github.com/fastapi-practices/fastapi_best_architecture/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=fastapi-practices/fastapi_best_architecture"/>
|
||||
</a>
|
||||
|
||||
## 特别鸣谢
|
||||
|
||||
@@ -204,34 +60,14 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
||||
|
||||
## 互动
|
||||
|
||||
有且仅有当前一个频道,请注意辨别真伪
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://t.me/+ZlPhIFkPp7E4NGI1">直链跳转</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> Telegram </td>
|
||||
</tr>
|
||||
</table>
|
||||
[Discord](https://wu-clan.github.io/homepage/)
|
||||
|
||||
## 赞助我们
|
||||
|
||||
如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励 :coffee:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://github.com/wu-clan/image/blob/master/pay/weixin.jpg?raw=true" width="180px" alt="Wechat"/>
|
||||
<td><img src="https://github.com/wu-clan/image/blob/master/pay/zfb.jpg?raw=true" width="180px" alt="Alipay"/>
|
||||
<td><img src="https://github.com/wu-clan/image/blob/master/pay/ERC20.jpg?raw=true" width="180px" alt="0x40D5e2304b452256afD9CE2d3d5531dc8d293138"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">微信</td>
|
||||
<td align="center">支付宝</td>
|
||||
<td align="center">ERC20</td>
|
||||
</tr>
|
||||
</table>
|
||||
如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励:[:coffee: Sponsor :coffee:](https://wu-clan.github.io/sponsor/)
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目根据 [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) 许可证的条款进行许可
|
||||
本项目由 [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) 许可证的条款进行许可
|
||||
|
||||
[](https://starchart.cc/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /fba
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||
&& sed -i 's|security.debian.org/debian-security|mirrors.ustc.edu.cn/debian-security|g' /etc/apt/sources.list.d/debian.sources
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends gcc python3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 某些包可能存在同步不及时导致安装失败的情况,可更改为官方源:https://pypi.org/simple
|
||||
RUN pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \
|
||||
&& pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
|
||||
|
||||
ENV TZ = Asia/Shanghai
|
||||
|
||||
RUN mkdir -p /var/log/fastapi_server
|
||||
|
||||
COPY ./deploy/fastapi_server.conf /etc/supervisor/conf.d/
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
CMD ["uvicorn", "backend.app.main:app", "--host", "127.0.0.1", "--port", "8000"]
|
||||
@@ -0,0 +1,35 @@
|
||||
# Env
|
||||
ENVIRONMENT='dev'
|
||||
# Database
|
||||
DATABASE_TYPE='mysql'
|
||||
DATABASE_HOST='127.0.0.1'
|
||||
DATABASE_PORT=3306
|
||||
DATABASE_USER='root'
|
||||
DATABASE_PASSWORD='123456'
|
||||
# Redis
|
||||
REDIS_HOST='127.0.0.1'
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=''
|
||||
REDIS_DATABASE=0
|
||||
# Token
|
||||
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
||||
# Opera Log
|
||||
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
||||
# [ App ] task
|
||||
# Celery
|
||||
CELERY_BROKER_REDIS_DATABASE=1
|
||||
# Rabbitmq
|
||||
CELERY_RABBITMQ_HOST='127.0.0.1'
|
||||
CELERY_RABBITMQ_PORT=5672
|
||||
CELERY_RABBITMQ_USERNAME='guest'
|
||||
CELERY_RABBITMQ_PASSWORD='guest'
|
||||
# [ Plugin ] oauth2
|
||||
OAUTH2_GITHUB_CLIENT_ID='test'
|
||||
OAUTH2_GITHUB_CLIENT_SECRET='test'
|
||||
OAUTH2_GOOGLE_CLIENT_ID='test'
|
||||
OAUTH2_GOOGLE_CLIENT_SECRET='test'
|
||||
OAUTH2_LINUX_DO_CLIENT_ID='test'
|
||||
OAUTH2_LINUX_DO_CLIENT_SECRET='test'
|
||||
# [ Plugin ] email
|
||||
EMAIL_USERNAME=''
|
||||
EMAIL_PASSWORD=''
|
||||
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
.env
|
||||
alembic/versions/
|
||||
static/media/
|
||||
*.log
|
||||
celerybeat-schedule.*
|
||||
@@ -0,0 +1,83 @@
|
||||
# FastAPI Best Architecture - Backend
|
||||
|
||||
## Docker
|
||||
|
||||
1. Make sure you're at the root of the project
|
||||
2. Run the following Docker command to build container:
|
||||
|
||||
```shell
|
||||
docker build -f Dockerfile -t fba_backend_independent .
|
||||
```
|
||||
|
||||
3. Start container
|
||||
|
||||
Native boot needs to change `127.0.0.1` in `.env` to `host.docker.internal`
|
||||
|
||||
```shell
|
||||
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Prerequisites
|
||||
|
||||
- Python >= 3.10
|
||||
- Git
|
||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
- Fork this repository to your GitHub account
|
||||
|
||||
2. Installation and setup
|
||||
|
||||
Clone your forked repository:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/<your account>/fastapi_best_architecture.git
|
||||
```
|
||||
|
||||
Go to the root directory of the project, open the terminal, and run the following command:
|
||||
|
||||
```sh
|
||||
uv sync --frozen
|
||||
```
|
||||
|
||||
3. Checkout
|
||||
|
||||
Checkout a new branch and make your changes
|
||||
|
||||
```shell
|
||||
git checkout -b your-new-feature-branch
|
||||
```
|
||||
|
||||
4. Format and Lint
|
||||
|
||||
Auto-formatting and lint via `pre-commit`
|
||||
|
||||
```shell
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
5. Commit and push
|
||||
|
||||
Commit your changes and push your branch to the GitHub.
|
||||
|
||||
6. PR
|
||||
|
||||
Create a PR via GitHub
|
||||
|
||||
## Scripts
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> The following script may not apply to the Windows platform
|
||||
>
|
||||
> It is recommended to execute under the backend directory, and chmod authorization may be required
|
||||
|
||||
- `pre_start.sh`: Perform automatic database migration
|
||||
|
||||
- `celery-start.sh`: For celery docker script, implementation is not recommended
|
||||
|
||||
- `scripts/format.sh`: Perform ruff format check
|
||||
|
||||
- `scripts/lint.sh`: Perform pre-commit formatting
|
||||
|
||||
- `scripts/export.sh`: Execute uv export dependency package
|
||||
@@ -0,0 +1,7 @@
|
||||
from backend.common.i18n import i18n
|
||||
|
||||
__version__ = '1.8.2'
|
||||
|
||||
|
||||
# 初始化 i18n
|
||||
i18n.load_locales()
|
||||
@@ -1,11 +1,13 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
# path to migration scripts.
|
||||
# Use forward slashes (/) also on windows to provide an os agnostic path
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration files
|
||||
file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(hour).2d-%%(minute).2d_%%(rev)s_%%(slug)s
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d_%%(minute).2d_%%(second).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
@@ -13,14 +15,13 @@ prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
||||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
@@ -35,16 +36,24 @@ prepend_sys_path = .
|
||||
# version location specification; This defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator"
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. Valid values are:
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # default: use os.pathsep
|
||||
# version_path_separator = newline
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
@@ -64,6 +73,12 @@ sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
@@ -0,0 +1,3 @@
|
||||
Generic single-database configuration with an async dbapi.
|
||||
|
||||
If you add a new app to the framework, be sure to read the contents of the `env.py` file.
|
||||
@@ -0,0 +1,122 @@
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from backend.app import get_app_models
|
||||
from backend.common.model import MappedBase
|
||||
from backend.core import path_conf
|
||||
from backend.database.db import SQLALCHEMY_DATABASE_URL
|
||||
from backend.plugin.tools import get_plugin_models
|
||||
|
||||
# import models
|
||||
for cls in get_app_models() + get_plugin_models():
|
||||
class_name = cls.__name__
|
||||
if class_name not in globals():
|
||||
globals()[class_name] = cls
|
||||
|
||||
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
|
||||
os.makedirs(path_conf.ALEMBIC_VERSION_DIR)
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
alembic_config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if alembic_config.config_file_name is not None:
|
||||
fileConfig(alembic_config.config_file_name)
|
||||
|
||||
# model's MetaData object
|
||||
# for 'autogenerate' support
|
||||
target_metadata = MappedBase.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
alembic_config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
SQLALCHEMY_DATABASE_URL.render_as_string(hide_password=False).replace('%', '%%'),
|
||||
)
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = alembic_config.get_main_option('sqlalchemy.url')
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={'paramstyle': 'named'},
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
transaction_per_migration=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
# 当迁移无变化时,不生成迁移记录
|
||||
def process_revision_directives(context, revision, directives) -> None: # noqa: ANN001
|
||||
if alembic_config.cmd_opts.autogenerate:
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
print('\nNo changes in model detected')
|
||||
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
transaction_per_migration=True,
|
||||
process_revision_directives=process_revision_directives,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
"""In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
connectable = async_engine_from_config(
|
||||
alembic_config.get_section(alembic_config.config_ini_section, {}),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode."""
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -7,6 +7,7 @@ Create Date: ${create_date}
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import backend.common.model
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Env: dev、pro
|
||||
ENVIRONMENT='dev'
|
||||
# MySQL
|
||||
DB_HOST='127.0.0.1'
|
||||
DB_PORT=3306
|
||||
DB_USER='root'
|
||||
DB_PASSWORD='123456'
|
||||
# Redis
|
||||
REDIS_HOST='127.0.0.1'
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=''
|
||||
REDIS_DATABASE=0
|
||||
# Celery
|
||||
CELERY_REDIS_HOST='127.0.0.1'
|
||||
CELERY_REDIS_PORT=6379
|
||||
CELERY_REDIS_PASSWORD=''
|
||||
CELERY_BROKER_REDIS_DATABASE=1
|
||||
CELERY_BACKEND_REDIS_DATABASE=2
|
||||
# Rabbitmq
|
||||
RABBITMQ_HOST='127.0.0.1'
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USERNAME='guest'
|
||||
RABBITMQ_PASSWORD='guest'
|
||||
# Token
|
||||
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
|
||||
# Opera Log
|
||||
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
|
||||
+29
-2
@@ -1,2 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os.path
|
||||
|
||||
from backend.core.path_conf import BASE_PATH
|
||||
from backend.utils.import_parse import get_model_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
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
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.monitor import router as monitor_router
|
||||
from backend.app.admin.api.v1.sys import router as sys_router
|
||||
from backend.core.conf import settings
|
||||
|
||||
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
|
||||
|
||||
v1.include_router(auth_router)
|
||||
v1.include_router(sys_router)
|
||||
v1.include_router(log_router)
|
||||
v1.include_router(monitor_router)
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.app.admin.api.v1.auth.auth import router as auth_router
|
||||
from backend.app.admin.api.v1.auth.captcha import router as captcha_router
|
||||
|
||||
router = APIRouter(prefix='/auth')
|
||||
|
||||
router.include_router(auth_router, tags=['授权'])
|
||||
router.include_router(captcha_router, tags=['验证码'])
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from fastapi.security import HTTPBasicCredentials
|
||||
from fastapi_limiter.depends import RateLimiter
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from backend.app.admin.schema.token import GetLoginToken, GetNewToken, GetSwaggerToken
|
||||
from backend.app.admin.schema.user import AuthLoginParam
|
||||
from backend.app.admin.service.auth_service import auth_service
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证')
|
||||
async def login_swagger(
|
||||
db: CurrentSessionTransaction, obj: Annotated[HTTPBasicCredentials, Depends()]
|
||||
) -> GetSwaggerToken:
|
||||
token, user = await auth_service.swagger_login(db=db, obj=obj)
|
||||
return GetSwaggerToken(access_token=token, user=user)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/login',
|
||||
summary='用户登录',
|
||||
description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman',
|
||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
||||
)
|
||||
async def login(
|
||||
db: CurrentSessionTransaction,
|
||||
response: Response,
|
||||
obj: AuthLoginParam,
|
||||
background_tasks: BackgroundTasks,
|
||||
) -> ResponseSchemaModel[GetLoginToken]:
|
||||
data = await auth_service.login(db=db, response=response, obj=obj, background_tasks=background_tasks)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/codes', summary='获取所有授权码', description='适配 vben admin v5', dependencies=[DependsJwtAuth])
|
||||
async def get_codes(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[str]]:
|
||||
codes = await auth_service.get_codes(db=db, request=request)
|
||||
return response_base.success(data=codes)
|
||||
|
||||
|
||||
@router.post('/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)
|
||||
|
||||
|
||||
@router.post('/logout', summary='用户登出')
|
||||
async def logout(request: Request, response: Response) -> ResponseModel:
|
||||
await auth_service.logout(request=request, response=response)
|
||||
return response_base.success()
|
||||
@@ -0,0 +1,34 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from fast_captcha import img_captcha
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi_limiter.depends import RateLimiter
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.app.admin.schema.captcha import GetCaptchaDetail
|
||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/captcha',
|
||||
summary='获取登录验证码',
|
||||
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
|
||||
)
|
||||
async def get_captcha() -> ResponseSchemaModel[GetCaptchaDetail]:
|
||||
"""
|
||||
此接口可能存在性能损耗,尽管是异步接口,但是验证码生成是IO密集型任务,使用线程池尽量减少性能损耗
|
||||
"""
|
||||
img_type: str = 'base64'
|
||||
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
|
||||
uuid = str(uuid4())
|
||||
await redis_client.set(
|
||||
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{uuid}',
|
||||
code,
|
||||
ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
|
||||
)
|
||||
data = GetCaptchaDetail(uuid=uuid, img_type=img_type, image=img)
|
||||
return response_base.success(data=data)
|
||||
@@ -0,0 +1,9 @@
|
||||
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.opera_log import router as opera_log
|
||||
|
||||
router = APIRouter(prefix='/logs')
|
||||
|
||||
router.include_router(login_log, prefix='/login', tags=['登录日志'])
|
||||
router.include_router(opera_log, prefix='/opera', tags=['操作日志'])
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.admin.schema.login_log import DeleteLoginLogParam, GetLoginLogDetail
|
||||
from backend.app.admin.service.login_log_service import login_log_service
|
||||
from backend.common.pagination import DependsPagination, PageData
|
||||
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(
|
||||
'',
|
||||
summary='分页获取登录日志',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_login_logs_paginated(
|
||||
db: CurrentSession,
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
|
||||
page_data = await login_log_service.get_list(db=db, username=username, status=status, ip=ip)
|
||||
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'',
|
||||
summary='批量删除登录日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:login:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_login_logs(db: CurrentSessionTransaction, obj: DeleteLoginLogParam) -> ResponseModel:
|
||||
count = await login_log_service.delete(db=db, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/all',
|
||||
summary='清空登录日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:login:clear')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_login_logs(db: CurrentSessionTransaction) -> ResponseModel:
|
||||
await login_log_service.delete_all(db=db)
|
||||
return response_base.success()
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from backend.app.admin.schema.opera_log import DeleteOperaLogParam, GetOperaLogDetail
|
||||
from backend.app.admin.service.opera_log_service import opera_log_service
|
||||
from backend.common.pagination import DependsPagination, PageData
|
||||
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(
|
||||
'',
|
||||
summary='分页获取操作日志',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_opera_logs_paginated(
|
||||
db: CurrentSession,
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
|
||||
page_data = await opera_log_service.get_list(db=db, username=username, status=status, ip=ip)
|
||||
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'',
|
||||
summary='批量删除操作日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:opera:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_opera_logs(db: CurrentSessionTransaction, obj: DeleteOperaLogParam) -> ResponseModel:
|
||||
count = await opera_log_service.delete(db=db, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/all',
|
||||
summary='清空操作日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:opera:clear')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_all_opera_logs(db: CurrentSessionTransaction) -> ResponseModel:
|
||||
await opera_log_service.delete_all(db=db)
|
||||
return response_base.success()
|
||||
@@ -0,0 +1,11 @@
|
||||
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.server import router as server_router
|
||||
|
||||
router = APIRouter(prefix='/monitors')
|
||||
|
||||
router.include_router(redis_router, prefix='/redis', tags=['redis监控'])
|
||||
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()
|
||||
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.common.response.response_schema import ResponseModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.utils.redis_info import redis_info
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='redis 监控', dependencies=[DependsJwtAuth])
|
||||
async def get_redis_info() -> ResponseModel:
|
||||
data = {
|
||||
'info': await redis_info.get_info(),
|
||||
'stats': await redis_info.get_stats(),
|
||||
}
|
||||
return response_base.success(data=data)
|
||||
@@ -1,23 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.utils.server_info import server_info
|
||||
from backend.common.response.response_schema import ResponseModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.utils.server_info import server_info
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/server', summary='server 监控', dependencies=[DependsJwtAuth])
|
||||
async def get_server_info():
|
||||
"""IO密集型任务,使用线程池尽量减少性能损耗"""
|
||||
@router.get('', summary='server 监控', dependencies=[DependsJwtAuth])
|
||||
async def get_server_info() -> ResponseModel:
|
||||
data = {
|
||||
# 扔到线程池,避免阻塞
|
||||
'cpu': await run_in_threadpool(server_info.get_cpu_info),
|
||||
'mem': await run_in_threadpool(server_info.get_mem_info),
|
||||
'sys': await run_in_threadpool(server_info.get_sys_info),
|
||||
'disk': await run_in_threadpool(server_info.get_disk_info),
|
||||
'service': await run_in_threadpool(server_info.get_service_info),
|
||||
}
|
||||
return await response_base.success(data=data)
|
||||
return response_base.success(data=data)
|
||||
@@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
|
||||
from backend.app.admin.api.v1.sys.data_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.files import router as file_router
|
||||
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
||||
from backend.app.admin.api.v1.sys.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.user import router as user_router
|
||||
|
||||
router = APIRouter(prefix='/sys')
|
||||
|
||||
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
|
||||
router.include_router(menu_router, prefix='/menus', tags=['系统菜单'])
|
||||
router.include_router(role_router, prefix='/roles', tags=['系统角色'])
|
||||
router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
||||
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
|
||||
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
|
||||
router.include_router(file_router, prefix='/files', tags=['系统文件'])
|
||||
router.include_router(plugin_router, prefix='/plugins', tags=['系统插件'])
|
||||
@@ -0,0 +1,112 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
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.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('/models', summary='获取数据规则可用模型', dependencies=[DependsJwtAuth])
|
||||
async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
||||
models = await data_rule_service.get_models()
|
||||
return response_base.success(data=models)
|
||||
|
||||
|
||||
@router.get('/models/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||
async def get_data_rule_model_columns(
|
||||
model: Annotated[str, Path(description='模型名称')],
|
||||
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
||||
models = await data_rule_service.get_columns(model=model)
|
||||
return response_base.success(data=models)
|
||||
|
||||
|
||||
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
||||
async def get_all_data_rules(db: CurrentSession) -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
||||
data = await data_rule_service.get_all(db=db)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
|
||||
async def get_data_rule(
|
||||
db: CurrentSession,
|
||||
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||
) -> ResponseSchemaModel[GetDataRuleDetail]:
|
||||
data = await data_rule_service.get(db=db, pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有数据规则',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_data_rules_paginated(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='规则名称')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
||||
page_data = await data_rule_service.get_list(db=db, name=name)
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建数据规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('data:rule:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_data_rule(db: CurrentSessionTransaction, obj: CreateDataRuleParam) -> ResponseModel:
|
||||
await data_rule_service.create(db=db, obj=obj)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新数据规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('data:rule:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_data_rule(
|
||||
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:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'',
|
||||
summary='批量删除数据规则',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('data:rule:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_data_rules(db: CurrentSessionTransaction, obj: DeleteDataRuleParam) -> ResponseModel:
|
||||
count = await data_rule_service.delete(db=db, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,79 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, GetDeptTree, UpdateDeptParam
|
||||
from backend.app.admin.service.dept_service import dept_service
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.common.security.rbac import DependsRBAC
|
||||
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
|
||||
async def get_dept(
|
||||
db: CurrentSession, pk: Annotated[int, Path(description='部门 ID')]
|
||||
) -> ResponseSchemaModel[GetDeptDetail]:
|
||||
data = await dept_service.get(db=db, pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='获取部门树', dependencies=[DependsJwtAuth])
|
||||
async def get_dept_tree(
|
||||
db: CurrentSession,
|
||||
request: Request,
|
||||
name: Annotated[str | None, Query(description='部门名称')] = None,
|
||||
leader: Annotated[str | None, Query(description='部门负责人')] = None,
|
||||
phone: Annotated[str | None, Query(description='联系电话')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
) -> ResponseSchemaModel[list[GetDeptTree]]:
|
||||
dept = await dept_service.get_tree(db=db, request=request, name=name, leader=leader, phone=phone, status=status)
|
||||
return response_base.success(data=dept)
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建部门',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dept:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_dept(db: CurrentSessionTransaction, obj: CreateDeptParam) -> ResponseModel:
|
||||
await dept_service.create(db=db, obj=obj)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新部门',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dept:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_dept(
|
||||
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam
|
||||
) -> ResponseModel:
|
||||
count = await dept_service.update(db=db, pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{pk}',
|
||||
summary='删除部门',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dept:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_dept(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
|
||||
count = await dept_service.delete(db=db, pk=pk)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -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}'})
|
||||
@@ -0,0 +1,82 @@
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.menu import CreateMenuParam, GetMenuDetail, GetMenuTree, UpdateMenuParam
|
||||
from backend.app.admin.service.menu_service import menu_service
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.common.security.rbac import DependsRBAC
|
||||
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='已适配 vben admin v5', dependencies=[DependsJwtAuth])
|
||||
async def get_user_sidebar(db: CurrentSession, request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||
menu = await menu_service.get_sidebar(db=db, request=request)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
|
||||
async def get_menu(
|
||||
db: CurrentSession, pk: Annotated[int, Path(description='菜单 ID')]
|
||||
) -> ResponseSchemaModel[GetMenuDetail]:
|
||||
data = await menu_service.get(db=db, pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='获取菜单树', dependencies=[DependsJwtAuth])
|
||||
async def get_menu_tree(
|
||||
db: CurrentSession,
|
||||
title: Annotated[str | None, Query(description='菜单标题')] = None,
|
||||
status: Annotated[int | None, Query(description='状体')] = None,
|
||||
) -> ResponseSchemaModel[list[GetMenuTree]]:
|
||||
menu = await menu_service.get_tree(db=db, title=title, status=status)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:menu:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_menu(db: CurrentSessionTransaction, obj: CreateMenuParam) -> ResponseModel:
|
||||
await menu_service.create(db=db, obj=obj)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:menu:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_menu(
|
||||
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam
|
||||
) -> ResponseModel:
|
||||
count = await menu_service.update(db=db, pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{pk}',
|
||||
summary='删除菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:menu:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_menu(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
|
||||
count = await menu_service.delete(db=db, pk=pk)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -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'},
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.admin.schema.menu import GetMenuTree
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
DeleteRoleParam,
|
||||
GetRoleDetail,
|
||||
GetRoleWithRelationDetail,
|
||||
UpdateRoleMenuParam,
|
||||
UpdateRoleParam,
|
||||
UpdateRoleScopeParam,
|
||||
)
|
||||
from backend.app.admin.service.role_service import role_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_roles(db: CurrentSession) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
data = await role_service.get_all(db=db)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}/menus', summary='获取角色菜单树', dependencies=[DependsJwtAuth])
|
||||
async def get_role_menu_tree(
|
||||
db: CurrentSession,
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
) -> ResponseSchemaModel[list[GetMenuTree] | None]:
|
||||
menu = await role_service.get_menu_tree(db=db, pk=pk)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
|
||||
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
|
||||
async def get_role_scopes(
|
||||
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
|
||||
) -> ResponseSchemaModel[list[int]]:
|
||||
rule = await role_service.get_scopes(db=db, pk=pk)
|
||||
return response_base.success(data=rule)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
|
||||
async def get_role(
|
||||
db: CurrentSession, pk: Annotated[int, Path(description='角色 ID')]
|
||||
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
|
||||
data = await role_service.get(db=db, pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有角色',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_roles_paginated(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='角色名称')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
|
||||
page_data = await role_service.get_list(db=db, name=name, status=status)
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def create_role(db: CurrentSessionTransaction, obj: CreateRoleParam) -> ResponseModel:
|
||||
await role_service.create(db=db, obj=obj)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_role(
|
||||
db: CurrentSessionTransaction, pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam
|
||||
) -> ResponseModel:
|
||||
count = await role_service.update(db=db, pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}/menus',
|
||||
summary='更新角色菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:menu:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_role_menus(
|
||||
db: CurrentSessionTransaction,
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
menu_ids: UpdateRoleMenuParam,
|
||||
) -> ResponseModel:
|
||||
count = await role_service.update_role_menu(db=db, pk=pk, menu_ids=menu_ids)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}/scopes',
|
||||
summary='更新角色数据范围',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:scope:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_role_scopes(
|
||||
db: CurrentSessionTransaction,
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
scope_ids: UpdateRoleScopeParam,
|
||||
) -> ResponseModel:
|
||||
count = await role_service.update_role_scope(db=db, pk=pk, scope_ids=scope_ids)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
'',
|
||||
summary='批量删除角色',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_roles(db: CurrentSessionTransaction, obj: DeleteRoleParam) -> ResponseModel:
|
||||
count = await role_service.delete(db=db, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -0,0 +1,172 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.role import GetRoleDetail
|
||||
from backend.app.admin.schema.user import (
|
||||
AddUserParam,
|
||||
GetCurrentUserInfoWithRelationDetail,
|
||||
GetUserInfoWithRelationDetail,
|
||||
ResetPasswordParam,
|
||||
UpdateUserParam,
|
||||
)
|
||||
from backend.app.admin.service.user_service import user_service
|
||||
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.security.jwt import DependsJwtAuth, DependsSuperUser
|
||||
from backend.common.security.permission import RequestPermission
|
||||
from backend.common.security.rbac import DependsRBAC
|
||||
from backend.database.db import CurrentSession, CurrentSessionTransaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
|
||||
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
|
||||
data = request.user.model_dump()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取用户信息', dependencies=[DependsJwtAuth])
|
||||
async def get_userinfo(
|
||||
db: CurrentSession,
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||
data = await user_service.get_userinfo(db=db, pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||
async def get_user_roles(
|
||||
db: CurrentSession, pk: Annotated[int, Path(description='用户 ID')]
|
||||
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
data = await user_service.get_roles(db=db, pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有用户',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_users_paginated(
|
||||
db: CurrentSession,
|
||||
dept: Annotated[int | None, Query(description='部门 ID')] = None,
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
phone: Annotated[str | None, Query(description='手机号')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
|
||||
page_data = await user_service.get_list(db=db, dept=dept, username=username, phone=phone, status=status)
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post('', summary='创建用户', dependencies=[DependsSuperUser])
|
||||
async def create_user(
|
||||
db: CurrentSessionTransaction, obj: AddUserParam
|
||||
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||
await user_service.create(db=db, obj=obj)
|
||||
data = await user_service.get_userinfo(db=db, username=obj.username)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新用户信息', dependencies=[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:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/{pk}/permissions', summary='更新用户权限', dependencies=[DependsSuperUser])
|
||||
async def update_user_permission(
|
||||
db: CurrentSessionTransaction,
|
||||
request: Request,
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
type: Annotated[UserPermissionType, Query(description='权限类型')],
|
||||
) -> ResponseModel:
|
||||
count = await user_service.update_permission(db=db, request=request, pk=pk, type=type)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/me/password', summary='更新当前用户密码', dependencies=[DependsJwtAuth])
|
||||
async def update_user_password(
|
||||
db: CurrentSessionTransaction, request: Request, obj: ResetPasswordParam
|
||||
) -> ResponseModel:
|
||||
count = await user_service.update_password(db=db, request=request, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsSuperUser])
|
||||
async def reset_user_password(
|
||||
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:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
|
||||
@router.delete(
|
||||
path='/{pk}',
|
||||
summary='删除用户',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:user:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_user(db: CurrentSessionTransaction, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||
count = await user_service.delete(db=db, pk=pk)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -0,0 +1,89 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import DataRule
|
||||
from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRuleParam
|
||||
|
||||
|
||||
class CRUDDataRule(CRUDPlus[DataRule]):
|
||||
"""数据规则数据库操作类"""
|
||||
|
||||
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
|
||||
"""
|
||||
获取规则详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 规则 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, pk)
|
||||
|
||||
async def get_select(self, name: str | None) -> Select:
|
||||
"""
|
||||
获取规则列表查询表达式
|
||||
|
||||
:param name: 规则名称
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if name is not None:
|
||||
filters['name__like'] = f'%{name}%'
|
||||
|
||||
return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
|
||||
|
||||
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
||||
"""
|
||||
通过名称获取规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:param name: 规则名称
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, name=name)
|
||||
|
||||
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
|
||||
"""
|
||||
获取所有规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
return await self.select_models(db)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateDataRuleParam) -> None:
|
||||
"""
|
||||
创建规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 创建规则参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||
"""
|
||||
更新规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 规则 ID
|
||||
:param obj: 更新规则参数
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, pk, obj)
|
||||
|
||||
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_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)
|
||||
@@ -0,0 +1,121 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Dept
|
||||
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
||||
from backend.common.security.permission import filter_data_permission
|
||||
|
||||
|
||||
class CRUDDept(CRUDPlus[Dept]):
|
||||
"""部门数据库操作类"""
|
||||
|
||||
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
||||
"""
|
||||
获取部门详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param dept_id: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
通过名称获取部门
|
||||
|
||||
:param db: 数据库会话
|
||||
:param name: 部门名称
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, name=name, del_flag=0)
|
||||
|
||||
async def get_all(
|
||||
self,
|
||||
request: Request,
|
||||
db: AsyncSession,
|
||||
name: str | None,
|
||||
leader: str | None,
|
||||
phone: str | None,
|
||||
status: int | None,
|
||||
) -> Sequence[Dept]:
|
||||
"""
|
||||
获取所有部门
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param db: 数据库会话
|
||||
:param name: 部门名称
|
||||
:param leader: 负责人
|
||||
:param phone: 联系电话
|
||||
:param status: 部门状态
|
||||
:return:
|
||||
"""
|
||||
filters = {'del_flag': 0}
|
||||
|
||||
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 obj: 创建部门参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def update(self, db: AsyncSession, dept_id: int, obj: UpdateDeptParam) -> int:
|
||||
"""
|
||||
更新部门
|
||||
|
||||
:param db: 数据库会话
|
||||
:param dept_id: 部门 ID
|
||||
:param obj: 更新部门参数
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, dept_id, obj)
|
||||
|
||||
async def delete(self, db: AsyncSession, dept_id: int) -> int:
|
||||
"""
|
||||
删除部门
|
||||
|
||||
:param db: 数据库会话
|
||||
:param dept_id: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
|
||||
|
||||
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
||||
"""
|
||||
获取部门及关联数据
|
||||
|
||||
:param db: 数据库会话
|
||||
:param dept_id: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, dept_id, load_strategies=['users'])
|
||||
|
||||
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
|
||||
"""
|
||||
获取子部门列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param dept_id: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_models(db, parent_id=dept_id, del_flag=0)
|
||||
|
||||
|
||||
dept_dao: CRUDDept = CRUDDept(Dept)
|
||||
@@ -0,0 +1,64 @@
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy import delete as sa_delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import LoginLog
|
||||
from backend.app.admin.schema.login_log import CreateLoginLogParam
|
||||
|
||||
|
||||
class CRUDLoginLog(CRUDPlus[LoginLog]):
|
||||
"""登录日志数据库操作类"""
|
||||
|
||||
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
||||
"""
|
||||
获取登录日志列表查询表达式
|
||||
|
||||
:param username: 用户名
|
||||
:param status: 登录状态
|
||||
:param ip: IP 地址
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if username is not None:
|
||||
filters['username__like'] = f'%{username}%'
|
||||
if status is not None:
|
||||
filters['status'] = status
|
||||
if ip is not None:
|
||||
filters['ip__like'] = f'%{ip}%'
|
||||
|
||||
return await self.select_order('created_time', 'desc', **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
|
||||
"""
|
||||
创建登录日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 创建登录日志参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj, commit=True)
|
||||
|
||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||
"""
|
||||
批量删除登录日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pks: 登录日志 ID 列表
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||
|
||||
@staticmethod
|
||||
async def delete_all(db: AsyncSession) -> None:
|
||||
"""
|
||||
删除所有日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
await db.execute(sa_delete(LoginLog))
|
||||
|
||||
|
||||
login_log_dao: CRUDLoginLog = CRUDLoginLog(LoginLog)
|
||||
@@ -0,0 +1,109 @@
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Menu
|
||||
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
||||
|
||||
|
||||
class CRUDMenu(CRUDPlus[Menu]):
|
||||
"""菜单数据库操作类"""
|
||||
|
||||
async def get(self, db: AsyncSession, menu_id: int) -> Menu | None:
|
||||
"""
|
||||
获取菜单详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param menu_id: 菜单 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, menu_id)
|
||||
|
||||
async def get_by_title(self, db: AsyncSession, title: str) -> Menu | None:
|
||||
"""
|
||||
通过标题获取菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param title: 菜单标题
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, title=title, type__ne=2)
|
||||
|
||||
async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
|
||||
"""
|
||||
获取菜单列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param title: 菜单标题
|
||||
:param status: 菜单状态
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if title is not None:
|
||||
filters['title__like'] = f'%{title}%'
|
||||
if status is not None:
|
||||
filters['status'] = status
|
||||
|
||||
return await self.select_models_order(db, 'sort', **filters)
|
||||
|
||||
async def get_sidebar(self, db: AsyncSession, menu_ids: list[int] | None) -> Sequence[Menu]:
|
||||
"""
|
||||
获取用户的菜单侧边栏
|
||||
|
||||
:param db: 数据库会话
|
||||
:param menu_ids: 菜单 ID 列表
|
||||
:return:
|
||||
"""
|
||||
filters = {'type__in': [0, 1, 3, 4]}
|
||||
|
||||
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 obj: 创建菜单参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def update(self, db: AsyncSession, menu_id: int, obj: UpdateMenuParam) -> int:
|
||||
"""
|
||||
更新菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param menu_id: 菜单 ID
|
||||
:param obj: 更新菜单参数
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, menu_id, obj)
|
||||
|
||||
async def delete(self, db: AsyncSession, menu_id: int) -> int:
|
||||
"""
|
||||
删除菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param menu_id: 菜单 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model(db, menu_id)
|
||||
|
||||
async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None]:
|
||||
"""
|
||||
获取子菜单列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param menu_id: 菜单 ID
|
||||
:return:
|
||||
"""
|
||||
menu = await self.select_model(db, menu_id, load_strategies=['children'])
|
||||
return menu.children
|
||||
|
||||
|
||||
menu_dao: CRUDMenu = CRUDMenu(Menu)
|
||||
@@ -0,0 +1,74 @@
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy import delete as sa_delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import OperaLog
|
||||
from backend.app.admin.schema.opera_log import CreateOperaLogParam
|
||||
|
||||
|
||||
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||
"""操作日志数据库操作类"""
|
||||
|
||||
async def get_select(self, username: str | None, status: int | None, ip: str | None) -> Select:
|
||||
"""
|
||||
获取操作日志列表查询表达式
|
||||
|
||||
:param username: 用户名
|
||||
:param status: 操作状态
|
||||
:param ip: IP 地址
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if username is not None:
|
||||
filters['username__like'] = f'%{username}%'
|
||||
if status is not None:
|
||||
filters['status__eq'] = status
|
||||
if ip is not None:
|
||||
filters['ip__like'] = f'%{ip}%'
|
||||
|
||||
return await self.select_order('created_time', 'desc', **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||
"""
|
||||
创建操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 操作日志创建参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def bulk_create(self, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
|
||||
"""
|
||||
批量创建操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param objs: 操作日志创建参数列表
|
||||
:return:
|
||||
"""
|
||||
await self.create_models(db, objs)
|
||||
|
||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||
"""
|
||||
批量删除操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pks: 操作日志 ID 列表
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||
|
||||
@staticmethod
|
||||
async def delete_all(db: AsyncSession) -> None:
|
||||
"""
|
||||
删除所有日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
await db.execute(sa_delete(OperaLog))
|
||||
|
||||
|
||||
opera_log_dao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog)
|
||||
@@ -0,0 +1,146 @@
|
||||
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 DataScope, Menu, Role
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
UpdateRoleMenuParam,
|
||||
UpdateRoleParam,
|
||||
UpdateRoleScopeParam,
|
||||
)
|
||||
|
||||
|
||||
class CRUDRole(CRUDPlus[Role]):
|
||||
"""角色数据库操作类"""
|
||||
|
||||
async def get(self, db: AsyncSession, role_id: int) -> Role | None:
|
||||
"""
|
||||
获取角色详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_id: 角色 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, role_id)
|
||||
|
||||
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
|
||||
"""
|
||||
获取角色及关联数据
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_id: 角色 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, role_id, load_strategies=['menus', 'scopes'])
|
||||
|
||||
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
|
||||
"""
|
||||
获取所有角色
|
||||
|
||||
: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={
|
||||
'users': 'noload',
|
||||
'menus': 'noload',
|
||||
'scopes': 'noload',
|
||||
},
|
||||
**filters,
|
||||
)
|
||||
|
||||
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
|
||||
"""
|
||||
通过名称获取角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param name: 角色名称
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, name=name)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateRoleParam) -> None:
|
||||
"""
|
||||
创建角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 创建角色参数
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
|
||||
async def update(self, db: AsyncSession, role_id: int, obj: UpdateRoleParam) -> int:
|
||||
"""
|
||||
更新角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_id: 角色 ID
|
||||
:param obj: 更新角色参数
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, role_id, obj)
|
||||
|
||||
async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||
"""
|
||||
更新角色菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_id: 角色 ID
|
||||
:param menu_ids: 菜单 ID 列表
|
||||
:return:
|
||||
"""
|
||||
current_role = await self.get_with_relation(db, role_id)
|
||||
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
|
||||
menus = await db.execute(stmt)
|
||||
current_role.menus = menus.scalars().all()
|
||||
return len(current_role.menus)
|
||||
|
||||
async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
|
||||
"""
|
||||
更新角色数据范围
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_id: 角色 ID
|
||||
:param scope_ids: 权限范围 ID 列表
|
||||
:return:
|
||||
"""
|
||||
current_role = await self.get_with_relation(db, role_id)
|
||||
stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
|
||||
scopes = await db.execute(stmt)
|
||||
current_role.scopes = scopes.scalars().all()
|
||||
return len(current_role.scopes)
|
||||
|
||||
async def delete(self, db: AsyncSession, role_ids: list[int]) -> int:
|
||||
"""
|
||||
批量删除角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_ids: 角色 ID 列表
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_ids)
|
||||
|
||||
|
||||
role_dao: CRUDRole = CRUDRole(Role)
|
||||
@@ -0,0 +1,289 @@
|
||||
import bcrypt
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload, selectinload
|
||||
from sqlalchemy.sql import Select
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Dept, Role, User
|
||||
from backend.app.admin.schema.user import (
|
||||
AddOAuth2UserParam,
|
||||
AddUserParam,
|
||||
UpdateUserParam,
|
||||
)
|
||||
from backend.common.security.jwt import get_hash_password
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class CRUDUser(CRUDPlus[User]):
|
||||
"""用户数据库操作类"""
|
||||
|
||||
async def get(self, db: AsyncSession, user_id: int) -> User | None:
|
||||
"""
|
||||
获取用户详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, user_id)
|
||||
|
||||
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
|
||||
"""
|
||||
通过用户名获取用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, username=username)
|
||||
|
||||
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
|
||||
"""
|
||||
通过昵称获取用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param nickname: 用户昵称
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, nickname=nickname)
|
||||
|
||||
async def update_login_time(self, db: AsyncSession, username: str) -> int:
|
||||
"""
|
||||
更新用户最后登录时间
|
||||
|
||||
:param db: 数据库会话
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
||||
|
||||
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
||||
"""
|
||||
添加用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 添加用户参数
|
||||
:return:
|
||||
"""
|
||||
salt = bcrypt.gensalt()
|
||||
obj.password = get_hash_password(obj.password, salt)
|
||||
dict_obj = obj.model_dump(exclude={'roles'})
|
||||
dict_obj.update({'salt': salt})
|
||||
new_user = self.model(**dict_obj)
|
||||
|
||||
stmt = select(Role).where(Role.id.in_(obj.roles))
|
||||
roles = await db.execute(stmt)
|
||||
new_user.roles = roles.scalars().all()
|
||||
|
||||
db.add(new_user)
|
||||
|
||||
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 input_user: 用户 ID
|
||||
:param obj: 更新用户参数
|
||||
:return:
|
||||
"""
|
||||
role_ids = obj.roles
|
||||
del obj.roles
|
||||
count = await self.update_model(db, input_user.id, obj)
|
||||
|
||||
stmt = select(Role).where(Role.id.in_(role_ids))
|
||||
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 user_id: 用户 ID
|
||||
:param nickname: 用户昵称
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'nickname': nickname})
|
||||
|
||||
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||
"""
|
||||
更新用户头像
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param avatar: 头像地址
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'avatar': avatar})
|
||||
|
||||
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
|
||||
"""
|
||||
更新用户邮箱
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param email: 邮箱
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'email': email})
|
||||
|
||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||
"""
|
||||
删除用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model(db, user_id)
|
||||
|
||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||
"""
|
||||
检查邮箱是否已被绑定
|
||||
|
||||
:param db: 数据库会话
|
||||
:param email: 电子邮箱
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, email=email)
|
||||
|
||||
async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:param password: 新密码
|
||||
:return:
|
||||
"""
|
||||
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_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
|
||||
"""
|
||||
获取用户列表查询表达式
|
||||
|
||||
:param dept: 部门 ID
|
||||
:param username: 用户名
|
||||
:param phone: 电话号码
|
||||
:param status: 用户状态
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if dept:
|
||||
filters['dept_id'] = dept
|
||||
if username:
|
||||
filters['username__like'] = f'%{username}%'
|
||||
if phone:
|
||||
filters['phone__like'] = f'%{phone}%'
|
||||
if status is not None:
|
||||
filters['status'] = status
|
||||
|
||||
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 user_id: 用户 ID
|
||||
:param is_super: 是否超级管理员
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'is_superuser': is_super})
|
||||
|
||||
async def set_staff(self, db: AsyncSession, user_id: int, *, is_staff: bool) -> int:
|
||||
"""
|
||||
设置用户后台登录状态
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param is_staff: 是否可登录后台
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'is_staff': is_staff})
|
||||
|
||||
async def set_status(self, db: AsyncSession, user_id: int, status: int) -> int:
|
||||
"""
|
||||
设置用户状态
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param status: 状态
|
||||
:return:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
设置用户多端登录状态
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param multi_login: 是否允许多端登录
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
||||
|
||||
async def get_with_relation(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
user_id: int | None = None,
|
||||
username: str | None = None,
|
||||
) -> User | None:
|
||||
"""
|
||||
获取用户关联信息
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 ID
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if user_id:
|
||||
filters['id'] = user_id
|
||||
if username:
|
||||
filters['username'] = username
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,8 @@
|
||||
from backend.app.admin.model.data_rule import DataRule as DataRule
|
||||
from backend.app.admin.model.data_scope import DataScope as DataScope
|
||||
from backend.app.admin.model.dept import Dept as Dept
|
||||
from backend.app.admin.model.login_log import LoginLog as LoginLog
|
||||
from backend.app.admin.model.menu import Menu as Menu
|
||||
from backend.app.admin.model.opera_log import OperaLog as OperaLog
|
||||
from backend.app.admin.model.role import Role as Role
|
||||
from backend.app.admin.model.user import User as User
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
from backend.common.model import Base, id_key
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import DataScope
|
||||
|
||||
|
||||
class DataRule(Base):
|
||||
"""数据规则表"""
|
||||
|
||||
__tablename__ = 'sys_data_rule'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
|
||||
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
|
||||
column: Mapped[str] = mapped_column(String(20), comment='模型字段名')
|
||||
operator: Mapped[int] = mapped_column(comment='运算符(0:and、1:or)')
|
||||
expression: Mapped[int] = mapped_column(
|
||||
comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)',
|
||||
)
|
||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||
|
||||
# 数据范围规则多对多
|
||||
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')
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import INTEGER
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.common.model import Base, id_key
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import User
|
||||
|
||||
|
||||
class Dept(Base):
|
||||
"""部门表"""
|
||||
|
||||
__tablename__ = 'sys_dept'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(50), comment='部门名称')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
leader: Mapped[str | None] = mapped_column(String(20), default=None, comment='负责人')
|
||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机')
|
||||
email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)')
|
||||
del_flag: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=False,
|
||||
comment='删除标志(0删除 1存在)',
|
||||
)
|
||||
|
||||
# 父级部门一对多
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger,
|
||||
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||
default=None,
|
||||
index=True,
|
||||
comment='父部门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')
|
||||
|
||||
# 部门用户一对多
|
||||
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
|
||||
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.app.models.base import DataClassBase, id_key
|
||||
from backend.app.utils.timezone import timezone
|
||||
from backend.common.model import DataClassBase, TimeZone, id_key
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class LoginLog(DataClassBase):
|
||||
@@ -27,6 +26,11 @@ class LoginLog(DataClassBase):
|
||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
||||
msg: Mapped[str] = mapped_column(LONGTEXT, comment='提示消息')
|
||||
login_time: Mapped[datetime] = mapped_column(comment='登录时间')
|
||||
created_time: Mapped[datetime] = mapped_column(init=False, default_factory=timezone.now, comment='创建时间')
|
||||
msg: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
||||
login_time: Mapped[datetime] = mapped_column(TimeZone, comment='登录时间')
|
||||
created_time: Mapped[datetime] = mapped_column(
|
||||
TimeZone,
|
||||
init=False,
|
||||
default_factory=timezone.now,
|
||||
comment='创建时间',
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
||||
|
||||
from backend.common.model import MappedBase
|
||||
|
||||
sys_user_role = Table(
|
||||
'sys_user_role',
|
||||
MappedBase.metadata,
|
||||
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column('user_id', BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
|
||||
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||
)
|
||||
|
||||
sys_role_menu = Table(
|
||||
'sys_role_menu',
|
||||
MappedBase.metadata,
|
||||
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||
Column('menu_id', BigInteger, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
|
||||
)
|
||||
|
||||
sys_role_data_scope = Table(
|
||||
'sys_role_data_scope',
|
||||
MappedBase.metadata,
|
||||
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
|
||||
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
|
||||
Column(
|
||||
'data_scope_id',
|
||||
BigInteger,
|
||||
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
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',
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BigInteger, ForeignKey, String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.m2m import sys_role_menu
|
||||
from backend.common.model import Base, id_key
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import Role
|
||||
|
||||
|
||||
class Menu(Base):
|
||||
"""菜单表"""
|
||||
|
||||
__tablename__ = 'sys_menu'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
|
||||
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
||||
path: Mapped[str | None] = mapped_column(String(200), comment='路由地址')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
|
||||
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
||||
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
||||
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
|
||||
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
|
||||
link: Mapped[str | None] = mapped_column(
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
default=None,
|
||||
comment='外链地址',
|
||||
)
|
||||
remark: Mapped[str | None] = mapped_column(
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
default=None,
|
||||
comment='备注',
|
||||
)
|
||||
|
||||
# 父级菜单一对多
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger,
|
||||
ForeignKey('sys_menu.id', ondelete='SET NULL'),
|
||||
default=None,
|
||||
index=True,
|
||||
comment='父菜单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')
|
||||
|
||||
# 菜单角色多对多
|
||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
|
||||
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from backend.app.models.base import DataClassBase, id_key
|
||||
from backend.app.utils.timezone import timezone
|
||||
from backend.common.model import DataClassBase, TimeZone, id_key
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class OperaLog(DataClassBase):
|
||||
@@ -16,6 +15,7 @@ class OperaLog(DataClassBase):
|
||||
__tablename__ = 'sys_opera_log'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
trace_id: Mapped[str] = mapped_column(String(32), comment='请求跟踪 ID')
|
||||
username: Mapped[str | None] = mapped_column(String(20), comment='用户名')
|
||||
method: Mapped[str] = mapped_column(String(20), comment='请求类型')
|
||||
title: Mapped[str] = mapped_column(String(255), comment='操作模块')
|
||||
@@ -24,14 +24,19 @@ class OperaLog(DataClassBase):
|
||||
country: Mapped[str | None] = mapped_column(String(50), comment='国家')
|
||||
region: Mapped[str | None] = mapped_column(String(50), comment='地区')
|
||||
city: Mapped[str | None] = mapped_column(String(50), comment='城市')
|
||||
user_agent: Mapped[str] = mapped_column(String(255), comment='请求头')
|
||||
user_agent: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='请求头')
|
||||
os: Mapped[str | None] = mapped_column(String(50), comment='操作系统')
|
||||
browser: Mapped[str | None] = mapped_column(String(50), comment='浏览器')
|
||||
device: Mapped[str | None] = mapped_column(String(50), comment='设备')
|
||||
args: Mapped[str | None] = mapped_column(JSON(), comment='请求参数')
|
||||
status: Mapped[int] = mapped_column(comment='操作状态(0异常 1正常)')
|
||||
code: Mapped[str] = mapped_column(String(20), insert_default='200', comment='操作状态码')
|
||||
msg: Mapped[str | None] = mapped_column(LONGTEXT, comment='提示消息')
|
||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时ms')
|
||||
opera_time: Mapped[datetime] = mapped_column(comment='操作时间')
|
||||
created_time: Mapped[datetime] = mapped_column(init=False, default_factory=timezone.now, comment='创建时间')
|
||||
msg: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='提示消息')
|
||||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时(ms)')
|
||||
opera_time: Mapped[datetime] = mapped_column(TimeZone, comment='操作时间')
|
||||
created_time: Mapped[datetime] = mapped_column(
|
||||
TimeZone,
|
||||
init=False,
|
||||
default_factory=timezone.now,
|
||||
comment='创建时间',
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, String
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import INTEGER, TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
|
||||
from backend.common.model import Base, id_key
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import DataScope, Menu, User
|
||||
|
||||
|
||||
class Role(Base):
|
||||
"""角色表"""
|
||||
|
||||
__tablename__ = 'sys_role'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
||||
is_filter_scopes: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=True,
|
||||
comment='过滤数据权限(0否 1是)',
|
||||
)
|
||||
remark: Mapped[str | None] = mapped_column(
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'),
|
||||
default=None,
|
||||
comment='备注',
|
||||
)
|
||||
|
||||
# 角色用户多对多
|
||||
users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles')
|
||||
|
||||
# 角色菜单多对多
|
||||
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
|
||||
|
||||
# 角色数据范围多对多
|
||||
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='roles')
|
||||
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import VARBINARY, Boolean, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.m2m import sys_user_role
|
||||
from backend.common.model import Base, TimeZone, id_key
|
||||
from backend.database.db import uuid4_str
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import Dept, Role
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户表"""
|
||||
|
||||
__tablename__ = 'sys_user'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
||||
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
||||
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
||||
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
||||
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
||||
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
|
||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||
is_superuser: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=False,
|
||||
comment='超级权限(0否 1是)',
|
||||
)
|
||||
is_staff: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=False,
|
||||
comment='后台管理登陆(0否 1是)',
|
||||
)
|
||||
is_multi_login: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'),
|
||||
default=False,
|
||||
comment='是否重复登陆(0否 1是)',
|
||||
)
|
||||
join_time: Mapped[datetime] = mapped_column(TimeZone, init=False, default_factory=timezone.now, comment='注册时间')
|
||||
last_login_time: Mapped[datetime | None] = mapped_column(
|
||||
TimeZone,
|
||||
init=False,
|
||||
onupdate=timezone.now,
|
||||
comment='上次登录',
|
||||
)
|
||||
|
||||
# 部门用户一对多
|
||||
dept_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_dept.id', ondelete='SET NULL'),
|
||||
default=None,
|
||||
comment='部门关联ID',
|
||||
)
|
||||
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||
|
||||
# 用户角色多对多
|
||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
||||
@@ -0,0 +1,11 @@
|
||||
from pydantic import Field
|
||||
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class GetCaptchaDetail(SchemaBase):
|
||||
"""验证码详情"""
|
||||
|
||||
uuid: str = Field(description='图片唯一标识')
|
||||
img_type: str = Field(description='图片类型')
|
||||
image: str = Field(description='图片内容')
|
||||
@@ -0,0 +1,48 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperatorType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class DataRuleSchemaBase(SchemaBase):
|
||||
"""数据规则基础模型"""
|
||||
|
||||
name: str = Field(description='规则名称')
|
||||
model: str = Field(description='模型名称')
|
||||
column: str = Field(description='字段名称')
|
||||
operator: RoleDataRuleOperatorType = Field(description='操作符(AND/OR)')
|
||||
expression: RoleDataRuleExpressionType = Field(description='表达式类型')
|
||||
value: str = Field(description='规则值')
|
||||
|
||||
|
||||
class CreateDataRuleParam(DataRuleSchemaBase):
|
||||
"""创建数据规则参数"""
|
||||
|
||||
|
||||
class UpdateDataRuleParam(DataRuleSchemaBase):
|
||||
"""更新数据规则参数"""
|
||||
|
||||
|
||||
class DeleteDataRuleParam(SchemaBase):
|
||||
"""删除数据规则参数"""
|
||||
|
||||
pks: list[int] = Field(description='规则 ID 列表')
|
||||
|
||||
|
||||
class GetDataRuleDetail(DataRuleSchemaBase):
|
||||
"""数据规则详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='规则 ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
|
||||
|
||||
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='数据规则列表')
|
||||
@@ -0,0 +1,43 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
||||
|
||||
|
||||
class DeptSchemaBase(SchemaBase):
|
||||
"""部门基础模型"""
|
||||
|
||||
name: str = Field(description='部门名称')
|
||||
parent_id: int | None = Field(None, description='部门父级 ID')
|
||||
sort: int = Field(0, ge=0, description='排序')
|
||||
leader: str | None = Field(None, description='负责人')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
status: StatusType = Field(description='状态')
|
||||
|
||||
|
||||
class CreateDeptParam(DeptSchemaBase):
|
||||
"""创建部门参数"""
|
||||
|
||||
|
||||
class UpdateDeptParam(DeptSchemaBase):
|
||||
"""更新部门参数"""
|
||||
|
||||
|
||||
class GetDeptDetail(DeptSchemaBase):
|
||||
"""部门详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='部门 ID')
|
||||
del_flag: bool = Field(description='是否删除')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
|
||||
|
||||
class GetDeptTree(GetDeptDetail):
|
||||
"""获取部门树"""
|
||||
|
||||
children: list['GetDeptTree'] | None = Field(None, description='子菜单')
|
||||
@@ -0,0 +1,46 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class LoginLogSchemaBase(SchemaBase):
|
||||
"""登录日志基础模型"""
|
||||
|
||||
user_uuid: str = Field(description='用户 UUID')
|
||||
username: str = Field(description='用户名')
|
||||
status: int = Field(description='登录状态')
|
||||
ip: str = Field(description='IP 地址')
|
||||
country: str | None = Field(None, description='国家')
|
||||
region: str | None = Field(None, description='地区')
|
||||
city: str | None = Field(None, description='城市')
|
||||
user_agent: str = Field(description='用户代理')
|
||||
browser: str | None = Field(None, description='浏览器')
|
||||
os: str | None = Field(None, description='操作系统')
|
||||
device: str | None = Field(None, description='设备')
|
||||
msg: str = Field(description='消息')
|
||||
login_time: datetime = Field(description='登录时间')
|
||||
|
||||
|
||||
class CreateLoginLogParam(LoginLogSchemaBase):
|
||||
"""创建登录日志参数"""
|
||||
|
||||
|
||||
class UpdateLoginLogParam(LoginLogSchemaBase):
|
||||
"""更新登录日志参数"""
|
||||
|
||||
|
||||
class DeleteLoginLogParam(SchemaBase):
|
||||
"""删除登录日志参数"""
|
||||
|
||||
pks: list[int] = Field(description='登录日志 ID 列表')
|
||||
|
||||
|
||||
class GetLoginLogDetail(LoginLogSchemaBase):
|
||||
"""登录日志详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='日志 ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
@@ -0,0 +1,49 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.enums import MenuType, StatusType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class MenuSchemaBase(SchemaBase):
|
||||
"""菜单基础模型"""
|
||||
|
||||
title: str = Field(description='菜单标题')
|
||||
name: str = Field(description='菜单名称')
|
||||
path: str | None = Field(None, description='路由地址')
|
||||
parent_id: int | None = Field(None, description='菜单父级 ID')
|
||||
sort: int = Field(0, ge=0, description='排序')
|
||||
icon: str | None = Field(None, description='图标')
|
||||
type: MenuType = Field(description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||
component: str | None = Field(None, description='组件路径')
|
||||
perms: str | None = Field(None, description='权限标识')
|
||||
status: StatusType = Field(description='状态')
|
||||
display: StatusType = Field(description='是否显示')
|
||||
cache: StatusType = Field(description='是否缓存')
|
||||
link: str | None = Field(None, description='外链地址')
|
||||
remark: str | None = Field(None, description='备注')
|
||||
|
||||
|
||||
class CreateMenuParam(MenuSchemaBase):
|
||||
"""创建菜单参数"""
|
||||
|
||||
|
||||
class UpdateMenuParam(MenuSchemaBase):
|
||||
"""更新菜单参数"""
|
||||
|
||||
|
||||
class GetMenuDetail(MenuSchemaBase):
|
||||
"""菜单详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='菜单 ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
|
||||
|
||||
class GetMenuTree(GetMenuDetail):
|
||||
"""获取菜单树"""
|
||||
|
||||
children: list['GetMenuTree'] | None = Field(None, description='子菜单')
|
||||
@@ -0,0 +1,54 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class OperaLogSchemaBase(SchemaBase):
|
||||
"""操作日志基础模型"""
|
||||
|
||||
trace_id: str = Field(description='追踪 ID')
|
||||
username: str | None = Field(None, description='用户名')
|
||||
method: str = Field(description='请求方法')
|
||||
title: str = Field(description='操作标题')
|
||||
path: str = Field(description='请求路径')
|
||||
ip: str = Field(description='IP 地址')
|
||||
country: str | None = Field(None, description='国家')
|
||||
region: str | None = Field(None, description='地区')
|
||||
city: str | None = Field(None, description='城市')
|
||||
user_agent: str = Field(description='用户代理')
|
||||
os: str | None = Field(None, description='操作系统')
|
||||
browser: str | None = Field(None, description='浏览器')
|
||||
device: str | None = Field(None, description='设备')
|
||||
args: dict[str, Any] | None = Field(None, description='请求参数')
|
||||
status: StatusType = Field(description='状态')
|
||||
code: str = Field(description='状态码')
|
||||
msg: str | None = Field(None, description='消息')
|
||||
cost_time: float = Field(description='耗时')
|
||||
opera_time: datetime = Field(description='操作时间')
|
||||
|
||||
|
||||
class CreateOperaLogParam(OperaLogSchemaBase):
|
||||
"""创建操作日志参数"""
|
||||
|
||||
|
||||
class UpdateOperaLogParam(OperaLogSchemaBase):
|
||||
"""更新操作日志参数"""
|
||||
|
||||
|
||||
class DeleteOperaLogParam(SchemaBase):
|
||||
"""删除操作日志参数"""
|
||||
|
||||
pks: list[int] = Field(description='操作日志 ID 列表')
|
||||
|
||||
|
||||
class GetOperaLogDetail(OperaLogSchemaBase):
|
||||
"""操作日志详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='日志 ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
@@ -0,0 +1,60 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from backend.app.admin.schema.data_scope import GetDataScopeDetail
|
||||
from backend.app.admin.schema.menu import GetMenuDetail
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class RoleSchemaBase(SchemaBase):
|
||||
"""角色基础模型"""
|
||||
|
||||
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 UpdateRoleParam(RoleSchemaBase):
|
||||
"""更新角色参数"""
|
||||
|
||||
|
||||
class DeleteRoleParam(SchemaBase):
|
||||
"""删除角色参数"""
|
||||
|
||||
pks: list[int] = Field(description='角色 ID 列表')
|
||||
|
||||
|
||||
class UpdateRoleMenuParam(SchemaBase):
|
||||
"""更新角色菜单参数"""
|
||||
|
||||
menus: list[int] = Field(description='菜单 ID 列表')
|
||||
|
||||
|
||||
class UpdateRoleScopeParam(SchemaBase):
|
||||
"""更新角色数据范围参数"""
|
||||
|
||||
scopes: list[int] = Field(description='数据范围 ID 列表')
|
||||
|
||||
|
||||
class GetRoleDetail(RoleSchemaBase):
|
||||
"""角色详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='角色 ID')
|
||||
created_time: datetime = Field(description='创建时间')
|
||||
updated_time: datetime | None = Field(None, description='更新时间')
|
||||
|
||||
|
||||
class GetRoleWithRelationDetail(GetRoleDetail):
|
||||
"""角色关联详情"""
|
||||
|
||||
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
|
||||
scopes: list[GetDataScopeDetail | None] = Field([], description='数据范围列表')
|
||||
@@ -0,0 +1,49 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from backend.app.admin.schema.user import GetUserInfoDetail
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class GetSwaggerToken(SchemaBase):
|
||||
"""Swagger 认证令牌"""
|
||||
|
||||
access_token: str = Field(description='访问令牌')
|
||||
token_type: str = Field('Bearer', description='令牌类型')
|
||||
user: GetUserInfoDetail = Field(description='用户信息')
|
||||
|
||||
|
||||
class AccessTokenBase(SchemaBase):
|
||||
"""访问令牌基础模型"""
|
||||
|
||||
access_token: str = Field(description='访问令牌')
|
||||
access_token_expire_time: datetime = Field(description='令牌过期时间')
|
||||
session_uuid: str = Field(description='会话 UUID')
|
||||
|
||||
|
||||
class GetNewToken(AccessTokenBase):
|
||||
"""获取新令牌"""
|
||||
|
||||
|
||||
class GetLoginToken(AccessTokenBase):
|
||||
"""获取登录令牌"""
|
||||
|
||||
user: GetUserInfoDetail = Field(description='用户信息')
|
||||
|
||||
|
||||
class GetTokenDetail(SchemaBase):
|
||||
"""令牌详情"""
|
||||
|
||||
id: int = Field(description='用户 ID')
|
||||
session_uuid: str = Field(description='会话 UUID')
|
||||
username: str = Field(description='用户名')
|
||||
nickname: str = Field(description='昵称')
|
||||
ip: str = Field(description='IP 地址')
|
||||
os: str = Field(description='操作系统')
|
||||
browser: str = Field(description='浏览器')
|
||||
device: str = Field(description='设备')
|
||||
status: StatusType = Field(description='状态')
|
||||
last_login_time: str = Field(description='最后登录时间')
|
||||
expire_time: datetime = Field(description='过期时间')
|
||||
@@ -0,0 +1,114 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ConfigDict, Field, HttpUrl, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from backend.app.admin.schema.dept import GetDeptDetail
|
||||
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
||||
|
||||
|
||||
class AuthSchemaBase(SchemaBase):
|
||||
"""用户认证基础模型"""
|
||||
|
||||
username: str = Field(description='用户名')
|
||||
password: str = Field(description='密码')
|
||||
|
||||
|
||||
class AuthLoginParam(AuthSchemaBase):
|
||||
"""用户登录参数"""
|
||||
|
||||
uuid: str = Field(description='验证码 UUID')
|
||||
captcha: str = Field(description='验证码')
|
||||
|
||||
|
||||
class AddUserParam(AuthSchemaBase):
|
||||
"""添加用户参数"""
|
||||
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号码')
|
||||
dept_id: int = Field(description='部门 ID')
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
|
||||
|
||||
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):
|
||||
"""用户信息基础模型"""
|
||||
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
username: str = Field(description='用户名')
|
||||
nickname: str = Field(description='昵称')
|
||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||
|
||||
|
||||
class UpdateUserParam(UserInfoSchemaBase):
|
||||
"""更新用户参数"""
|
||||
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
|
||||
|
||||
class GetUserInfoDetail(UserInfoSchemaBase):
|
||||
"""用户信息详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
id: int = Field(description='用户 ID')
|
||||
uuid: str = Field(description='用户 UUID')
|
||||
status: StatusType = Field(description='状态')
|
||||
is_superuser: bool = Field(description='是否超级管理员')
|
||||
is_staff: bool = Field(description='是否管理员')
|
||||
is_multi_login: bool = Field(description='是否允许多端登录')
|
||||
join_time: datetime = Field(description='加入时间')
|
||||
last_login_time: datetime | None = Field(None, description='最后登录时间')
|
||||
|
||||
|
||||
class GetUserInfoWithRelationDetail(GetUserInfoDetail):
|
||||
"""用户信息关联详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
dept: GetDeptDetail | None = Field(None, description='部门信息')
|
||||
roles: list[GetRoleWithRelationDetail] = Field(description='角色列表')
|
||||
|
||||
|
||||
class GetCurrentUserInfoWithRelationDetail(GetUserInfoWithRelationDetail):
|
||||
"""当前用户信息关联详情"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
dept: str | None = Field(None, description='部门名称')
|
||||
roles: list[str] = Field(description='角色名称列表')
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def handel(cls, data: Any) -> Self:
|
||||
"""处理部门和角色数据"""
|
||||
dept = data['dept']
|
||||
if dept:
|
||||
data['dept'] = dept['name']
|
||||
roles = data['roles']
|
||||
if roles:
|
||||
data['roles'] = [role['name'] for role in roles]
|
||||
return data
|
||||
@@ -0,0 +1,260 @@
|
||||
from fastapi import Request, Response
|
||||
from fastapi.security import HTTPBasicCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.background import BackgroundTask, BackgroundTasks
|
||||
|
||||
from backend.app.admin.crud.crud_menu import menu_dao
|
||||
from backend.app.admin.crud.crud_user import user_dao
|
||||
from backend.app.admin.model import User
|
||||
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
|
||||
from backend.app.admin.schema.user import AuthLoginParam
|
||||
from backend.app.admin.service.login_log_service import login_log_service
|
||||
from backend.common.context import ctx
|
||||
from backend.common.enums import LoginLogStatusType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.i18n import t
|
||||
from backend.common.log import log
|
||||
from backend.common.response.response_code import CustomErrorCode
|
||||
from backend.common.security.jwt import (
|
||||
create_access_token,
|
||||
create_new_token,
|
||||
create_refresh_token,
|
||||
get_token,
|
||||
jwt_decode,
|
||||
password_verify,
|
||||
)
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import uuid4_str
|
||||
from backend.database.redis import redis_client
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""认证服务类"""
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户名或密码有误')
|
||||
|
||||
if user.password is None:
|
||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||
if not password_verify(password, user.password):
|
||||
raise errors.AuthorizationError(msg='用户名或密码有误')
|
||||
|
||||
if not user.status:
|
||||
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
|
||||
|
||||
return user
|
||||
|
||||
async def swagger_login(self, *, db: AsyncSession, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
||||
"""
|
||||
Swagger 文档登录
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 登录凭证
|
||||
:return:
|
||||
"""
|
||||
user = await self.user_verify(db, obj.username, obj.password)
|
||||
await user_dao.update_login_time(db, obj.username)
|
||||
access_token = await create_access_token(
|
||||
user.id,
|
||||
multi_login=user.is_multi_login,
|
||||
# extra info
|
||||
swagger=True,
|
||||
)
|
||||
return access_token.access_token, user
|
||||
|
||||
async def login(
|
||||
self,
|
||||
*,
|
||||
db: AsyncSession,
|
||||
response: Response,
|
||||
obj: AuthLoginParam,
|
||||
background_tasks: BackgroundTasks,
|
||||
) -> GetLoginToken:
|
||||
"""
|
||||
用户登录
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: 请求对象
|
||||
:param response: 响应对象
|
||||
:param obj: 登录参数
|
||||
:param background_tasks: 后台任务
|
||||
:return:
|
||||
"""
|
||||
user = None
|
||||
try:
|
||||
user = await self.user_verify(db, obj.username, obj.password)
|
||||
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{obj.uuid}')
|
||||
if not captcha_code:
|
||||
raise errors.RequestError(msg=t('error.captcha.expired'))
|
||||
if captcha_code.lower() != obj.captcha.lower():
|
||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{obj.uuid}')
|
||||
await user_dao.update_login_time(db, obj.username)
|
||||
await db.refresh(user)
|
||||
access_token = await create_access_token(
|
||||
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=ctx.device,
|
||||
)
|
||||
refresh_token = await create_refresh_token(
|
||||
access_token.session_uuid,
|
||||
user.id,
|
||||
multi_login=user.is_multi_login,
|
||||
)
|
||||
response.set_cookie(
|
||||
key=settings.COOKIE_REFRESH_TOKEN_KEY,
|
||||
value=refresh_token.refresh_token,
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
async def get_codes(*, db: AsyncSession, request: Request) -> list[str]:
|
||||
"""
|
||||
获取用户权限码
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
codes = set()
|
||||
if request.user.is_superuser:
|
||||
menus = await menu_dao.get_all(db, None, None)
|
||||
for menu in menus:
|
||||
if menu.perms:
|
||||
codes.add(*menu.perms.split(','))
|
||||
else:
|
||||
roles = request.user.roles
|
||||
if roles:
|
||||
for role in roles:
|
||||
for menu in role.menus:
|
||||
if menu.perms:
|
||||
codes.add(*menu.perms.split(','))
|
||||
|
||||
return list(codes)
|
||||
|
||||
@staticmethod
|
||||
async def refresh_token(*, db: AsyncSession, request: Request) -> GetNewToken:
|
||||
"""
|
||||
刷新令牌
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
: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()
|
||||
@@ -0,0 +1,131 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.admin.crud.crud_data_rule import data_rule_dao
|
||||
from backend.app.admin.model import DataRule
|
||||
from backend.app.admin.schema.data_rule import (
|
||||
CreateDataRuleParam,
|
||||
DeleteDataRuleParam,
|
||||
GetDataRuleColumnDetail,
|
||||
UpdateDataRuleParam,
|
||||
)
|
||||
from backend.common.exception import errors
|
||||
from backend.common.pagination import paging_data
|
||||
from backend.core.conf import settings
|
||||
from backend.utils.import_parse import dynamic_import_data_model
|
||||
|
||||
|
||||
class DataRuleService:
|
||||
"""数据规则服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get(*, db: AsyncSession, pk: int) -> DataRule:
|
||||
"""
|
||||
获取数据规则详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 规则 ID
|
||||
:return:
|
||||
"""
|
||||
|
||||
data_rule = await data_rule_dao.get(db, pk)
|
||||
if not data_rule:
|
||||
raise errors.NotFoundError(msg='数据规则不存在')
|
||||
return data_rule
|
||||
|
||||
@staticmethod
|
||||
async def get_models() -> list[str]:
|
||||
"""获取所有数据规则可用模型"""
|
||||
return list(settings.DATA_PERMISSION_MODELS.keys())
|
||||
|
||||
@staticmethod
|
||||
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
|
||||
"""
|
||||
获取数据规则可用模型的字段列表
|
||||
|
||||
:param model: 模型名称
|
||||
:return:
|
||||
"""
|
||||
if model not in settings.DATA_PERMISSION_MODELS:
|
||||
raise errors.NotFoundError(msg='数据规则可用模型不存在')
|
||||
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
|
||||
|
||||
model_columns = [
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
async def get_list(*, db: AsyncSession, name: str | None) -> dict[str, Any]:
|
||||
"""
|
||||
获取数据规则列表
|
||||
|
||||
: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
|
||||
async def get_all(*, db: AsyncSession) -> Sequence[DataRule]:
|
||||
"""
|
||||
获取所有数据规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
|
||||
data_rules = await data_rule_dao.get_all(db)
|
||||
return data_rules
|
||||
|
||||
@staticmethod
|
||||
async def create(*, db: AsyncSession, obj: CreateDataRuleParam) -> None:
|
||||
"""
|
||||
创建数据规则
|
||||
|
||||
:param db: 数据库会话
|
||||
: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
|
||||
async def update(*, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||
"""
|
||||
更新数据规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 规则 ID
|
||||
: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
|
||||
async def delete(*, db: AsyncSession, obj: DeleteDataRuleParam) -> int:
|
||||
"""
|
||||
批量删除数据规则
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 规则 ID 列表
|
||||
:return:
|
||||
"""
|
||||
count = await data_rule_dao.delete(db, obj.pks)
|
||||
return count
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,122 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.admin.crud.crud_dept import dept_dao
|
||||
from backend.app.admin.model import Dept
|
||||
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
||||
from backend.common.exception import errors
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
from backend.utils.build_tree import get_tree_data
|
||||
|
||||
|
||||
class DeptService:
|
||||
"""部门服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get(*, db: AsyncSession, pk: int) -> Dept:
|
||||
"""
|
||||
获取部门详情
|
||||
|
||||
: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]]:
|
||||
"""
|
||||
获取部门树形结构
|
||||
|
||||
: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
|
||||
async def create(*, db: AsyncSession, obj: CreateDeptParam) -> None:
|
||||
"""
|
||||
创建部门
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 部门创建参数
|
||||
:return:
|
||||
"""
|
||||
dept = await dept_dao.get_by_name(db, obj.name)
|
||||
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
|
||||
async def update(*, db: AsyncSession, pk: int, obj: UpdateDeptParam) -> int:
|
||||
"""
|
||||
更新部门
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 部门 ID
|
||||
:param obj: 部门更新参数
|
||||
:return:
|
||||
"""
|
||||
dept = await dept_dao.get(db, pk)
|
||||
if not dept:
|
||||
raise errors.NotFoundError(msg='部门不存在')
|
||||
if dept.name != obj.name and await dept_dao.get_by_name(db, obj.name):
|
||||
raise errors.ConflictError(msg='部门名称已存在')
|
||||
if obj.parent_id:
|
||||
parent_dept = await dept_dao.get(db, obj.parent_id)
|
||||
if not parent_dept:
|
||||
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
|
||||
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||
"""
|
||||
删除部门
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
dept = await dept_dao.get_with_relation(db, pk)
|
||||
if dept.users:
|
||||
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()
|
||||
@@ -0,0 +1,89 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.admin.crud.crud_login_log import login_log_dao
|
||||
from backend.app.admin.schema.login_log import CreateLoginLogParam, DeleteLoginLogParam
|
||||
from backend.common.context import ctx
|
||||
from backend.common.log import log
|
||||
from backend.common.pagination import paging_data
|
||||
|
||||
|
||||
class LoginLogService:
|
||||
"""登录日志服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
|
||||
"""
|
||||
获取登录日志列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param username: 用户名
|
||||
:param status: 状态
|
||||
:param ip: IP 地址
|
||||
:return:
|
||||
"""
|
||||
log_select = await login_log_dao.get_select(username=username, status=status, ip=ip)
|
||||
return await paging_data(db, log_select)
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
user_uuid: str,
|
||||
username: str,
|
||||
login_time: datetime,
|
||||
status: int,
|
||||
msg: str,
|
||||
) -> None:
|
||||
"""
|
||||
创建登录日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_uuid: 用户 UUID
|
||||
:param username: 用户名
|
||||
:param login_time: 登录时间
|
||||
:param status: 状态
|
||||
:param msg: 消息
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
obj = CreateLoginLogParam(
|
||||
user_uuid=user_uuid,
|
||||
username=username,
|
||||
status=status,
|
||||
ip=ctx.ip,
|
||||
country=ctx.country,
|
||||
region=ctx.region,
|
||||
city=ctx.city,
|
||||
user_agent=ctx.user_agent,
|
||||
browser=ctx.browser,
|
||||
os=ctx.os,
|
||||
device=ctx.device,
|
||||
msg=msg,
|
||||
login_time=login_time,
|
||||
)
|
||||
await login_log_dao.create(db, obj)
|
||||
except Exception as e:
|
||||
log.error(f'登录日志创建失败: {e}')
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, db: AsyncSession, obj: DeleteLoginLogParam) -> int:
|
||||
"""
|
||||
批量删除登录日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 日志 ID 列表
|
||||
:return:
|
||||
"""
|
||||
count = await login_log_dao.delete(db, obj.pks)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete_all(*, db: AsyncSession) -> None:
|
||||
"""清空所有登录日志"""
|
||||
await login_log_dao.delete_all(db)
|
||||
|
||||
|
||||
login_log_service: LoginLogService = LoginLogService()
|
||||
@@ -0,0 +1,139 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.admin.crud.crud_menu import menu_dao
|
||||
from backend.app.admin.model import Menu
|
||||
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
||||
from backend.common.exception import errors
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
|
||||
|
||||
|
||||
class MenuService:
|
||||
"""菜单服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get(*, db: AsyncSession, pk: int) -> Menu:
|
||||
"""
|
||||
获取菜单详情
|
||||
|
||||
: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
|
||||
async def get_tree(*, db: AsyncSession, title: str | None, status: int | None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取菜单树形结构
|
||||
|
||||
:param db: 数据库会话
|
||||
:param title: 菜单标题
|
||||
:param status: 状态
|
||||
:return:
|
||||
"""
|
||||
|
||||
menu_data = await menu_dao.get_all(db, title=title, status=status)
|
||||
menu_tree = get_tree_data(menu_data)
|
||||
return menu_tree
|
||||
|
||||
@staticmethod
|
||||
async def get_sidebar(*, db: AsyncSession, request: Request) -> list[dict[str, Any] | None]:
|
||||
"""
|
||||
获取用户的菜单侧边栏
|
||||
|
||||
: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
|
||||
menu_ids = set()
|
||||
if roles:
|
||||
for role in roles:
|
||||
menu_ids.update(menu.id for menu in role.menus)
|
||||
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
|
||||
menu_tree = get_vben5_tree_data(menu_data)
|
||||
return menu_tree
|
||||
|
||||
@staticmethod
|
||||
async def create(*, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||
"""
|
||||
创建菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 菜单创建参数
|
||||
:return:
|
||||
"""
|
||||
|
||||
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
|
||||
async def update(*, db: AsyncSession, pk: int, obj: UpdateMenuParam) -> int:
|
||||
"""
|
||||
更新菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 菜单 ID
|
||||
:param obj: 菜单更新参数
|
||||
:return:
|
||||
"""
|
||||
|
||||
menu = await menu_dao.get(db, pk)
|
||||
if not menu:
|
||||
raise errors.NotFoundError(msg='菜单不存在')
|
||||
if menu.title != obj.title and await menu_dao.get_by_title(db, obj.title):
|
||||
raise errors.ConflictError(msg='菜单标题已存在')
|
||||
if obj.parent_id:
|
||||
parent_menu = await menu_dao.get(db, obj.parent_id)
|
||||
if not parent_menu:
|
||||
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
|
||||
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||
"""
|
||||
删除菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 菜单 ID
|
||||
:return:
|
||||
"""
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,72 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.admin.crud.crud_opera_log import opera_log_dao
|
||||
from backend.app.admin.schema.opera_log import CreateOperaLogParam, DeleteOperaLogParam
|
||||
from backend.common.pagination import paging_data
|
||||
|
||||
|
||||
class OperaLogService:
|
||||
"""操作日志服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_list(*, db: AsyncSession, username: str | None, status: int | None, ip: str | None) -> dict[str, Any]:
|
||||
"""
|
||||
获取操作日志列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param username: 用户名
|
||||
:param status: 状态
|
||||
:param ip: IP 地址
|
||||
:return:
|
||||
"""
|
||||
log_select = await opera_log_dao.get_select(username=username, status=status, ip=ip)
|
||||
return await paging_data(db, log_select)
|
||||
|
||||
@staticmethod
|
||||
async def create(*, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||
"""
|
||||
创建操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 操作日志创建参数
|
||||
:return:
|
||||
"""
|
||||
await opera_log_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def bulk_create(*, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
|
||||
"""
|
||||
批量创建操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param objs: 操作日志创建参数列表
|
||||
:return:
|
||||
"""
|
||||
await opera_log_dao.bulk_create(db, objs)
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, db: AsyncSession, obj: DeleteOperaLogParam) -> int:
|
||||
"""
|
||||
批量删除操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 日志 ID 列表
|
||||
:return:
|
||||
"""
|
||||
count = await opera_log_dao.delete(db, obj.pks)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete_all(*, db: AsyncSession) -> None:
|
||||
"""
|
||||
清空所有操作日志
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
await opera_log_dao.delete_all(db)
|
||||
|
||||
|
||||
opera_log_service: OperaLogService = OperaLogService()
|
||||
@@ -0,0 +1,123 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from backend.common.enums import PluginType, StatusType
|
||||
from backend.common.exception import errors
|
||||
from backend.core.conf import settings
|
||||
from backend.core.path_conf import PLUGIN_DIR
|
||||
from backend.database.redis import redis_client
|
||||
from backend.plugin.tools import uninstall_requirements_async
|
||||
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class PluginService:
|
||||
"""插件服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> list[dict[str, Any]]:
|
||||
"""获取所有插件"""
|
||||
|
||||
keys = [key async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*')]
|
||||
|
||||
result = [json.loads(info) for info in await redis_client.mget(*keys)]
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def changed() -> str | None:
|
||||
"""检查插件是否发生变更"""
|
||||
return await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||
|
||||
@staticmethod
|
||||
async def install(*, type: PluginType, file: UploadFile | None = None, repo_url: str | None = None) -> str:
|
||||
"""
|
||||
安装插件
|
||||
|
||||
:param type: 插件类型
|
||||
:param file: 插件 zip 压缩包
|
||||
:param repo_url: git 仓库地址
|
||||
:return:
|
||||
"""
|
||||
if type == PluginType.zip:
|
||||
if not file:
|
||||
raise errors.RequestError(msg='ZIP 压缩包不能为空')
|
||||
return await install_zip_plugin(file)
|
||||
if not repo_url:
|
||||
raise errors.RequestError(msg='Git 仓库地址不能为空')
|
||||
return await install_git_plugin(repo_url)
|
||||
|
||||
@staticmethod
|
||||
async def uninstall(*, plugin: str) -> None:
|
||||
"""
|
||||
卸载插件
|
||||
|
||||
:param plugin: 插件名称
|
||||
:return:
|
||||
"""
|
||||
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
|
||||
if not await plugin_dir.exists():
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
await uninstall_requirements_async(plugin)
|
||||
bacup_dir = PLUGIN_DIR / f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup'
|
||||
shutil.move(plugin_dir, bacup_dir)
|
||||
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||
|
||||
@staticmethod
|
||||
async def update_status(*, plugin: str) -> None:
|
||||
"""
|
||||
更新插件状态
|
||||
|
||||
:param plugin: 插件名称
|
||||
:return:
|
||||
"""
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||
if not plugin_info:
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
plugin_info = json.loads(plugin_info)
|
||||
|
||||
# 更新持久缓存状态
|
||||
new_status = (
|
||||
str(StatusType.enable.value)
|
||||
if plugin_info['plugin']['enable'] == str(StatusType.disable.value)
|
||||
else str(StatusType.disable.value)
|
||||
)
|
||||
plugin_info['plugin']['enable'] = new_status
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(plugin_info, ensure_ascii=False))
|
||||
|
||||
@staticmethod
|
||||
async def build(*, plugin: str) -> io.BytesIO:
|
||||
"""
|
||||
打包插件为 zip 压缩包
|
||||
|
||||
:param plugin: 插件名称
|
||||
:return:
|
||||
"""
|
||||
plugin_dir = anyio.Path(PLUGIN_DIR / plugin)
|
||||
if not await plugin_dir.exists():
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
|
||||
bio = io.BytesIO()
|
||||
with zipfile.ZipFile(bio, 'w') as zf:
|
||||
for root, dirs, files in os.walk(plugin_dir):
|
||||
dirs[:] = [d for d in dirs if d != '__pycache__']
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, start=plugin_dir) # noqa: ASYNC240
|
||||
zf.write(file_path, os.path.join(plugin, arcname))
|
||||
|
||||
bio.seek(0)
|
||||
return bio
|
||||
|
||||
|
||||
plugin_service: PluginService = PluginService()
|
||||
@@ -0,0 +1,200 @@
|
||||
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.crud.crud_menu import menu_dao
|
||||
from backend.app.admin.crud.crud_role import role_dao
|
||||
from backend.app.admin.model import Role
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
DeleteRoleParam,
|
||||
UpdateRoleMenuParam,
|
||||
UpdateRoleParam,
|
||||
UpdateRoleScopeParam,
|
||||
)
|
||||
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
|
||||
from backend.utils.build_tree import get_tree_data
|
||||
|
||||
|
||||
class RoleService:
|
||||
"""角色服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get(*, db: AsyncSession, pk: int) -> Role:
|
||||
"""
|
||||
获取角色详情
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 角色 ID
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get_with_relation(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
return role
|
||||
|
||||
@staticmethod
|
||||
async def get_all(*, db: AsyncSession) -> Sequence[Role]:
|
||||
"""
|
||||
获取所有角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
|
||||
roles = await role_dao.get_all(db)
|
||||
return roles
|
||||
|
||||
@staticmethod
|
||||
async def get_list(*, db: AsyncSession, name: str | None, status: int | None) -> dict[str, Any]:
|
||||
"""
|
||||
获取角色列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param name: 角色名称
|
||||
:param status: 状态
|
||||
:return:
|
||||
"""
|
||||
role_select = await role_dao.get_select(name=name, status=status)
|
||||
return await paging_data(db, role_select)
|
||||
|
||||
@staticmethod
|
||||
async def get_menu_tree(*, db: AsyncSession, pk: int) -> list[dict[str, Any] | None]:
|
||||
"""
|
||||
获取角色的菜单树形结构
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 角色 ID
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get_with_relation(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
menu_tree = get_tree_data(role.menus) if role.menus else []
|
||||
return menu_tree
|
||||
|
||||
@staticmethod
|
||||
async def get_scopes(*, db: AsyncSession, pk: int) -> list[int]:
|
||||
"""
|
||||
获取角色数据范围列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk:
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get_with_relation(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
scope_ids = [scope.id for scope in role.scopes]
|
||||
return scope_ids
|
||||
|
||||
@staticmethod
|
||||
async def create(*, db: AsyncSession, obj: CreateRoleParam) -> None:
|
||||
"""
|
||||
创建角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 角色创建参数
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get_by_name(db, obj.name)
|
||||
if role:
|
||||
raise errors.ConflictError(msg='角色已存在')
|
||||
await role_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def update(*, db: AsyncSession, pk: int, obj: UpdateRoleParam) -> int:
|
||||
"""
|
||||
更新角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 角色 ID
|
||||
:param obj: 角色更新参数
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
if role.name != obj.name and await role_dao.get_by_name(db, obj.name):
|
||||
raise errors.ConflictError(msg='角色已存在')
|
||||
count = await role_dao.update(db, pk, obj)
|
||||
for user in await role.awaitable_attrs.users:
|
||||
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_role_menu(*, db: AsyncSession, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||
"""
|
||||
更新角色菜单
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 角色 ID
|
||||
:param menu_ids: 菜单 ID 列表
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
for menu_id in menu_ids.menus:
|
||||
menu = await menu_dao.get(db, menu_id)
|
||||
if not menu:
|
||||
raise errors.NotFoundError(msg='菜单不存在')
|
||||
count = await role_dao.update_menus(db, pk, menu_ids)
|
||||
for user in await role.awaitable_attrs.users:
|
||||
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_role_scope(*, db: AsyncSession, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
|
||||
"""
|
||||
更新角色数据范围
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 角色 ID
|
||||
:param scope_ids: 权限规则 ID 列表
|
||||
:return:
|
||||
"""
|
||||
|
||||
role = await role_dao.get(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
for scope_id in scope_ids.scopes:
|
||||
scope = await data_scope_dao.get(db, scope_id)
|
||||
if not scope:
|
||||
raise errors.NotFoundError(msg='数据范围不存在')
|
||||
count = await role_dao.update_scopes(db, pk, scope_ids)
|
||||
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 delete(*, db: AsyncSession, obj: DeleteRoleParam) -> int:
|
||||
"""
|
||||
批量删除角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 角色 ID 列表
|
||||
:return:
|
||||
"""
|
||||
|
||||
count = await role_dao.delete(db, obj.pks)
|
||||
for pk in obj.pks:
|
||||
role = await role_dao.get(db, pk)
|
||||
if role:
|
||||
for user in await role.awaitable_attrs.users:
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
|
||||
role_service: RoleService = RoleService()
|
||||
@@ -0,0 +1,317 @@
|
||||
import random
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.admin.crud.crud_dept import dept_dao
|
||||
from backend.app.admin.crud.crud_role import role_dao
|
||||
from backend.app.admin.crud.crud_user import user_dao
|
||||
from backend.app.admin.model import Role, User
|
||||
from backend.app.admin.schema.user import (
|
||||
AddUserParam,
|
||||
ResetPasswordParam,
|
||||
UpdateUserParam,
|
||||
)
|
||||
from backend.common.context import ctx
|
||||
from backend.common.enums import UserPermissionType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.pagination import paging_data
|
||||
from backend.common.response.response_code import CustomErrorCode
|
||||
from backend.common.security.jwt import get_token, jwt_decode, password_verify
|
||||
from backend.core.conf import settings
|
||||
from backend.database.redis import redis_client
|
||||
|
||||
|
||||
class UserService:
|
||||
"""用户服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_userinfo(*, db: AsyncSession, pk: int | None = None, username: str | None = None) -> User:
|
||||
"""
|
||||
获取用户信息
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
user = await user_dao.get_with_relation(db, user_id=pk, username=username)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def get_roles(*, db: AsyncSession, pk: int) -> Sequence[Role]:
|
||||
"""
|
||||
获取用户所有角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
user = await user_dao.get_with_relation(db, user_id=pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
return user.roles
|
||||
|
||||
@staticmethod
|
||||
async def get_list(*, db: AsyncSession, dept: int, username: str, phone: str, status: int) -> dict[str, Any]:
|
||||
"""
|
||||
获取用户列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param dept: 部门 ID
|
||||
:param username: 用户名
|
||||
:param phone: 手机号
|
||||
:param status: 状态
|
||||
:return:
|
||||
"""
|
||||
user_select = await user_dao.get_select(dept=dept, username=username, phone=phone, status=status)
|
||||
return await paging_data(db, user_select)
|
||||
|
||||
@staticmethod
|
||||
async def create(*, db: AsyncSession, obj: AddUserParam) -> None:
|
||||
"""
|
||||
创建用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 用户添加参数
|
||||
:return:
|
||||
"""
|
||||
if await user_dao.get_by_username(db, obj.username):
|
||||
raise errors.ConflictError(msg='用户名已注册')
|
||||
obj.nickname = obj.nickname or f'#{random.randrange(88888, 99999)}'
|
||||
if not obj.password:
|
||||
raise errors.RequestError(msg='密码不允许为空')
|
||||
if not await dept_dao.get(db, obj.dept_id):
|
||||
raise errors.NotFoundError(msg='部门不存在')
|
||||
for role_id in obj.roles:
|
||||
if not await role_dao.get(db, role_id):
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
await user_dao.add(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def update(*, db: AsyncSession, pk: int, obj: UpdateUserParam) -> int:
|
||||
"""
|
||||
更新用户信息
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:param obj: 用户更新参数
|
||||
:return:
|
||||
"""
|
||||
user = await user_dao.get_with_relation(db, user_id=pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if obj.username != user.username and await user_dao.get_by_username(db, obj.username):
|
||||
raise errors.ConflictError(msg='用户名已注册')
|
||||
for role_id in obj.roles:
|
||||
if not await role_dao.get(db, role_id):
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
count = await user_dao.update(db, user, obj)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_permission(*, db: AsyncSession, request: Request, pk: int, type: UserPermissionType) -> int: # noqa: C901
|
||||
"""
|
||||
更新用户权限
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
:param type: 权限类型
|
||||
:return:
|
||||
"""
|
||||
match type:
|
||||
case UserPermissionType.superuser:
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
count = await user_dao.set_super(db, pk, is_super=not user.status)
|
||||
case UserPermissionType.staff:
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
count = await user_dao.set_staff(db, pk, is_staff=not user.is_staff)
|
||||
case UserPermissionType.status:
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1)
|
||||
case UserPermissionType.multi_login:
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
|
||||
new_multi_login = not multi_login
|
||||
count = await user_dao.set_multi_login(db, pk, multi_login=new_multi_login)
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
if pk == user.id:
|
||||
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
|
||||
if not new_multi_login:
|
||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||
await redis_client.delete_prefix(
|
||||
key_prefix,
|
||||
exclude=f'{key_prefix}:{token_payload.session_uuid}',
|
||||
)
|
||||
else:
|
||||
# 系统管理员修改他人时,他人 token 全部失效
|
||||
if not new_multi_login:
|
||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
|
||||
await redis_client.delete_prefix(key_prefix)
|
||||
case _:
|
||||
raise errors.RequestError(msg='权限类型不存在')
|
||||
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def reset_password(*, db: AsyncSession, pk: int, password: str) -> int:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:param password: 新密码
|
||||
:return:
|
||||
"""
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.reset_password(db, user.id, password)
|
||||
key_prefix = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||
]
|
||||
for prefix in key_prefix:
|
||||
await redis_client.delete(prefix)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_nickname(*, db: AsyncSession, request: Request, nickname: str) -> int:
|
||||
"""
|
||||
更新当前用户昵称
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
:param nickname: 用户昵称
|
||||
:return:
|
||||
"""
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
user = await user_dao.get(db, token_payload.id)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.update_nickname(db, token_payload.id, nickname)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_avatar(*, db: AsyncSession, request: Request, avatar: str) -> int:
|
||||
"""
|
||||
更新当前用户头像
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
:param avatar: 头像地址
|
||||
:return:
|
||||
"""
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
user = await user_dao.get(db, token_payload.id)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.update_avatar(db, token_payload.id, avatar)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_email(*, db: AsyncSession, request: Request, captcha: str, email: str) -> int:
|
||||
"""
|
||||
更新当前用户邮箱
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
:param captcha: 邮箱验证码
|
||||
:param email: 邮箱
|
||||
:return:
|
||||
"""
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
user = await user_dao.get(db, token_payload.id)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
|
||||
if not captcha_code:
|
||||
raise errors.RequestError(msg='验证码已失效,请重新获取')
|
||||
if captcha != captcha_code:
|
||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
|
||||
count = await user_dao.update_email(db, token_payload.id, email)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_password(*, db: AsyncSession, request: Request, obj: ResetPasswordParam) -> int:
|
||||
"""
|
||||
更新当前用户密码
|
||||
|
||||
:param db: 数据库会话
|
||||
:param request: FastAPI 请求对象
|
||||
:param obj: 密码重置参数
|
||||
:return:
|
||||
"""
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
user = await user_dao.get(db, token_payload.id)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if not password_verify(obj.old_password, user.password):
|
||||
raise errors.RequestError(msg='原密码错误')
|
||||
if obj.new_password != obj.confirm_password:
|
||||
raise errors.RequestError(msg='密码输入不一致')
|
||||
count = await user_dao.reset_password(db, user.id, obj.new_password)
|
||||
key_prefix = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||
]
|
||||
for prefix in key_prefix:
|
||||
await redis_client.delete_prefix(prefix)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, db: AsyncSession, pk: int) -> int:
|
||||
"""
|
||||
删除用户
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
user = await user_dao.get(db, pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.delete(db, user.id)
|
||||
key_prefix = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
]
|
||||
for key in key_prefix:
|
||||
await redis_client.delete_prefix(key)
|
||||
return count
|
||||
|
||||
|
||||
user_service: UserService = UserService()
|
||||
@@ -0,0 +1,7 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
def test_logout(client: TestClient, token_headers: dict[str, str]) -> None:
|
||||
response = client.post('/auth/logout', headers=token_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()['code'] == 200
|
||||
@@ -0,0 +1,39 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from backend.app.admin.tests.utils.db import override_get_db
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import get_db
|
||||
from backend.main import app
|
||||
|
||||
# 重载数据库
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
# Test data
|
||||
PYTEST_USERNAME = 'admin'
|
||||
PYTEST_PASSWORD = '123456'
|
||||
PYTEST_BASE_URL = f'http://testserver{settings.FASTAPI_API_V1_PATH}'
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def client() -> Generator:
|
||||
with TestClient(app, base_url=PYTEST_BASE_URL) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def token_headers(client: TestClient) -> dict[str, str]:
|
||||
params = {
|
||||
'username': PYTEST_USERNAME,
|
||||
'password': PYTEST_PASSWORD,
|
||||
}
|
||||
response = client.post('/auth/login/swagger', params=params)
|
||||
response.raise_for_status()
|
||||
token_type = response.json()['token_type']
|
||||
access_token = response.json()['access_token']
|
||||
headers = {'Authorization': f'{token_type} {access_token}'}
|
||||
return headers
|
||||
@@ -0,0 +1,15 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio.session import AsyncSession
|
||||
|
||||
from backend.database.db import create_async_engine_and_session, create_database_url
|
||||
|
||||
TEST_SQLALCHEMY_DATABASE_URL = create_database_url(unittest=True)
|
||||
|
||||
_, async_test_db_session = create_async_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL)
|
||||
|
||||
|
||||
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""session 生成器"""
|
||||
async with async_test_db_session() as session:
|
||||
yield session
|
||||
@@ -1 +0,0 @@
|
||||
Generic single-database configuration with an async dbapi.
|
||||
@@ -1,105 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
sys.path.append('../../')
|
||||
|
||||
from backend.app.core import path_conf # noqa: E402
|
||||
|
||||
if not os.path.exists(path_conf.Versions):
|
||||
os.makedirs(path_conf.Versions)
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
from backend.app.models import MappedBase # noqa: E402
|
||||
|
||||
target_metadata = MappedBase.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
from backend.app.database.db_mysql import SQLALCHEMY_DATABASE_URL # noqa: E402
|
||||
|
||||
config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL)
|
||||
|
||||
|
||||
def include_name(name, type_, parent_names):
|
||||
if type_ == 'table':
|
||||
return name in target_metadata.tables
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option('sqlalchemy.url')
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={'paramstyle': 'named'},
|
||||
include_name=include_name,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection):
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
include_name=include_name,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = AsyncEngine(
|
||||
engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
future=True,
|
||||
)
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.app.api.v1.api import router as api_router
|
||||
from backend.app.api.v1.auth import router as auth_router
|
||||
from backend.app.api.v1.casbin import router as casbin_router
|
||||
from backend.app.api.v1.dept import router as dept_router
|
||||
from backend.app.api.v1.dict_data import router as dict_data_router
|
||||
from backend.app.api.v1.dict_type import router as dict_type_router
|
||||
from backend.app.api.v1.log import router as log_router
|
||||
from backend.app.api.v1.menu import router as menu_router
|
||||
from backend.app.api.v1.mixed import router as mixed_router
|
||||
from backend.app.api.v1.monitor import router as monitor_router
|
||||
from backend.app.api.v1.role import router as role_router
|
||||
from backend.app.api.v1.task import router as task_router
|
||||
from backend.app.api.v1.user import router as user_router
|
||||
from backend.app.core.conf import settings
|
||||
|
||||
v1 = APIRouter(prefix=settings.API_V1_STR)
|
||||
|
||||
v1.include_router(auth_router, prefix='/auth', tags=['认证'])
|
||||
v1.include_router(user_router, prefix='/users', tags=['用户管理'])
|
||||
v1.include_router(casbin_router, prefix='/casbin', tags=['权限管理'])
|
||||
v1.include_router(dept_router, prefix='/depts', tags=['部门管理'])
|
||||
v1.include_router(role_router, prefix='/roles', tags=['角色管理'])
|
||||
v1.include_router(menu_router, prefix='/menus', tags=['菜单管理'])
|
||||
v1.include_router(api_router, prefix='/apis', tags=['API管理'])
|
||||
v1.include_router(dict_type_router, prefix='/dict-types', tags=['字典类型管理'])
|
||||
v1.include_router(dict_data_router, prefix='/dict-datas', tags=['字典数据管理'])
|
||||
v1.include_router(log_router, prefix='/logs', tags=['日志管理'])
|
||||
v1.include_router(monitor_router, prefix='/monitors', tags=['监控管理'])
|
||||
v1.include_router(task_router, prefix='/tasks', tags=['任务管理'])
|
||||
v1.include_router(mixed_router, prefix='/mixes', tags=['杂项'])
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.pagination import PageDepends, paging_data
|
||||
from backend.app.common.rbac import DependsRBAC
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.database.db_mysql import CurrentSession
|
||||
from backend.app.schemas.api import CreateApi, GetAllApi, UpdateApi
|
||||
from backend.app.services.api_service import ApiService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/all', summary='获取所有接口', dependencies=[DependsJwtAuth])
|
||||
async def get_all_apis():
|
||||
data = await ApiService.get_all()
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取接口详情', dependencies=[DependsJwtAuth])
|
||||
async def get_api(pk: int):
|
||||
api = await ApiService.get(pk=pk)
|
||||
return await response_base.success(data=api)
|
||||
|
||||
|
||||
@router.get('', summary='(模糊条件)分页获取所有接口', dependencies=[DependsJwtAuth, PageDepends])
|
||||
async def get_api_list(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query()] = None,
|
||||
method: Annotated[str | None, Query()] = None,
|
||||
path: Annotated[str | None, Query()] = None,
|
||||
):
|
||||
api_select = await ApiService.get_select(name=name, method=method, path=path)
|
||||
page_data = await paging_data(db, api_select, GetAllApi)
|
||||
return await response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post('', summary='创建接口', dependencies=[DependsRBAC])
|
||||
async def create_api(obj: CreateApi):
|
||||
await ApiService.create(obj=obj)
|
||||
return await response_base.success()
|
||||
|
||||
|
||||
@router.put('/{pk}', summary='更新接口', dependencies=[DependsRBAC])
|
||||
async def update_api(pk: int, obj: UpdateApi):
|
||||
count = await ApiService.update(pk=pk, obj=obj)
|
||||
if count > 0:
|
||||
return await response_base.success()
|
||||
return await response_base.fail()
|
||||
|
||||
|
||||
@router.delete('', summary='(批量)删除接口', dependencies=[DependsRBAC])
|
||||
async def delete_api(pk: Annotated[list[int], Query(...)]):
|
||||
count = await ApiService.delete(pk=pk)
|
||||
if count > 0:
|
||||
return await response_base.success()
|
||||
return await response_base.fail()
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from fastapi import APIRouter
|
||||
|
||||
from backend.app.api.v1.auth.auth import router as auth_router
|
||||
from backend.app.api.v1.auth.captcha import router as captcha_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(auth_router)
|
||||
router.include_router(captcha_router)
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi_limiter.depends import RateLimiter
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from backend.app.common.jwt import DependsJwtAuth
|
||||
from backend.app.common.response.response_schema import response_base
|
||||
from backend.app.schemas.token import GetLoginToken, GetNewToken, GetSwaggerToken
|
||||
from backend.app.schemas.user import AuthLogin
|
||||
from backend.app.services.auth_service import AuthService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口')
|
||||
async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> GetSwaggerToken:
|
||||
token, user = await AuthService().swagger_login(form_data=form_data)
|
||||
return GetSwaggerToken(access_token=token, user=user) # type: ignore
|
||||
|
||||
|
||||
@router.post(
|
||||
'/login',
|
||||
summary='用户登录',
|
||||
description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman',
|
||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
||||
)
|
||||
async def user_login(request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):
|
||||
access_token, refresh_token, access_expire, refresh_expire, user = await AuthService().login(
|
||||
request=request, obj=obj, background_tasks=background_tasks
|
||||
)
|
||||
data = GetLoginToken(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
access_token_expire_time=access_expire,
|
||||
refresh_token_expire_time=refresh_expire,
|
||||
user=user, # type: ignore
|
||||
)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post('/new_token', summary='创建新 token', dependencies=[DependsJwtAuth])
|
||||
async def create_new_token(request: Request, refresh_token: Annotated[str, Query(...)]):
|
||||
(
|
||||
new_access_token,
|
||||
new_refresh_token,
|
||||
new_access_token_expire_time,
|
||||
new_refresh_token_expire_time,
|
||||
) = await AuthService.new_token(request=request, refresh_token=refresh_token)
|
||||
data = GetNewToken(
|
||||
access_token=new_access_token,
|
||||
access_token_expire_time=new_access_token_expire_time,
|
||||
refresh_token=new_refresh_token,
|
||||
refresh_token_expire_time=new_refresh_token_expire_time,
|
||||
)
|
||||
return await response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post('/logout', summary='用户登出', dependencies=[DependsJwtAuth])
|
||||
async def user_logout(request: Request):
|
||||
await AuthService.logout(request=request)
|
||||
return await response_base.success()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user