mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e492cec7d7 | ||
|
|
92fe1e7554 | ||
|
|
7b080600f6 | ||
|
|
71f9667bc0 | ||
|
|
2bf548f8b9 | ||
|
|
15fd4545aa | ||
|
|
7f6c4ad6da | ||
|
|
2be12b4ea4 | ||
|
|
baebdd9870 | ||
|
|
d1648bb709 | ||
|
|
a4a394a18d | ||
|
|
951fdc902a | ||
|
|
18ffa06d70 | ||
|
|
d28f152d78 | ||
|
|
1e3e9ea84a | ||
|
|
5b42e59d65 | ||
|
|
1882467554 | ||
|
|
f1cd48e9d0 | ||
|
|
d1fd84617b | ||
|
|
03fcf82b26 | ||
|
|
f75e1bf3df | ||
|
|
554a6f59bd | ||
|
|
68d41cf028 | ||
|
|
5defeca590 | ||
|
|
4d94418124 | ||
|
|
6c258f056a | ||
|
|
87af1f1319 | ||
|
|
3f9badd4b9 | ||
|
|
06de770c4d | ||
|
|
b5948441ac | ||
|
|
acbab69f12 |
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.ruff_cache/
|
||||||
|
.pytest_cache/
|
||||||
@@ -25,12 +25,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
|
||||||
uv sync --only-group lint
|
uv sync --only-group lint
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
chmod 755 scripts/lint.sh
|
chmod 755 backend/scripts/lint.sh
|
||||||
./scripts/lint.sh
|
./backend/scripts/lint.sh
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.cursor/
|
||||||
|
.DS_Store
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.python-version
|
||||||
|
.ruff_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
|||||||
@@ -23,15 +23,8 @@ repos:
|
|||||||
rev: 0.5.29
|
rev: 0.5.29
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
args:
|
|
||||||
- '--directory'
|
|
||||||
- 'backend'
|
|
||||||
files: backend/uv\.lock$
|
|
||||||
- id: uv-export
|
- id: uv-export
|
||||||
args:
|
args:
|
||||||
- '--directory'
|
|
||||||
- 'backend'
|
|
||||||
- '-o'
|
- '-o'
|
||||||
- 'requirements.txt'
|
- 'requirements.txt'
|
||||||
- '--no-hashes'
|
- '--no-hashes'
|
||||||
files: backend/uv\.lock$
|
|
||||||
|
|||||||
+383
@@ -0,0 +1,383 @@
|
|||||||
|
<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.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 -->
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
# 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 python:3.10-slim AS builder
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
|
|
||||||
|
# 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/*
|
||||||
|
|
||||||
|
# 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 \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --frozen --no-default-groups --group server
|
||||||
|
|
||||||
|
# === Runtime base server image ===
|
||||||
|
FROM python:3.10-slim AS base_server
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
|
||||||
|
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends supervisor \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . /fba
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local /usr/local
|
||||||
|
|
||||||
|
# Install plugin dependencies
|
||||||
|
WORKDIR /fba
|
||||||
|
ENV PYTHONPATH=/fba
|
||||||
|
RUN python3 backend/scripts/init_plugin.py
|
||||||
|
|
||||||
|
# === FastAPI server image ===
|
||||||
|
FROM base_server AS fastapi_server
|
||||||
|
|
||||||
|
WORKDIR /fba
|
||||||
|
|
||||||
|
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
|
COPY deploy/backend/fastapi_server.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log/fastapi_server
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port","8000"]
|
||||||
|
|
||||||
|
# === Celery server image ===
|
||||||
|
FROM base_server AS celery
|
||||||
|
|
||||||
|
WORKDIR /fba/backend/
|
||||||
|
|
||||||
|
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
|
COPY deploy/backend/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}
|
||||||
@@ -6,9 +6,7 @@
|
|||||||
|
|
||||||
English | [简体中文](./README.zh-CN.md)
|
English | [简体中文](./README.zh-CN.md)
|
||||||
|
|
||||||
A backend and frontend separation solution based on the FastAPI framework, following
|
Enterprise-level backend architecture solution
|
||||||
the [pseudo 3-tier architecture](#pseudo-3-tier-architecture) design, supporting **Python 3.10** and above
|
|
||||||
versions
|
|
||||||
|
|
||||||
**🔥Continuously updated and maintained🔥**
|
**🔥Continuously updated and maintained🔥**
|
||||||
|
|
||||||
@@ -49,37 +47,39 @@ pattern, use templates to transform it to your heart's content!
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] Design with FastAPI PEP 593 Annotated Parameters
|
- [x] Global FastAPI PEP 593 Annotated parameter style
|
||||||
- [x] Global asynchronous design with async/await + asgiref
|
- [x] Comprehensive async/await + asgiref asynchronous design
|
||||||
- [x] Follows Restful API specification
|
- [x] Adheres to RESTful API specifications
|
||||||
- [x] Global SQLAlchemy 2.0 syntax
|
- [x] Uses SQLAlchemy 2.0 with new syntax
|
||||||
- [x] Pydantic v1 and v2 (different branches)
|
- [x] Uses Pydantic v2 version
|
||||||
- [x] Casbin RBAC access control model
|
- [x] Implements role-menu RBAC access control
|
||||||
- [x] Role menu RBAC access control model
|
- [x] Integrates Casbin RBAC access control
|
||||||
- [x] Celery asynchronous tasks
|
- [x] Supports Celery asynchronous tasks
|
||||||
- [x] JWT middleware whitelist authentication
|
- [x] Custom-developed JWT authentication middleware
|
||||||
- [x] Global customizable time zone time
|
- [x] Supports global custom time zones
|
||||||
- [x] Docker / Docker-compose deployment
|
- [x] Supports Docker / Docker-compose deployment
|
||||||
- [x] Pytest Unit Testing
|
- [x] Integrates Pytest unit testing
|
||||||
|
|
||||||
## Built-in features
|
## Built-in Functions
|
||||||
|
|
||||||
- [x] User management: System User Role Management, Permission Allocation
|
- [x] User Management: Assign roles and permissions
|
||||||
- [x] Department management: Configure system organization (company, department, team...)
|
- [x] Department Management: Configure organizational structure (company, department, team, etc.)
|
||||||
- [x] Menu management: Configure system menu, user menu, button permission tags
|
- [x] Menu Management: Set up menus and button-level permissions
|
||||||
- [x] Role management: role menu permission allocation, role route permission allocation
|
- [x] Role Management: Configure roles, assign menus and permissions
|
||||||
- [x] Dictionary management: Maintain commonly used fixed data or parameters within the system
|
- [x] Dictionary Management: Maintain common parameters and configurations
|
||||||
- [x] Token management: System user online status detection, supports kicking users offline
|
- [x] Parameter Management: Dynamically configure commonly used system parameters
|
||||||
- [x] Login authentication: backend-based graphical captcha background authentication login
|
- [x] Notification Announcements: Publish and maintain system notification and announcement information
|
||||||
- [x] Multipoint login: One-click modification of multipoint login through user information
|
- [x] Token Management: Detect online status, support forced logout
|
||||||
- [x] OAuth 2.0: Built-in self-developed OAuth 2.0 login integration
|
- [x] Multi-device Login: Support one-click switching between multi-device login modes
|
||||||
- [x] Code generation: automatic backend code generation, supports preview, writing, and download
|
- [x] OAuth 2.0: Built-in custom-developed OAuth 2.0 authorization login
|
||||||
- [x] Scheduled task: Automated task, asynchronous task, supports function calls
|
- [x] Plugin System: Hot-swappable plugin design to reduce coupling
|
||||||
- [x] Plugin system: Say goodbye to high coupling integration through hot-pluggable plugin mode
|
- [x] Scheduled Tasks: Support scheduled, asynchronous tasks, and function calls
|
||||||
- [x] Operation log: Record and query of system normal and abnormal operations
|
- [x] Code Generation: Automatically generate code with preview, write, and download support
|
||||||
- [x] Login log: Record and query of normal and abnormal user login
|
- [x] Operation Logs: Record and query normal and abnormal operations
|
||||||
- [x] Service monitoring: Server hardware device information and status
|
- [x] Login Logs: Record and query normal and abnormal logins
|
||||||
- [x] API documentation: Automatically generate online interactive API documentation
|
- [x] Cache Monitoring: Query system cache information and command statistics
|
||||||
|
- [x] Service Monitoring: View server hardware information and status
|
||||||
|
- [x] API Documentation: Automatically generate online interactive API documentation
|
||||||
|
|
||||||
## Development and deployment
|
## Development and deployment
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ the [official documentation](https://fastapi-practices.github.io/fastapi_best_ar
|
|||||||
|
|
||||||
## Interactivity
|
## Interactivity
|
||||||
|
|
||||||
[TG / Discord](https://wu-clan.github.io/homepage/)
|
[Discord](https://wu-clan.github.io/homepage/)
|
||||||
|
|
||||||
## Sponsor us
|
## Sponsor us
|
||||||
|
|
||||||
|
|||||||
+31
-29
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
简体中文 | [English](./README.md)
|
简体中文 | [English](./README.md)
|
||||||
|
|
||||||
基于 FastAPI 框架的前后端分离中后台解决方案,遵循[伪三层架构](#伪三层架构)设计, 支持 **python3.10** 及以上版本
|
企业级后端架构解决方案
|
||||||
|
|
||||||
**🔥持续更新维护中🔥**
|
**🔥持续更新维护中🔥**
|
||||||
|
|
||||||
@@ -43,38 +43,40 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
|||||||
| 数据访问 | dao / mapper | crud |
|
| 数据访问 | dao / mapper | crud |
|
||||||
| 模型 | model / entity | model |
|
| 模型 | model / entity | model |
|
||||||
|
|
||||||
## 特征
|
## 特性
|
||||||
|
|
||||||
- [x] 全局 FastAPI PEP 593 Annotated 参数风格
|
- [x] 全局 FastAPI PEP 593 Annotated 参数风格
|
||||||
- [x] async/await + asgiref 的全局异步设计
|
- [x] 全面 async/await + asgiref 异步设计
|
||||||
- [x] 遵循 Restful API 规范
|
- [x] 遵循 RESTful API 规范
|
||||||
- [x] 全局 SQLAlchemy 2.0 语法
|
- [x] 使用 SQLAlchemy 2.0 全新语法
|
||||||
- [x] Pydantic v1 和 v2 (不同分支)
|
- [x] 使用 Pydantic v2 版本
|
||||||
- [x] Casbin RBAC 访问控制模型
|
- [x] 实现角色菜单 RBAC 访问控制
|
||||||
- [x] 角色菜单 RBAC 访问控制模型
|
- [x] 集成 Casbin RBAC 访问控制
|
||||||
- [x] Celery 异步任务
|
- [x] 支持 Celery 异步任务
|
||||||
- [x] JWT 中间件白名单认证
|
- [x] 自研 JWT 认证中间件
|
||||||
- [x] 全局自定义时区时间
|
- [x] 支持全局自定义时间时区
|
||||||
- [x] Docker / Docker-compose 部署
|
- [x] 支持 Docker / Docker-compose 部署
|
||||||
- [x] Pytest 单元测试
|
- [x] 集成 Pytest 单元测试
|
||||||
|
|
||||||
## 内置功能
|
## 内置功能
|
||||||
|
|
||||||
- [x] 用户管理:系统用户角色管理,权限分配
|
- [x] 用户管理:分配角色和权限
|
||||||
- [x] 部门管理:配置系统组织机构(公司、部门、小组...)
|
- [x] 部门管理:配置组织架构(公司、部门、小组等)
|
||||||
- [x] 菜单管理:配置系统菜单,用户菜单,按钮权限标识
|
- [x] 菜单管理:设置菜单及按钮级权限
|
||||||
- [x] 角色管理:角色菜单权限分配,角色路由权限分配
|
- [x] 角色管理:配置角色、分配菜单和权限
|
||||||
- [x] 字典管理:维护系统内部常用固定数据或参数
|
- [x] 字典管理:维护常用参数和配置
|
||||||
- [x] 令牌管理:系统用户在线状态检测,支持踢人下线
|
- [x] 参数管理:系统常用参数动态配置
|
||||||
- [x] 登录认证:基于后端的图形验证码后台认证登录
|
- [x] 通知公告:发布和维护系统通知公告信息
|
||||||
- [x] 多点登录:通过用户信息一键修改多点登录支持
|
- [x] 令牌管理:检测在线状态,支持强制下线
|
||||||
- [x] OAuth20:内置自研 OAuth 2.0 登录集成
|
- [x] 多端登录:支持一键切换多端登录模式
|
||||||
- [x] 代码生成:后端代码自动生成,支持预览,写入及下载
|
- [x] OAuth 2.0:内置自研 OAuth 2.0 授权登录
|
||||||
- [x] 定时任务:自动化任务,异步任务,支持函数调用
|
- [x] 插件系统:热插拔插件设计,降低耦合
|
||||||
- [x] 插件系统:通过热插拔插件模式告别高耦合集成
|
- [x] 定时任务:支持定时,异步任务及函数调用
|
||||||
- [x] 操作日志:系统正常和异常操作的日志记录与查询
|
- [x] 代码生成:自动生成代码,支持预览、写入和下载
|
||||||
- [x] 登录日志:用户正常和异常登录的日志记录与查询
|
- [x] 操作日志:记录和查询正常和异常操作
|
||||||
- [x] 服务监控:服务器硬件设备信息与状态
|
- [x] 登录日志:记录和查询正常和异常登录
|
||||||
|
- [x] 缓存监控:查询系统缓存信息和命令统计
|
||||||
|
- [x] 服务监控:查看服务器硬件信息和状态
|
||||||
- [x] 接口文档:自动生成在线交互式 API 文档
|
- [x] 接口文档:自动生成在线交互式 API 文档
|
||||||
|
|
||||||
## 开发部署
|
## 开发部署
|
||||||
@@ -98,7 +100,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
|||||||
|
|
||||||
## 互动
|
## 互动
|
||||||
|
|
||||||
[TG / Discord](https://wu-clan.github.io/homepage/)
|
[Discord](https://wu-clan.github.io/homepage/)
|
||||||
|
|
||||||
## 赞助我们
|
## 赞助我们
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
.python-version
|
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.env
|
.env
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
.mypy_cache/
|
|
||||||
log/
|
log/
|
||||||
alembic/versions/
|
alembic/versions/
|
||||||
static/media/
|
static/media/
|
||||||
.ruff_cache/
|
|
||||||
.pytest_cache/
|
|
||||||
.python-version
|
|
||||||
celerybeat-schedule.*
|
celerybeat-schedule.*
|
||||||
|
|||||||
+27
-23
@@ -1,64 +1,68 @@
|
|||||||
# FBA Project - Backend
|
# FastAPI Best Architecture - Backend
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Due to Docker context limitations, you can't build an image in this directory
|
|
||||||
|
|
||||||
1. Make sure you're at the root of the project
|
1. Make sure you're at the root of the project
|
||||||
2. Run the following Docker command to build container:
|
2. Run the following Docker command to build container:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build -f backend/backend.dockerfile -t fba_backend_independent .
|
docker build -f Dockerfile -t fba_backend_independent .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start container
|
3. Start container
|
||||||
|
|
||||||
|
Native boot needs to change `127.0.0.1` in `.env` to `host.docker.internal`
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -d fba_backend_independent -p 8000:8000 --name fba_app
|
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Prerequisites
|
1. Prerequisites
|
||||||
|
|
||||||
You'll need the following prerequisites:
|
- Python >= 3.10
|
||||||
- Any python version between Python >= 3.10
|
|
||||||
- Git
|
- Git
|
||||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||||
- Fork this repository to your GitHub account
|
- Fork this repository to your GitHub account
|
||||||
|
|
||||||
2. Installation and setup
|
2. Installation and setup
|
||||||
|
|
||||||
Clone your fork and cd into the repo directory
|
Clone your forked repository:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/<your username>/fastapi_best_architecture.git
|
git clone https://github.com/<your account>/fastapi_best_architecture.git
|
||||||
|
|
||||||
cd fastapi_best_architecture/backend
|
|
||||||
|
|
||||||
uv venv
|
|
||||||
|
|
||||||
uv pip install -r requirements.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Checkout a new branch and make your changes
|
Go to the root directory of the project, open the terminal, and run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv sync --frozen
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Checkout
|
||||||
|
|
||||||
|
Checkout a new branch and make your changes
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Checkout a new branch and make your changes
|
|
||||||
git checkout -b your-new-feature-branch
|
git checkout -b your-new-feature-branch
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run linting
|
4. Format and Lint
|
||||||
|
|
||||||
|
Auto-formatting and lint via `pre-commit`
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Run automated code formatting and linting
|
|
||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Commit and push your changes
|
5. Commit and push
|
||||||
|
|
||||||
Commit your changes, push your branch to GitHub, and create a pull request.
|
Commit your changes and push your branch to the GitHub.
|
||||||
|
|
||||||
|
6. PR
|
||||||
|
|
||||||
|
Create a PR via GitHub
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def user_login(
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/token/new', summary='创建新 token')
|
@router.post('/token/new', summary='创建新 token')
|
||||||
async def create_new_token(request: Request, response: Response) -> ResponseSchemaModel[GetNewToken]:
|
async def create_new_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
||||||
data = await auth_service.new_token(request=request)
|
data = await auth_service.new_token(request=request)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取登录日志',
|
summary='分页获取登录日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
@@ -26,9 +26,9 @@ router = APIRouter()
|
|||||||
)
|
)
|
||||||
async def get_pagination_login_logs(
|
async def get_pagination_login_logs(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
username: Annotated[str | None, Query()] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
ip: Annotated[str | None, Query()] = None,
|
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
|
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
|
||||||
log_select = await login_log_service.get_select(username=username, status=status, ip=ip)
|
log_select = await login_log_service.get_select(username=username, status=status, ip=ip)
|
||||||
page_data = await paging_data(db, log_select)
|
page_data = await paging_data(db, log_select)
|
||||||
@@ -37,13 +37,13 @@ async def get_pagination_login_logs(
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除登录日志',
|
summary='批量删除登录日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('log:login:del')),
|
Depends(RequestPermission('log:login:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_login_log(pk: Annotated[list[int], Query(description='登录日志 ID 列表')]) -> ResponseModel:
|
||||||
count = await login_log_service.delete(pk=pk)
|
count = await login_log_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取操作日志',
|
summary='分页获取操作日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
@@ -26,9 +26,9 @@ router = APIRouter()
|
|||||||
)
|
)
|
||||||
async def get_pagination_opera_logs(
|
async def get_pagination_opera_logs(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
username: Annotated[str | None, Query()] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
ip: Annotated[str | None, Query()] = None,
|
ip: Annotated[str | None, Query(description='IP 地址')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
|
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
|
||||||
log_select = await opera_log_service.get_select(username=username, status=status, ip=ip)
|
log_select = await opera_log_service.get_select(username=username, status=status, ip=ip)
|
||||||
page_data = await paging_data(db, log_select)
|
page_data = await paging_data(db, log_select)
|
||||||
@@ -37,13 +37,13 @@ async def get_pagination_opera_logs(
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除操作日志',
|
summary='批量删除操作日志',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('log:opera:del')),
|
Depends(RequestPermission('log:opera:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_opera_log(pk: Annotated[list[int], Query(description='操作日志 ID 列表')]) -> ResponseModel:
|
||||||
count = await opera_log_service.delete(pk=pk)
|
count = await opera_log_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -19,5 +19,8 @@ router = APIRouter()
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_redis_info() -> ResponseModel:
|
async def get_redis_info() -> ResponseModel:
|
||||||
data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()}
|
data = {
|
||||||
|
'info': await redis_info.get_info(),
|
||||||
|
'stats': await redis_info.get_stats(),
|
||||||
|
}
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ _linux_do_client = LinuxDoOAuth20(
|
|||||||
_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取 Linux Do 授权链接')
|
@router.get('', summary='获取 LinuxDo 授权链接')
|
||||||
async def linux_do_auth2() -> ResponseSchemaModel[str]:
|
async def linux_do_auth2() -> ResponseSchemaModel[str]:
|
||||||
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
|
||||||
return response_base.success(data=auth_url)
|
return response_base.success(data=auth_url)
|
||||||
@@ -27,8 +27,8 @@ async def linux_do_auth2() -> ResponseSchemaModel[str]:
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/callback',
|
'/callback',
|
||||||
summary='Linux Do 授权自动重定向',
|
summary='LinuxDo 授权自动重定向',
|
||||||
description='Linux Do 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户',
|
description='LinuxDo 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户',
|
||||||
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
|
||||||
)
|
)
|
||||||
async def linux_do_login(
|
async def linux_do_login(
|
||||||
@@ -45,6 +45,6 @@ async def linux_do_login(
|
|||||||
response=response,
|
response=response,
|
||||||
background_tasks=background_tasks,
|
background_tasks=background_tasks,
|
||||||
user=user,
|
user=user,
|
||||||
social=UserSocialType.linuxdo,
|
social=UserSocialType.linux_do,
|
||||||
)
|
)
|
||||||
return RedirectResponse(url=f'{admin_settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}')
|
return RedirectResponse(url=f'{admin_settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from backend.app.admin.api.v1.sys.dept import router as dept_router
|
|||||||
from backend.app.admin.api.v1.sys.dict_data import router as dict_data_router
|
from backend.app.admin.api.v1.sys.dict_data import router as dict_data_router
|
||||||
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_router
|
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_router
|
||||||
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
||||||
|
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
|
||||||
from backend.app.admin.api.v1.sys.role import router as role_router
|
from backend.app.admin.api.v1.sys.role import router as role_router
|
||||||
from backend.app.admin.api.v1.sys.token import router as token_router
|
from backend.app.admin.api.v1.sys.token import router as token_router
|
||||||
from backend.app.admin.api.v1.sys.upload import router as upload_router
|
from backend.app.admin.api.v1.sys.upload import router as upload_router
|
||||||
@@ -15,7 +16,7 @@ from backend.app.admin.api.v1.sys.user import router as user_router
|
|||||||
|
|
||||||
router = APIRouter(prefix='/sys')
|
router = APIRouter(prefix='/sys')
|
||||||
|
|
||||||
router.include_router(config_router, prefix='/configs', tags=['系统配置'])
|
router.include_router(config_router, prefix='/configs', tags=['系统参数配置'])
|
||||||
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
|
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
|
||||||
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
|
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
|
||||||
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
|
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
|
||||||
@@ -25,3 +26,4 @@ router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
|||||||
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则'])
|
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则'])
|
||||||
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
|
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
|
||||||
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
|
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
|
||||||
|
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ from backend.database.db import CurrentSession
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/website', summary='获取网站配置信息', dependencies=[DependsJwtAuth])
|
@router.get('/website', summary='获取网站参数配置', dependencies=[DependsJwtAuth])
|
||||||
async def get_website_config() -> ResponseSchemaModel[GetConfigDetail]:
|
async def get_website_config() -> ResponseSchemaModel[list[GetConfigDetail]]:
|
||||||
config = await config_service.get_built_in_config('website')
|
config = await config_service.get_built_in_config('website')
|
||||||
return response_base.success(data=config)
|
return response_base.success(data=config)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/website',
|
'/website',
|
||||||
summary='保存网站配置信息',
|
summary='保存网站参数配置',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:config:website:add')),
|
Depends(RequestPermission('sys:config:website:add')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
@@ -41,7 +41,7 @@ async def save_website_config(objs: list[SaveBuiltInConfigParam]) -> ResponseMod
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/protocol', summary='获取用户协议', dependencies=[DependsJwtAuth])
|
@router.get('/protocol', summary='获取用户协议', dependencies=[DependsJwtAuth])
|
||||||
async def get_protocol_config() -> ResponseSchemaModel[GetConfigDetail]:
|
async def get_protocol_config() -> ResponseSchemaModel[list[GetConfigDetail]]:
|
||||||
config = await config_service.get_built_in_config('protocol')
|
config = await config_service.get_built_in_config('protocol')
|
||||||
return response_base.success(data=config)
|
return response_base.success(data=config)
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ async def save_protocol_config(objs: list[SaveBuiltInConfigParam]) -> ResponseMo
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/policy', summary='获取用户政策', dependencies=[DependsJwtAuth])
|
@router.get('/policy', summary='获取用户政策', dependencies=[DependsJwtAuth])
|
||||||
async def get_policy_config() -> ResponseSchemaModel[GetConfigDetail]:
|
async def get_policy_config() -> ResponseSchemaModel[list[GetConfigDetail]]:
|
||||||
config = await config_service.get_built_in_config('policy')
|
config = await config_service.get_built_in_config('policy')
|
||||||
return response_base.success(data=config)
|
return response_base.success(data=config)
|
||||||
|
|
||||||
@@ -78,23 +78,23 @@ async def save_policy_config(objs: list[SaveBuiltInConfigParam]) -> ResponseMode
|
|||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取系统参数配置详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取参数配置详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_config(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetConfigDetail]:
|
async def get_config(pk: Annotated[int, Path(description='参数配置 ID')]) -> ResponseSchemaModel[GetConfigDetail]:
|
||||||
config = await config_service.get(pk)
|
config = await config_service.get(pk)
|
||||||
return response_base.success(data=config)
|
return response_base.success(data=config)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有系统参数配置',
|
summary='分页获取所有参数配置',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_config(
|
async def get_pagination_configs(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[str | None, Query()] = None,
|
name: Annotated[str | None, Query(description='参数配置名称')] = None,
|
||||||
type: Annotated[str | None, Query()] = None,
|
type: Annotated[str | None, Query()] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetConfigDetail]]:
|
) -> ResponseSchemaModel[PageData[GetConfigDetail]]:
|
||||||
config_select = await config_service.get_select(name=name, type=type)
|
config_select = await config_service.get_select(name=name, type=type)
|
||||||
@@ -104,7 +104,7 @@ async def get_pagination_config(
|
|||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'',
|
'',
|
||||||
summary='创建系统参数配置',
|
summary='创建参数配置',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:config:add')),
|
Depends(RequestPermission('sys:config:add')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
@@ -117,13 +117,13 @@ async def create_config(obj: CreateConfigParam) -> ResponseModel:
|
|||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}',
|
'/{pk}',
|
||||||
summary='更新系统参数配置',
|
summary='更新参数配置',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:config:edit')),
|
Depends(RequestPermission('sys:config:edit')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -> ResponseModel:
|
async def update_config(pk: Annotated[int, Path(description='参数配置 ID')], obj: UpdateConfigParam) -> ResponseModel:
|
||||||
count = await config_service.update(pk=pk, obj=obj)
|
count = await config_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -132,13 +132,13 @@ async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除系统参数配置',
|
summary='批量删除参数配置',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:config:del')),
|
Depends(RequestPermission('sys:config:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_config(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_config(pk: Annotated[list[int], Query(description='参数配置 ID 列表')]) -> ResponseModel:
|
||||||
count = await config_service.delete(pk=pk)
|
count = await config_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from backend.common.security.jwt import DependsJwtAuth
|
|||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession
|
||||||
from backend.utils.serializers import select_as_dict, select_list_serialize
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -24,35 +23,37 @@ async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/model/{model}/columns', summary='获取支持过滤的数据库模型列', dependencies=[DependsJwtAuth])
|
@router.get('/model/{model}/columns', summary='获取支持过滤的数据库模型列', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_rule_model_columns(model: Annotated[str, Path()]) -> ResponseSchemaModel[list[str]]:
|
async def get_data_rule_model_columns(
|
||||||
|
model: Annotated[str, Path(description='模型名称')],
|
||||||
|
) -> ResponseSchemaModel[list[str]]:
|
||||||
models = await data_rule_service.get_columns(model=model)
|
models = await data_rule_service.get_columns(model=model)
|
||||||
return response_base.success(data=models)
|
return response_base.success(data=models)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_data_rule() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
async def get_all_data_rules() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
|
||||||
data_rules = await data_rule_service.get_all()
|
data = await data_rule_service.get_all()
|
||||||
data = select_list_serialize(data_rules)
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_rule(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDataRuleDetail]:
|
async def get_data_rule(
|
||||||
data_rule = await data_rule_service.get(pk=pk)
|
pk: Annotated[int, Path(description='数据规则 ID')],
|
||||||
data = GetDataRuleDetail(**select_as_dict(data_rule))
|
) -> ResponseSchemaModel[GetDataRuleDetail]:
|
||||||
|
data = await data_rule_service.get(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有数据权限规则',
|
summary='分页获取所有数据权限规则',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def get_pagination_data_rule(
|
async def get_pagination_data_rules(
|
||||||
db: CurrentSession, name: Annotated[str | None, Query()] = None
|
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
|
||||||
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
|
||||||
data_rule_select = await data_rule_service.get_select(name=name)
|
data_rule_select = await data_rule_service.get_select(name=name)
|
||||||
page_data = await paging_data(db, data_rule_select)
|
page_data = await paging_data(db, data_rule_select)
|
||||||
@@ -80,7 +81,9 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRuleParam) -> ResponseModel:
|
async def update_data_rule(
|
||||||
|
pk: Annotated[int, Path(description='数据规则 ID')], obj: UpdateDataRuleParam
|
||||||
|
) -> ResponseModel:
|
||||||
count = await data_rule_service.update(pk=pk, obj=obj)
|
count = await data_rule_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -89,13 +92,15 @@ async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRulePar
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除数据权限规则',
|
summary='批量删除数据权限规则',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('data:rule:del')),
|
Depends(RequestPermission('data:rule:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_data_rule(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_data_rule(
|
||||||
|
request: Request, pk: Annotated[list[int], Query(description='数据规则 ID 列表')]
|
||||||
|
) -> ResponseModel:
|
||||||
count = await data_rule_service.delete(request=request, pk=pk)
|
count = await data_rule_service.delete(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -10,24 +10,22 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
|
|||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_dept(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDeptDetail]:
|
async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseSchemaModel[GetDeptDetail]:
|
||||||
dept = await dept_service.get(pk=pk)
|
data = await dept_service.get(pk=pk)
|
||||||
data = GetDeptDetail(**select_as_dict(dept))
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_depts_tree(
|
async def get_all_depts(
|
||||||
name: Annotated[str | None, Query()] = None,
|
name: Annotated[str | None, Query(description='部门名称')] = None,
|
||||||
leader: Annotated[str | None, Query()] = None,
|
leader: Annotated[str | None, Query(description='部门负责人')] = None,
|
||||||
phone: Annotated[str | None, Query()] = None,
|
phone: Annotated[str | None, Query(description='联系电话')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||||
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status)
|
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status)
|
||||||
return response_base.success(data=dept)
|
return response_base.success(data=dept)
|
||||||
@@ -54,7 +52,7 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> ResponseModel:
|
async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam) -> ResponseModel:
|
||||||
count = await dept_service.update(pk=pk, obj=obj)
|
count = await dept_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -69,7 +67,7 @@ async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> Re
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_dept(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def delete_dept(request: Request, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
|
||||||
count = await dept_service.delete(request=request, pk=pk)
|
count = await dept_service.delete(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query
|
from fastapi import APIRouter, Depends, Path, Query
|
||||||
|
|
||||||
from backend.app.admin.schema.dict_data import CreateDictDataParam, GetDictDataDetail, UpdateDictDataParam
|
from backend.app.admin.schema.dict_data import (
|
||||||
|
CreateDictDataParam,
|
||||||
|
GetDictDataDetail,
|
||||||
|
GetDictDataWithRelation,
|
||||||
|
UpdateDictDataParam,
|
||||||
|
)
|
||||||
from backend.app.admin.service.dict_data_service import dict_data_service
|
from backend.app.admin.service.dict_data_service import dict_data_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
@@ -12,21 +17,21 @@ from backend.common.security.jwt import DependsJwtAuth
|
|||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDictDataDetail]:
|
async def get_dict_data(
|
||||||
dict_data = await dict_data_service.get(pk=pk)
|
pk: Annotated[int, Path(description='字典数据 ID')],
|
||||||
data = GetDictDataDetail(**select_as_dict(dict_data))
|
) -> ResponseSchemaModel[GetDictDataWithRelation]:
|
||||||
|
data = await dict_data_service.get(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有字典',
|
summary='分页获取所有字典',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
@@ -34,9 +39,9 @@ async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[Ge
|
|||||||
)
|
)
|
||||||
async def get_pagination_dict_datas(
|
async def get_pagination_dict_datas(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
label: Annotated[str | None, Query()] = None,
|
label: Annotated[str | None, Query(description='字典数据标签')] = None,
|
||||||
value: Annotated[str | None, Query()] = None,
|
value: Annotated[str | None, Query(description='字典数据键值')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetDictDataDetail]]:
|
) -> ResponseSchemaModel[PageData[GetDictDataDetail]]:
|
||||||
dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status)
|
dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status)
|
||||||
page_data = await paging_data(db, dict_data_select)
|
page_data = await paging_data(db, dict_data_select)
|
||||||
@@ -64,7 +69,9 @@ async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataParam) -> ResponseModel:
|
async def update_dict_data(
|
||||||
|
pk: Annotated[int, Path(description='字典数据 ID')], obj: UpdateDictDataParam
|
||||||
|
) -> ResponseModel:
|
||||||
count = await dict_data_service.update(pk=pk, obj=obj)
|
count = await dict_data_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -73,13 +80,13 @@ async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataPar
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除字典',
|
summary='批量删除字典',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:dict:data:del')),
|
Depends(RequestPermission('sys:dict:data:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_dict_data(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_dict_data(pk: Annotated[list[int], Query(description='字典数据 ID 列表')]) -> ResponseModel:
|
||||||
count = await dict_data_service.delete(pk=pk)
|
count = await dict_data_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有字典类型',
|
summary='分页获取所有字典类型',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
@@ -26,9 +26,9 @@ router = APIRouter()
|
|||||||
)
|
)
|
||||||
async def get_pagination_dict_types(
|
async def get_pagination_dict_types(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[str | None, Query()] = None,
|
name: Annotated[str | None, Query(description='字典类型名称')] = None,
|
||||||
code: Annotated[str | None, Query()] = None,
|
code: Annotated[str | None, Query(description='字典类型编码')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetDictTypeDetail]]:
|
) -> ResponseSchemaModel[PageData[GetDictTypeDetail]]:
|
||||||
dict_type_select = await dict_type_service.get_select(name=name, code=code, status=status)
|
dict_type_select = await dict_type_service.get_select(name=name, code=code, status=status)
|
||||||
page_data = await paging_data(db, dict_type_select)
|
page_data = await paging_data(db, dict_type_select)
|
||||||
@@ -56,7 +56,9 @@ async def create_dict_type(obj: CreateDictTypeParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypeParam) -> ResponseModel:
|
async def update_dict_type(
|
||||||
|
pk: Annotated[int, Path(description='字典类型 ID')], obj: UpdateDictTypeParam
|
||||||
|
) -> ResponseModel:
|
||||||
count = await dict_type_service.update(pk=pk, obj=obj)
|
count = await dict_type_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -65,13 +67,13 @@ async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypePar
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除字典类型',
|
summary='批量删除字典类型',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:dict:type:del')),
|
Depends(RequestPermission('sys:dict:type:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_dict_type(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_dict_type(pk: Annotated[list[int], Query(description='字典类型 ID 列表')]) -> ResponseModel:
|
||||||
count = await dict_type_service.delete(pk=pk)
|
count = await dict_type_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -10,27 +10,26 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
|
|||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/sidebar', summary='获取用户菜单展示树', dependencies=[DependsJwtAuth])
|
@router.get('/sidebar', summary='获取用户侧边栏', dependencies=[DependsJwtAuth])
|
||||||
async def get_user_sidebar_tree(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||||
menu = await menu_service.get_user_menu_tree(request=request)
|
menu = await menu_service.get_user_menu_tree(request=request)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_menu(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetMenuDetail]:
|
async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseSchemaModel[GetMenuDetail]:
|
||||||
menu = await menu_service.get(pk=pk)
|
data = await menu_service.get(pk=pk)
|
||||||
data = GetMenuDetail(**select_as_dict(menu))
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取所有菜单展示树', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取所有菜单展示树', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_menus(
|
async def get_all_menus(
|
||||||
title: Annotated[str | None, Query()] = None, status: Annotated[int | None, Query()] = None
|
title: Annotated[str | None, Query(description='菜单标题')] = None,
|
||||||
|
status: Annotated[int | None, Query(description='状体')] = None,
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||||
menu = await menu_service.get_menu_tree(title=title, status=status)
|
menu = await menu_service.get_menu_tree(title=title, status=status)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
@@ -57,7 +56,7 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> ResponseModel:
|
async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam) -> ResponseModel:
|
||||||
count = await menu_service.update(pk=pk, obj=obj)
|
count = await menu_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -72,7 +71,7 @@ async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> Re
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_menu(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def delete_menu(request: Request, pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
|
||||||
count = await menu_service.delete(request=request, pk=pk)
|
count = await menu_service.delete(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import io
|
||||||
|
import os.path
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
|
from fastapi.params import Query
|
||||||
|
from starlette.responses import StreamingResponse
|
||||||
|
|
||||||
|
from backend.common.exception import errors
|
||||||
|
from backend.common.response.response_schema import ResponseModel, response_base
|
||||||
|
from backend.common.security.permission import RequestPermission
|
||||||
|
from backend.common.security.rbac import DependsRBAC
|
||||||
|
from backend.core.path_conf import PLUGIN_DIR
|
||||||
|
from backend.plugin.tools import install_requirements_async
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'/install',
|
||||||
|
summary='安装插件',
|
||||||
|
description='需使用插件 zip 压缩包进行安装',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:plugin:install')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def install_plugin(file: Annotated[UploadFile, File()]) -> ResponseModel:
|
||||||
|
contents = await file.read()
|
||||||
|
file_bytes = io.BytesIO(contents)
|
||||||
|
if not zipfile.is_zipfile(file_bytes):
|
||||||
|
raise errors.ForbiddenError(msg='插件压缩包格式非法')
|
||||||
|
with zipfile.ZipFile(file_bytes) as zf:
|
||||||
|
# 校验压缩包
|
||||||
|
plugin_dir_in_zip = f'{file.filename[:-4]}/backend/plugin/'
|
||||||
|
members_in_plugin_dir = [name for name in zf.namelist() if name.startswith(plugin_dir_in_zip)]
|
||||||
|
if not members_in_plugin_dir:
|
||||||
|
raise errors.ForbiddenError(msg='插件压缩包内容非法')
|
||||||
|
plugin_name = members_in_plugin_dir[1].replace(plugin_dir_in_zip, '').replace('/', '')
|
||||||
|
if (
|
||||||
|
len(members_in_plugin_dir) <= 3
|
||||||
|
or f'{plugin_dir_in_zip}{plugin_name}/plugin.toml' not in members_in_plugin_dir
|
||||||
|
or f'{plugin_dir_in_zip}{plugin_name}/README.md' not in members_in_plugin_dir
|
||||||
|
):
|
||||||
|
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
|
||||||
|
|
||||||
|
# 插件是否可安装
|
||||||
|
full_plugin_path = os.path.join(PLUGIN_DIR, plugin_name)
|
||||||
|
if os.path.exists(full_plugin_path):
|
||||||
|
raise errors.ForbiddenError(msg='此插件已安装')
|
||||||
|
else:
|
||||||
|
os.makedirs(full_plugin_path, exist_ok=True)
|
||||||
|
|
||||||
|
# 解压(安装)
|
||||||
|
members = []
|
||||||
|
for member in zf.infolist():
|
||||||
|
if member.filename.startswith(plugin_dir_in_zip):
|
||||||
|
new_filename = member.filename.replace(plugin_dir_in_zip, '')
|
||||||
|
if new_filename:
|
||||||
|
member.filename = new_filename
|
||||||
|
members.append(member)
|
||||||
|
zf.extractall(PLUGIN_DIR, members)
|
||||||
|
if os.path.exists(os.path.join(full_plugin_path, 'requirements.txt')):
|
||||||
|
await install_requirements_async()
|
||||||
|
|
||||||
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'/zip',
|
||||||
|
summary='打包插件',
|
||||||
|
dependencies=[
|
||||||
|
Depends(RequestPermission('sys:plugin:zip')),
|
||||||
|
DependsRBAC,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def build_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> StreamingResponse:
|
||||||
|
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
||||||
|
if not os.path.exists(plugin_dir):
|
||||||
|
raise errors.ForbiddenError(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)
|
||||||
|
zf.write(file_path, arcname)
|
||||||
|
|
||||||
|
bio.seek(0)
|
||||||
|
return StreamingResponse(
|
||||||
|
bio,
|
||||||
|
media_type='application/x-zip-compressed',
|
||||||
|
headers={'Content-Disposition': f'attachment; filename={plugin}.zip'},
|
||||||
|
)
|
||||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, Path, Query, Request
|
|||||||
from backend.app.admin.schema.role import (
|
from backend.app.admin.schema.role import (
|
||||||
CreateRoleParam,
|
CreateRoleParam,
|
||||||
GetRoleDetail,
|
GetRoleDetail,
|
||||||
|
GetRoleWithRelationDetail,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
UpdateRoleParam,
|
UpdateRoleParam,
|
||||||
UpdateRoleRuleParam,
|
UpdateRoleRuleParam,
|
||||||
@@ -20,47 +21,49 @@ from backend.common.security.jwt import DependsJwtAuth
|
|||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession
|
||||||
from backend.utils.serializers import select_as_dict, select_list_serialize
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
|
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||||
roles = await role_service.get_all()
|
data = await role_service.get_all()
|
||||||
data = select_list_serialize(roles)
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||||
async def get_user_all_roles(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
async def get_user_all_roles(
|
||||||
roles = await role_service.get_by_user(pk=pk)
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
data = select_list_serialize(roles)
|
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||||
|
data = await role_service.get_by_user(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
||||||
async def get_role_all_menus(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
async def get_role_all_menus(
|
||||||
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
|
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||||
menu = await menu_service.get_role_menu_tree(pk=pk)
|
menu = await menu_service.get_role_menu_tree(pk=pk)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth])
|
||||||
async def get_role_all_rules(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[int]]:
|
async def get_role_all_rules(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
|
||||||
rule = await data_rule_service.get_role_rules(pk=pk)
|
rule = await data_rule_service.get_role_rules(pk=pk)
|
||||||
return response_base.success(data=rule)
|
return response_base.success(data=rule)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_role(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetRoleDetail]:
|
async def get_role(
|
||||||
role = await role_service.get(pk=pk)
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
data = GetRoleDetail(**select_as_dict(role))
|
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
|
||||||
|
data = await role_service.get(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有角色',
|
summary='分页获取所有角色',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
@@ -68,8 +71,8 @@ async def get_role(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetRole
|
|||||||
)
|
)
|
||||||
async def get_pagination_roles(
|
async def get_pagination_roles(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
name: Annotated[str | None, Query()] = None,
|
name: Annotated[str | None, Query(description='角色名称')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
|
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
|
||||||
role_select = await role_service.get_select(name=name, status=status)
|
role_select = await role_service.get_select(name=name, status=status)
|
||||||
page_data = await paging_data(db, role_select)
|
page_data = await paging_data(db, role_select)
|
||||||
@@ -97,7 +100,7 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> ResponseModel:
|
async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam) -> ResponseModel:
|
||||||
count = await role_service.update(pk=pk, obj=obj)
|
count = await role_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -113,7 +116,7 @@ async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> Re
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_menus(
|
async def update_role_menus(
|
||||||
request: Request, pk: Annotated[int, Path(...)], menu_ids: UpdateRoleMenuParam
|
request: Request, pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids)
|
count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@@ -130,7 +133,7 @@ async def update_role_menus(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_role_rules(
|
async def update_role_rules(
|
||||||
request: Request, pk: Annotated[int, Path(...)], rule_ids: UpdateRoleRuleParam
|
request: Request, pk: Annotated[int, Path(description='角色 ID')], rule_ids: UpdateRoleRuleParam
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
count = await role_service.update_role_rule(request=request, pk=pk, rule_ids=rule_ids)
|
count = await role_service.update_role_rule(request=request, pk=pk, rule_ids=rule_ids)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@@ -140,13 +143,13 @@ async def update_role_rules(
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
'',
|
'',
|
||||||
summary='(批量)删除角色',
|
summary='批量删除角色',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:del')),
|
Depends(RequestPermission('sys:role:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_role(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
|
async def delete_role(request: Request, pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
|
||||||
count = await role_service.delete(request=request, pk=pk)
|
count = await role_service.delete(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, Path, Query, Request
|
|||||||
from backend.app.admin.schema.token import GetTokenDetail, KickOutToken
|
from backend.app.admin.schema.token import GetTokenDetail, KickOutToken
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, superuser_verify
|
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
@@ -19,10 +19,28 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
|
||||||
async def get_tokens(username: Annotated[str | None, Query()] = None) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
async def get_tokens(
|
||||||
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
|
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
||||||
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
||||||
token_online = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
|
online_clients = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
|
||||||
data = []
|
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:
|
for key in token_keys:
|
||||||
token = await redis_client.get(key)
|
token = await redis_client.get(key)
|
||||||
token_payload = jwt_decode(token)
|
token_payload = jwt_decode(token)
|
||||||
@@ -36,31 +54,15 @@ async def get_tokens(username: Annotated[str | None, Query()] = None) -> Respons
|
|||||||
os='未知',
|
os='未知',
|
||||||
browser='未知',
|
browser='未知',
|
||||||
device='未知',
|
device='未知',
|
||||||
status=StatusType.disable if session_uuid not in token_online else StatusType.enable,
|
status=StatusType.enable if session_uuid in online_clients else StatusType.disable,
|
||||||
last_login_time='未知',
|
last_login_time='未知',
|
||||||
expire_time=token_payload.expire_time,
|
expire_time=token_payload.expire_time,
|
||||||
)
|
)
|
||||||
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
|
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
|
||||||
if extra_info:
|
if extra_info:
|
||||||
|
|
||||||
def append_token_detail():
|
|
||||||
data.append(
|
|
||||||
token_detail.model_copy(
|
|
||||||
update={
|
|
||||||
'username': extra_info.get('username'),
|
|
||||||
'nickname': extra_info.get('nickname'),
|
|
||||||
'ip': extra_info.get('ip'),
|
|
||||||
'os': extra_info.get('os'),
|
|
||||||
'browser': extra_info.get('browser'),
|
|
||||||
'device': extra_info.get('device'),
|
|
||||||
'last_login_time': extra_info.get('last_login_time'),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
extra_info = json.loads(extra_info)
|
extra_info = json.loads(extra_info)
|
||||||
if extra_info.get('login_type') != 'swagger':
|
if extra_info.get('login_type') != 'swagger':
|
||||||
if username:
|
if username is not None:
|
||||||
if username == extra_info.get('username'):
|
if username == extra_info.get('username'):
|
||||||
append_token_detail()
|
append_token_detail()
|
||||||
else:
|
else:
|
||||||
@@ -78,7 +80,9 @@ async def get_tokens(username: Annotated[str | None, Query()] = None) -> Respons
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def kick_out(request: Request, pk: Annotated[int, Path(...)], session_uuid: KickOutToken) -> ResponseModel:
|
async def kick_out(
|
||||||
|
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: KickOutToken
|
||||||
|
) -> ResponseModel:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{pk}:{session_uuid}')
|
await revoke_token(str(pk), obj.session_uuid)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from fastapi import APIRouter, Depends, Path, Query, Request
|
|||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
AvatarParam,
|
AvatarParam,
|
||||||
GetCurrentUserInfoDetail,
|
GetCurrentUserInfoWithRelationDetail,
|
||||||
GetUserInfoDetail,
|
GetUserInfoWithRelationDetail,
|
||||||
RegisterUserParam,
|
RegisterUserParam,
|
||||||
ResetPasswordParam,
|
ResetPasswordParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
@@ -21,7 +21,6 @@ from backend.common.security.jwt import DependsJwtAuth
|
|||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.database.db import CurrentSession
|
from backend.database.db import CurrentSession
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -33,10 +32,9 @@ async def register_user(obj: RegisterUserParam) -> ResponseModel:
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
||||||
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoDetail]:
|
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||||
await user_service.add(request=request, obj=obj)
|
await user_service.add(request=request, obj=obj)
|
||||||
current_user = await user_service.get_userinfo(username=obj.username)
|
data = await user_service.get_userinfo(username=obj.username)
|
||||||
data = GetUserInfoDetail(**select_as_dict(current_user))
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,21 +46,24 @@ async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseM
|
|||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'})
|
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoDetail]:
|
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
|
||||||
data = GetCurrentUserInfoDetail(**request.user.model_dump())
|
data = request.user.model_dump()
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
|
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def get_user(username: Annotated[str, Path(...)]) -> ResponseSchemaModel[GetUserInfoDetail]:
|
async def get_user(
|
||||||
current_user = await user_service.get_userinfo(username=username)
|
username: Annotated[str, Path(description='用户名')],
|
||||||
data = GetUserInfoDetail(**select_as_dict(current_user))
|
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||||
|
data = await user_service.get_userinfo(username=username)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
||||||
async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel:
|
async def update_user(
|
||||||
|
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserParam
|
||||||
|
) -> ResponseModel:
|
||||||
count = await user_service.update(request=request, username=username, obj=obj)
|
count = await user_service.update(request=request, username=username, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -78,14 +79,16 @@ async def update_user(request: Request, username: Annotated[str, Path(...)], obj
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_user_role(
|
async def update_user_role(
|
||||||
request: Request, username: Annotated[str, Path(...)], obj: UpdateUserRoleParam
|
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserRoleParam
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
await user_service.update_roles(request=request, username=username, obj=obj)
|
await user_service.update_roles(request=request, username=username, obj=obj)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtAuth])
|
@router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtAuth])
|
||||||
async def update_avatar(request: Request, username: Annotated[str, Path(...)], avatar: AvatarParam) -> ResponseModel:
|
async def update_avatar(
|
||||||
|
request: Request, username: Annotated[str, Path(description='用户名')], avatar: AvatarParam
|
||||||
|
) -> ResponseModel:
|
||||||
count = await user_service.update_avatar(request=request, username=username, avatar=avatar)
|
count = await user_service.update_avatar(request=request, username=username, avatar=avatar)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -94,7 +97,7 @@ async def update_avatar(request: Request, username: Annotated[str, Path(...)], a
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'',
|
'',
|
||||||
summary='(模糊条件)分页获取所有用户',
|
summary='分页获取所有用户',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
DependsJwtAuth,
|
DependsJwtAuth,
|
||||||
DependsPagination,
|
DependsPagination,
|
||||||
@@ -102,18 +105,18 @@ async def update_avatar(request: Request, username: Annotated[str, Path(...)], a
|
|||||||
)
|
)
|
||||||
async def get_pagination_users(
|
async def get_pagination_users(
|
||||||
db: CurrentSession,
|
db: CurrentSession,
|
||||||
dept: Annotated[int | None, Query()] = None,
|
dept: Annotated[int | None, Query(description='部门 ID')] = None,
|
||||||
username: Annotated[str | None, Query()] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
phone: Annotated[str | None, Query()] = None,
|
phone: Annotated[str | None, Query(description='手机号')] = None,
|
||||||
status: Annotated[int | None, Query()] = None,
|
status: Annotated[int | None, Query(description='状态')] = None,
|
||||||
) -> ResponseSchemaModel[PageData[GetUserInfoDetail]]:
|
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
|
||||||
user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status)
|
user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status)
|
||||||
page_data = await paging_data(db, user_select)
|
page_data = await paging_data(db, user_select)
|
||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
|
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
|
||||||
async def super_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||||
count = await user_service.update_permission(request=request, pk=pk)
|
count = await user_service.update_permission(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -121,7 +124,7 @@ async def super_set(request: Request, pk: Annotated[int, Path(...)]) -> Response
|
|||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/staff', summary='修改用户后台登录权限', dependencies=[DependsRBAC])
|
@router.put('/{pk}/staff', summary='修改用户后台登录权限', dependencies=[DependsRBAC])
|
||||||
async def staff_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def staff_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||||
count = await user_service.update_staff(request=request, pk=pk)
|
count = await user_service.update_staff(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -129,15 +132,15 @@ async def staff_set(request: Request, pk: Annotated[int, Path(...)]) -> Response
|
|||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsRBAC])
|
@router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsRBAC])
|
||||||
async def status_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def status_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||||
count = await user_service.update_status(request=request, pk=pk)
|
count = await user_service.update_status(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/multi', summary='修改用户多点登录状态', dependencies=[DependsRBAC])
|
@router.put('/{pk}/multi', summary='修改用户多端登录状态', dependencies=[DependsRBAC])
|
||||||
async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def multi_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||||
count = await user_service.update_multi_login(request=request, pk=pk)
|
count = await user_service.update_multi_login(request=request, pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -153,7 +156,7 @@ async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> Response
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_user(username: Annotated[str, Path(...)]) -> ResponseModel:
|
async def delete_user(username: Annotated[str, Path(description='用户名')]) -> ResponseModel:
|
||||||
count = await user_service.delete(username=username)
|
count = await user_service.delete(username=username)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
+12
-15
@@ -4,39 +4,36 @@ from functools import lru_cache
|
|||||||
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
from backend.core.path_conf import BasePath
|
from backend.core.path_conf import BASE_PATH
|
||||||
|
|
||||||
|
|
||||||
class AdminSettings(BaseSettings):
|
class AdminSettings(BaseSettings):
|
||||||
"""Admin Settings"""
|
"""Admin 配置"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
|
model_config = SettingsConfigDict(env_file=f'{BASE_PATH}/.env', env_file_encoding='utf-8', extra='ignore')
|
||||||
|
|
||||||
# OAuth2:https://github.com/fastapi-practices/fastapi_oauth20
|
# .env OAuth2
|
||||||
# GitHub
|
|
||||||
OAUTH2_GITHUB_CLIENT_ID: str
|
OAUTH2_GITHUB_CLIENT_ID: str
|
||||||
OAUTH2_GITHUB_CLIENT_SECRET: str
|
OAUTH2_GITHUB_CLIENT_SECRET: str
|
||||||
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
|
|
||||||
|
|
||||||
# Linux Do
|
|
||||||
OAUTH2_LINUX_DO_CLIENT_ID: str
|
OAUTH2_LINUX_DO_CLIENT_ID: str
|
||||||
OAUTH2_LINUX_DO_CLIENT_SECRET: str
|
OAUTH2_LINUX_DO_CLIENT_SECRET: str
|
||||||
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
|
|
||||||
|
|
||||||
# Front-end redirect address
|
# OAuth2
|
||||||
|
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
|
||||||
|
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
|
||||||
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
|
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
|
||||||
|
|
||||||
# Captcha
|
# 验证码
|
||||||
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
|
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
|
||||||
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒
|
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
|
||||||
|
|
||||||
# Config
|
# 参数配置
|
||||||
CONFIG_BUILT_IN_TYPES: list = ['website', 'protocol', 'policy']
|
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_admin_settings() -> AdminSettings:
|
def get_admin_settings() -> AdminSettings:
|
||||||
"""获取 admin 配置"""
|
"""获取 admin 参数配置"""
|
||||||
return AdminSettings()
|
return AdminSettings()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,57 +12,55 @@ from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam
|
|||||||
|
|
||||||
|
|
||||||
class CRUDConfig(CRUDPlus[Config]):
|
class CRUDConfig(CRUDPlus[Config]):
|
||||||
|
"""系统参数参数配置数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> Config | None:
|
async def get(self, db: AsyncSession, pk: int) -> Config | None:
|
||||||
"""
|
"""
|
||||||
获取系统参数配置
|
获取参数配置详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 参数配置 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, id=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES)
|
return await self.select_model_by_column(db, id=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES)
|
||||||
|
|
||||||
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
|
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
|
||||||
"""
|
"""
|
||||||
通过 type 获取内置系统配置
|
通过类型获取参数配置
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param type:
|
:param type: 参数配置类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models(db, type=type)
|
return await self.select_models(db, type=type)
|
||||||
|
|
||||||
async def get_by_key_and_type(self, db: AsyncSession, key: str, type: str) -> Config | None:
|
async def get_by_key_and_type(self, db: AsyncSession, key: str, type: str) -> Config | None:
|
||||||
"""
|
"""
|
||||||
通过 name 和 type 获取内置系统配置
|
通过键名和类型获取参数配置
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param key:
|
:param key: 参数配置键名
|
||||||
:param type:
|
:param type: 参数配置类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, key=key, type=type)
|
return await self.select_model_by_column(db, key=key, type=type)
|
||||||
|
|
||||||
async def get_by_key(self, db: AsyncSession, key: str, built_in: bool = False) -> Config | None:
|
async def get_by_key(self, db: AsyncSession, key: str) -> Config | None:
|
||||||
"""
|
"""
|
||||||
通过 key 获取系统配置参数
|
通过键名获取参数配置
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param key:
|
:param key: 参数配置键名
|
||||||
:param built_in:
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {'key': key}
|
return await self.select_model_by_column(db, key=key)
|
||||||
if not built_in:
|
|
||||||
filters.update({'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES})
|
|
||||||
return await self.select_model_by_column(db, **filters)
|
|
||||||
|
|
||||||
async def get_list(self, name: str = None, type: str = None) -> Select:
|
async def get_list(self, name: str | None = None, type: str | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取系统参数配置列表
|
获取参数配置列表
|
||||||
|
|
||||||
:param name:
|
:param name: 参数配置名称
|
||||||
:param type:
|
:param type: 参数配置类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES}
|
filters = {'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES}
|
||||||
@@ -72,33 +70,33 @@ class CRUDConfig(CRUDPlus[Config]):
|
|||||||
filters.update(type__like=f'%{type}%')
|
filters.update(type__like=f'%{type}%')
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
return await self.select_order('created_time', 'desc', **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateConfigParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateConfigParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建 Config
|
创建参数配置
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建参数配置参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateConfigParam) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateConfigParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新 Config
|
更新参数配置
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 参数配置 ID
|
||||||
:param obj_in:
|
:param obj: 更新参数配置参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除 Config
|
删除参数配置
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 参数配置 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(
|
return await self.delete_model_by_column(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import Select, desc, select
|
from sqlalchemy import Select, and_, desc, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload
|
from sqlalchemy.orm import noload
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
@@ -12,76 +12,82 @@ from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRu
|
|||||||
|
|
||||||
|
|
||||||
class CRUDDataRule(CRUDPlus[DataRule]):
|
class CRUDDataRule(CRUDPlus[DataRule]):
|
||||||
|
"""数据权限规则数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
|
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
|
||||||
"""
|
"""
|
||||||
获取数据权限规则
|
获取规则详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 规则 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, pk)
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
async def get_list(self, name: str = None) -> Select:
|
async def get_list(self, name: str | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取数据权限规则列表
|
获取规则列表
|
||||||
|
|
||||||
|
:param name: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(noload(self.model.roles)).order_by(desc(self.model.created_time))
|
stmt = select(self.model).options(noload(self.model.roles)).order_by(desc(self.model.created_time))
|
||||||
where_list = []
|
|
||||||
|
filters = []
|
||||||
if name is not None:
|
if name is not None:
|
||||||
where_list.append(self.model.name.like(f'%{name}%'))
|
filters.append(self.model.name.like(f'%{name}%'))
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(*where_list)
|
if filters:
|
||||||
|
stmt = stmt.where(and_(*filters))
|
||||||
|
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str):
|
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取数据权限规则
|
通过名称获取规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name)
|
return await self.select_model_by_column(db, name=name)
|
||||||
|
|
||||||
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
|
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
|
||||||
"""
|
"""
|
||||||
获取所有数据权限规则
|
获取所有规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDataRuleParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDataRuleParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建数据权限规则
|
创建规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建规则参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDataRuleParam) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据权限规则
|
更新规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 规则 ID
|
||||||
:param obj_in:
|
:param obj: 更新规则参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除数据权限规则
|
删除规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
||||||
|
|||||||
@@ -7,42 +7,49 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.app.admin.model import Dept, User
|
from backend.app.admin.model import Dept
|
||||||
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
|
||||||
|
|
||||||
|
|
||||||
class CRUDDept(CRUDPlus[Dept]):
|
class CRUDDept(CRUDPlus[Dept]):
|
||||||
|
"""部门数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
获取部门
|
获取部门详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, id=dept_id, del_flag=0)
|
return await self.select_model_by_column(db, id=dept_id, del_flag=0)
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取 API
|
通过名称获取部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 部门名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name, del_flag=0)
|
return await self.select_model_by_column(db, name=name, del_flag=0)
|
||||||
|
|
||||||
async def get_all(
|
async def get_all(
|
||||||
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: int = None
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
name: str | None = None,
|
||||||
|
leader: str | None = None,
|
||||||
|
phone: str | None = None,
|
||||||
|
status: int | None = None,
|
||||||
) -> Sequence[Dept]:
|
) -> Sequence[Dept]:
|
||||||
"""
|
"""
|
||||||
获取所有部门
|
获取所有部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 部门名称
|
||||||
:param leader:
|
:param leader: 负责人
|
||||||
:param phone:
|
:param phone: 联系电话
|
||||||
:param status:
|
:param status: 部门状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {'del_flag__eq': 0}
|
filters = {'del_flag__eq': 0}
|
||||||
@@ -56,62 +63,60 @@ class CRUDDept(CRUDPlus[Dept]):
|
|||||||
filters.update(status=status)
|
filters.update(status=status)
|
||||||
return await self.select_models_order(db, sort_columns='sort', **filters)
|
return await self.select_models_order(db, sort_columns='sort', **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDeptParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建部门
|
创建部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建部门参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, dept_id: int, obj_in: UpdateDeptParam) -> int:
|
async def update(self, db: AsyncSession, dept_id: int, obj: UpdateDeptParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新部门
|
更新部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:param obj_in:
|
:param obj: 更新部门参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, dept_id, obj_in)
|
return await self.update_model(db, dept_id, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, dept_id: int) -> int:
|
async def delete(self, db: AsyncSession, dept_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除部门
|
删除部门
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
|
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> list[User]:
|
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
|
||||||
"""
|
"""
|
||||||
获取关联
|
获取部门及关联数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
|
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
user_relation = result.scalars().first()
|
return result.scalars().first()
|
||||||
return user_relation.users
|
|
||||||
|
|
||||||
async def get_children(self, db: AsyncSession, dept_id: int) -> list[Dept]:
|
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
|
||||||
"""
|
"""
|
||||||
获取子部门
|
获取子部门列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param dept_id:
|
:param dept_id: 部门 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == dept_id)
|
stmt = select(self.model).where(self.model.parent_id == dept_id, self.model.del_flag == 0)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
dept = result.scalars().first()
|
return result.scalars().all()
|
||||||
return dept.children
|
|
||||||
|
|
||||||
|
|
||||||
dept_dao: CRUDDept = CRUDDept(Dept)
|
dept_dao: CRUDDept = CRUDDept(Dept)
|
||||||
|
|||||||
@@ -10,84 +10,89 @@ from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDa
|
|||||||
|
|
||||||
|
|
||||||
class CRUDDictData(CRUDPlus[DictData]):
|
class CRUDDictData(CRUDPlus[DictData]):
|
||||||
|
"""字典数据数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> DictData | None:
|
async def get(self, db: AsyncSession, pk: int) -> DictData | None:
|
||||||
"""
|
"""
|
||||||
获取字典数据
|
获取字典数据详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典数据 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, pk)
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
async def get_list(self, label: str = None, value: str = None, status: int = None) -> Select:
|
async def get_list(self, label: str | None = None, value: str | None = None, status: int | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取所有字典数据
|
获取字典数据列表
|
||||||
|
|
||||||
:param label:
|
:param label: 字典数据标签
|
||||||
:param value:
|
:param value: 字典数据键值
|
||||||
:param status:
|
:param status: 字典状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
|
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
|
||||||
where_list = []
|
|
||||||
|
filters = []
|
||||||
if label is not None:
|
if label is not None:
|
||||||
where_list.append(self.model.label.like(f'%{label}%'))
|
filters.append(self.model.label.like(f'%{label}%'))
|
||||||
if value is not None:
|
if value is not None:
|
||||||
where_list.append(self.model.value.like(f'%{value}%'))
|
filters.append(self.model.value.like(f'%{value}%'))
|
||||||
if status is not None:
|
if status is not None:
|
||||||
where_list.append(self.model.status == status)
|
filters.append(self.model.status == status)
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(and_(*where_list))
|
if filters:
|
||||||
|
stmt = stmt.where(and_(*filters))
|
||||||
|
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
|
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
|
||||||
"""
|
"""
|
||||||
通过 label 获取字典数据
|
通过标签获取字典数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param label:
|
:param label: 字典标签
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, label=label)
|
return await self.select_model_by_column(db, label=label)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDictDataParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDictDataParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建数据字典
|
创建字典数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建字典数据参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictDataParam) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateDictDataParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新数据字典
|
更新字典数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典数据 ID
|
||||||
:param obj_in:
|
:param obj: 更新字典数据参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除字典数据
|
删除字典数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典数据 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
|
async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
|
||||||
"""
|
"""
|
||||||
获取字典数据和类型
|
获取字典数据及关联数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典数据 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk)
|
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk)
|
||||||
|
|||||||
@@ -9,23 +9,25 @@ from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTy
|
|||||||
|
|
||||||
|
|
||||||
class CRUDDictType(CRUDPlus[DictType]):
|
class CRUDDictType(CRUDPlus[DictType]):
|
||||||
|
"""字典类型数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> DictType | None:
|
async def get(self, db: AsyncSession, pk: int) -> DictType | None:
|
||||||
"""
|
"""
|
||||||
获取字典类型
|
获取字典类型详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典类型 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, pk)
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
async def get_list(self, *, name: str = None, code: str = None, status: int = None) -> Select:
|
async def get_list(self, *, name: str | None = None, code: str | None = None, status: int | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取所有字典类型
|
获取字典类型列表
|
||||||
|
|
||||||
:param name:
|
:param name: 字典类型名称
|
||||||
:param code:
|
:param code: 字典类型编码
|
||||||
:param status:
|
:param status: 字典状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -39,41 +41,41 @@ class CRUDDictType(CRUDPlus[DictType]):
|
|||||||
|
|
||||||
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
|
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
|
||||||
"""
|
"""
|
||||||
通过 code 获取字典类型
|
通过编码获取字典类型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param code:
|
:param code: 字典编码
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, code=code)
|
return await self.select_model_by_column(db, code=code)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateDictTypeParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateDictTypeParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建字典类型
|
创建字典类型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建字典类型参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictTypeParam) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateDictTypeParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新字典类型
|
更新字典类型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典类型 ID
|
||||||
:param obj_in:
|
:param obj: 更新字典类型参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除字典类型
|
删除字典类型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 字典类型 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
|
|||||||
|
|
||||||
|
|
||||||
class CRUDLoginLog(CRUDPlus[LoginLog]):
|
class CRUDLoginLog(CRUDPlus[LoginLog]):
|
||||||
|
"""登录日志数据库操作类"""
|
||||||
|
|
||||||
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取登录日志列表
|
获取登录日志列表
|
||||||
|
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:param status:
|
:param status: 登录状态
|
||||||
:param ip:
|
:param ip: IP 地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -27,31 +29,31 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
|
|||||||
filters.update(ip__like=f'%{ip}%')
|
filters.update(ip__like=f'%{ip}%')
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
return await self.select_order('created_time', 'desc', **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建登录日志
|
创建登录日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建登录日志参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in, commit=True)
|
await self.create_model(db, obj, commit=True)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除登录日志
|
删除登录日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 登录日志 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
||||||
|
|
||||||
async def delete_all(self, db: AsyncSession) -> int:
|
async def delete_all(self, db: AsyncSession) -> int:
|
||||||
"""
|
"""
|
||||||
删除所有登录日志
|
删除所有日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True)
|
return await self.delete_model_by_column(db, allow_multiple=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import and_, asc, select
|
from sqlalchemy import and_, asc, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
@@ -11,33 +12,35 @@ from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
|
|||||||
|
|
||||||
|
|
||||||
class CRUDMenu(CRUDPlus[Menu]):
|
class CRUDMenu(CRUDPlus[Menu]):
|
||||||
async def get(self, db, menu_id: int) -> Menu | None:
|
"""菜单数据库操作类"""
|
||||||
"""
|
|
||||||
获取菜单
|
|
||||||
|
|
||||||
:param db:
|
async def get(self, db: AsyncSession, menu_id: int) -> Menu | None:
|
||||||
:param menu_id:
|
"""
|
||||||
|
获取菜单详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, menu_id)
|
return await self.select_model(db, menu_id)
|
||||||
|
|
||||||
async def get_by_title(self, db, title: str) -> Menu | None:
|
async def get_by_title(self, db: AsyncSession, title: str) -> Menu | None:
|
||||||
"""
|
"""
|
||||||
通过 title 获取菜单
|
通过标题获取菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param title:
|
:param title: 菜单标题
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, title=title, menu_type__ne=2)
|
return await self.select_model_by_column(db, title=title, menu_type__ne=2)
|
||||||
|
|
||||||
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
|
async def get_all(self, db: AsyncSession, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
|
||||||
"""
|
"""
|
||||||
获取所有菜单
|
获取菜单列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param title:
|
:param title: 菜单标题
|
||||||
:param status:
|
:param status: 菜单状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -47,60 +50,60 @@ class CRUDMenu(CRUDPlus[Menu]):
|
|||||||
filters.update(status=status)
|
filters.update(status=status)
|
||||||
return await self.select_models_order(db, 'sort', **filters)
|
return await self.select_models_order(db, 'sort', **filters)
|
||||||
|
|
||||||
async def get_role_menus(self, db, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
|
async def get_role_menus(self, db: AsyncSession, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
|
||||||
"""
|
"""
|
||||||
获取角色菜单
|
获取角色菜单列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param superuser:
|
:param superuser: 是否超级管理员
|
||||||
:param menu_ids:
|
:param menu_ids: 菜单 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).order_by(asc(self.model.sort))
|
stmt = select(self.model).order_by(asc(self.model.sort))
|
||||||
where_list = [self.model.menu_type.in_([0, 1])]
|
filters = [self.model.menu_type.in_([0, 1])]
|
||||||
if not superuser:
|
if not superuser:
|
||||||
where_list.append(self.model.id.in_(menu_ids))
|
filters.append(self.model.id.in_(menu_ids))
|
||||||
stmt = stmt.where(and_(*where_list))
|
stmt = stmt.where(and_(*filters))
|
||||||
menu = await db.execute(stmt)
|
menu = await db.execute(stmt)
|
||||||
return menu.scalars().all()
|
return menu.scalars().all()
|
||||||
|
|
||||||
async def create(self, db, obj_in: CreateMenuParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建菜单
|
创建菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建菜单参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db, menu_id: int, obj_in: UpdateMenuParam) -> int:
|
async def update(self, db: AsyncSession, menu_id: int, obj: UpdateMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新菜单
|
更新菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param menu_id:
|
:param menu_id: 菜单 ID
|
||||||
:param obj_in:
|
:param obj: 更新菜单参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, menu_id, obj_in)
|
return await self.update_model(db, menu_id, obj)
|
||||||
|
|
||||||
async def delete(self, db, menu_id: int) -> int:
|
async def delete(self, db: AsyncSession, menu_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除菜单
|
删除菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param menu_id:
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, menu_id)
|
return await self.delete_model(db, menu_id)
|
||||||
|
|
||||||
async def get_children(self, db, menu_id: int) -> list[Menu]:
|
async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None]:
|
||||||
"""
|
"""
|
||||||
获取子菜单
|
获取子菜单列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param menu_id:
|
:param menu_id: 菜单 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
|
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
|
||||||
|
|||||||
@@ -9,49 +9,51 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
|
|||||||
|
|
||||||
|
|
||||||
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||||
|
"""操作日志数据库操作类"""
|
||||||
|
|
||||||
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取操作日志列表
|
获取操作日志列表
|
||||||
|
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:param status:
|
:param status: 操作状态
|
||||||
:param ip:
|
:param ip: IP 地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
if username is not None:
|
if username is not None:
|
||||||
filters.update(username=f'%{username}%')
|
filters.update(username__like=f'%{username}%')
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.update(status=status)
|
filters.update(status=status)
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
filters.update(ip=f'%{ip}%')
|
filters.update(ip__like=f'%{ip}%')
|
||||||
return await self.select_order('created_time', 'desc', **filters)
|
return await self.select_order('created_time', 'desc', **filters)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateOperaLogParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建操作日志
|
创建操作日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建操作日志参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除操作日志
|
删除操作日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 操作日志 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
|
||||||
|
|
||||||
async def delete_all(self, db: AsyncSession) -> int:
|
async def delete_all(self, db: AsyncSession) -> int:
|
||||||
"""
|
"""
|
||||||
删除所有操作日志
|
删除所有日志
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True)
|
return await self.delete_model_by_column(db, allow_multiple=True)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import Select, desc, select
|
from sqlalchemy import Select, and_, desc, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy.orm import noload, selectinload
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
@@ -16,22 +17,24 @@ from backend.app.admin.schema.role import (
|
|||||||
|
|
||||||
|
|
||||||
class CRUDRole(CRUDPlus[Role]):
|
class CRUDRole(CRUDPlus[Role]):
|
||||||
async def get(self, db, role_id: int) -> Role | None:
|
"""角色数据库操作类"""
|
||||||
"""
|
|
||||||
获取角色
|
|
||||||
|
|
||||||
:param db:
|
async def get(self, db: AsyncSession, role_id: int) -> Role | None:
|
||||||
:param role_id:
|
"""
|
||||||
|
获取角色详情
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param role_id: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, role_id)
|
return await self.select_model(db, role_id)
|
||||||
|
|
||||||
async def get_with_relation(self, db, role_id: int) -> Role | None:
|
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
|
||||||
"""
|
"""
|
||||||
获取角色和菜单
|
获取角色及关联数据
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
stmt = (
|
||||||
@@ -42,33 +45,33 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
role = await db.execute(stmt)
|
role = await db.execute(stmt)
|
||||||
return role.scalars().first()
|
return role.scalars().first()
|
||||||
|
|
||||||
async def get_all(self, db) -> Sequence[Role]:
|
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
|
||||||
"""
|
"""
|
||||||
获取所有角色
|
获取所有角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def get_by_user(self, db, user_id: int) -> Sequence[Role]:
|
async def get_by_user(self, db: AsyncSession, user_id: int) -> Sequence[Role]:
|
||||||
"""
|
"""
|
||||||
获取用户所有角色
|
获取用户角色列表
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).join(self.model.users).where(User.id == user_id)
|
stmt = select(self.model).join(self.model.users).where(User.id == user_id)
|
||||||
roles = await db.execute(stmt)
|
roles = await db.execute(stmt)
|
||||||
return roles.scalars().all()
|
return roles.scalars().all()
|
||||||
|
|
||||||
async def get_list(self, name: str = None, status: int = None) -> Select:
|
async def get_list(self, name: str | None = None, status: int | None = None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取角色列表
|
获取角色列表
|
||||||
|
|
||||||
:param name:
|
:param name: 角色名称
|
||||||
:param status:
|
:param status: 角色状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
stmt = (
|
||||||
@@ -76,84 +79,85 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.rules))
|
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.rules))
|
||||||
.order_by(desc(self.model.created_time))
|
.order_by(desc(self.model.created_time))
|
||||||
)
|
)
|
||||||
where_list = []
|
|
||||||
if name:
|
filters = []
|
||||||
where_list.append(self.model.name.like(f'%{name}%'))
|
if name is not None:
|
||||||
|
filters.append(self.model.name.like(f'%{name}%'))
|
||||||
if status is not None:
|
if status is not None:
|
||||||
where_list.append(self.model.status == status)
|
filters.append(self.model.status == status)
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(*where_list)
|
if filters:
|
||||||
|
stmt = stmt.where(and_(*filters))
|
||||||
|
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
async def get_by_name(self, db, name: str) -> Role | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取角色
|
通过名称获取角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 角色名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, name=name)
|
return await self.select_model_by_column(db, name=name)
|
||||||
|
|
||||||
async def create(self, db, obj_in: CreateRoleParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateRoleParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建角色
|
创建角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建角色参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db, role_id: int, obj_in: UpdateRoleParam) -> int:
|
async def update(self, db: AsyncSession, role_id: int, obj: UpdateRoleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色
|
更新角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:param obj_in:
|
:param obj: 更新角色参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, role_id, obj_in)
|
return await self.update_model(db, role_id, obj)
|
||||||
|
|
||||||
async def update_menus(self, db, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色菜单
|
更新角色菜单
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:param menu_ids:
|
:param menu_ids: 菜单 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_role = await self.get_with_relation(db, role_id)
|
current_role = await self.get_with_relation(db, role_id)
|
||||||
# 更新菜单
|
|
||||||
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
|
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
|
||||||
menus = await db.execute(stmt)
|
menus = await db.execute(stmt)
|
||||||
current_role.menus = menus.scalars().all()
|
current_role.menus = menus.scalars().all()
|
||||||
return len(current_role.menus)
|
return len(current_role.menus)
|
||||||
|
|
||||||
async def update_rules(self, db, role_id: int, rule_ids: UpdateRoleRuleParam) -> int:
|
async def update_rules(self, db: AsyncSession, role_id: int, rule_ids: UpdateRoleRuleParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新角色数据权限
|
更新角色数据规则
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID
|
||||||
:param rule_ids:
|
:param rule_ids: 权限规则 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
current_role = await self.get_with_relation(db, role_id)
|
current_role = await self.get_with_relation(db, role_id)
|
||||||
# 更新数据权限
|
|
||||||
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
|
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
|
||||||
rules = await db.execute(stmt)
|
rules = await db.execute(stmt)
|
||||||
current_role.rules = rules.scalars().all()
|
current_role.rules = rules.scalars().all()
|
||||||
return len(current_role.rules)
|
return len(current_role.rules)
|
||||||
|
|
||||||
async def delete(self, db, role_id: list[int]) -> int:
|
async def delete(self, db: AsyncSession, role_id: list[int]) -> int:
|
||||||
"""
|
"""
|
||||||
删除角色
|
删除角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param role_id:
|
:param role_id: 角色 ID 列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_id)
|
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_id)
|
||||||
|
|||||||
@@ -21,42 +21,44 @@ from backend.utils.timezone import timezone
|
|||||||
|
|
||||||
|
|
||||||
class CRUDUser(CRUDPlus[User]):
|
class CRUDUser(CRUDPlus[User]):
|
||||||
|
"""用户数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, user_id: int) -> User | None:
|
async def get(self, db: AsyncSession, user_id: int) -> User | None:
|
||||||
"""
|
"""
|
||||||
获取用户
|
获取用户详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, user_id)
|
return await self.select_model(db, user_id)
|
||||||
|
|
||||||
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
|
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
通过 username 获取用户
|
通过用户名获取用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, username=username)
|
return await self.select_model_by_column(db, username=username)
|
||||||
|
|
||||||
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
|
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
通过 nickname 获取用户
|
通过昵称获取用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param nickname:
|
:param nickname: 用户昵称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, nickname=nickname)
|
return await self.select_model_by_column(db, nickname=nickname)
|
||||||
|
|
||||||
async def update_login_time(self, db: AsyncSession, username: str) -> int:
|
async def update_login_time(self, db: AsyncSession, username: str) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户登录时间
|
更新用户最后登录时间
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
||||||
@@ -65,9 +67,9 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
创建用户
|
创建用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj:
|
:param obj: 注册用户参数
|
||||||
:param social: 社交用户,适配 oauth 2.0
|
:param social: 是否社交用户
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if not social:
|
if not social:
|
||||||
@@ -83,10 +85,10 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
|
|
||||||
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
||||||
"""
|
"""
|
||||||
后台添加用户
|
添加用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj:
|
:param obj: 添加用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
@@ -94,19 +96,21 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
dict_obj = obj.model_dump(exclude={'roles'})
|
dict_obj = obj.model_dump(exclude={'roles'})
|
||||||
dict_obj.update({'salt': salt})
|
dict_obj.update({'salt': salt})
|
||||||
new_user = self.model(**dict_obj)
|
new_user = self.model(**dict_obj)
|
||||||
|
|
||||||
role_list = []
|
role_list = []
|
||||||
for role_id in obj.roles:
|
for role_id in obj.roles:
|
||||||
role_list.append(await db.get(Role, role_id))
|
role_list.append(await db.get(Role, role_id))
|
||||||
new_user.roles.extend(role_list)
|
new_user.roles.extend(role_list)
|
||||||
|
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
|
|
||||||
async def update_userinfo(self, db: AsyncSession, input_user: int, obj: UpdateUserParam) -> int:
|
async def update_userinfo(self, db: AsyncSession, input_user: int, obj: UpdateUserParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户信息
|
更新用户信息
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param input_user:
|
:param input_user: 用户 ID
|
||||||
:param obj:
|
:param obj: 更新用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, input_user, obj)
|
return await self.update_model(db, input_user, obj)
|
||||||
@@ -116,15 +120,14 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
更新用户角色
|
更新用户角色
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param input_user:
|
:param input_user: 用户对象
|
||||||
:param obj:
|
:param obj: 更新角色参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 删除用户所有角色
|
|
||||||
for i in list(input_user.roles):
|
for i in list(input_user.roles):
|
||||||
input_user.roles.remove(i)
|
input_user.roles.remove(i)
|
||||||
# 添加用户角色
|
|
||||||
role_list = []
|
role_list = []
|
||||||
for role_id in obj.roles:
|
for role_id in obj.roles:
|
||||||
role_list.append(await db.get(Role, role_id))
|
role_list.append(await db.get(Role, role_id))
|
||||||
@@ -134,9 +137,9 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
更新用户头像
|
更新用户头像
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param input_user:
|
:param input_user: 用户 ID
|
||||||
:param avatar:
|
:param avatar: 头像地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, input_user, {'avatar': avatar.url})
|
return await self.update_model(db, input_user, {'avatar': avatar.url})
|
||||||
@@ -145,18 +148,18 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
删除用户
|
删除用户
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, user_id)
|
return await self.delete_model(db, user_id)
|
||||||
|
|
||||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
检查邮箱是否存在
|
检查邮箱是否已被注册
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param email:
|
:param email: 电子邮箱
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, email=email)
|
return await self.select_model_by_column(db, email=email)
|
||||||
@@ -165,21 +168,23 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
重置用户密码
|
重置用户密码
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 用户 ID
|
||||||
:param new_pwd:
|
:param new_pwd: 新密码(已加密)
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, {'password': new_pwd})
|
return await self.update_model(db, pk, {'password': new_pwd})
|
||||||
|
|
||||||
async def get_list(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select:
|
async def get_list(
|
||||||
|
self, dept: int | None = None, username: str | None = None, phone: str | None = None, status: int | None = None
|
||||||
|
) -> Select:
|
||||||
"""
|
"""
|
||||||
获取用户列表
|
获取用户列表
|
||||||
|
|
||||||
:param dept:
|
:param dept: 部门 ID
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:param phone:
|
:param phone: 电话号码
|
||||||
:param status:
|
:param status: 用户状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = (
|
stmt = (
|
||||||
@@ -191,25 +196,28 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
)
|
)
|
||||||
.order_by(desc(self.model.join_time))
|
.order_by(desc(self.model.join_time))
|
||||||
)
|
)
|
||||||
where_list = []
|
|
||||||
|
filters = []
|
||||||
if dept:
|
if dept:
|
||||||
where_list.append(self.model.dept_id == dept)
|
filters.append(self.model.dept_id == dept)
|
||||||
if username:
|
if username:
|
||||||
where_list.append(self.model.username.like(f'%{username}%'))
|
filters.append(self.model.username.like(f'%{username}%'))
|
||||||
if phone:
|
if phone:
|
||||||
where_list.append(self.model.phone.like(f'%{phone}%'))
|
filters.append(self.model.phone.like(f'%{phone}%'))
|
||||||
if status is not None:
|
if status is not None:
|
||||||
where_list.append(self.model.status == status)
|
filters.append(self.model.status == status)
|
||||||
if where_list:
|
|
||||||
stmt = stmt.where(and_(*where_list))
|
if filters:
|
||||||
|
stmt = stmt.where(and_(*filters))
|
||||||
|
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
async def get_super(self, db: AsyncSession, user_id: int) -> bool:
|
async def get_super(self, db: AsyncSession, user_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
获取用户超级管理员状态
|
获取用户是否为超级管理员
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
user = await self.get(db, user_id)
|
user = await self.get(db, user_id)
|
||||||
@@ -217,10 +225,10 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
|
|
||||||
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
|
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
获取用户后台登录状态
|
获取用户是否可以登录后台
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
user = await self.get(db, user_id)
|
user = await self.get(db, user_id)
|
||||||
@@ -230,8 +238,8 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
获取用户状态
|
获取用户状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
user = await self.get(db, user_id)
|
user = await self.get(db, user_id)
|
||||||
@@ -239,81 +247,85 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
|
|
||||||
async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool:
|
async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
获取用户多点登录状态
|
获取用户是否允许多端登录
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
user = await self.get(db, user_id)
|
user = await self.get(db, user_id)
|
||||||
return user.is_multi_login
|
return user.is_multi_login
|
||||||
|
|
||||||
async def set_super(self, db: AsyncSession, user_id: int, _super: bool) -> int:
|
async def set_super(self, db: AsyncSession, user_id: int, is_super: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户超级管理员
|
设置用户超级管理员状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param _super:
|
:param is_super: 是否超级管理员
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'is_superuser': _super})
|
return await self.update_model(db, user_id, {'is_superuser': is_super})
|
||||||
|
|
||||||
async def set_staff(self, db: AsyncSession, user_id: int, staff: bool) -> int:
|
async def set_staff(self, db: AsyncSession, user_id: int, is_staff: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户后台登录
|
设置用户后台登录状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param staff:
|
:param is_staff: 是否可登录后台
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'is_staff': staff})
|
return await self.update_model(db, user_id, {'is_staff': is_staff})
|
||||||
|
|
||||||
async def set_status(self, db: AsyncSession, user_id: int, status: bool) -> int:
|
async def set_status(self, db: AsyncSession, user_id: int, status: int) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户状态
|
设置用户状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param status:
|
:param status: 状态
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'status': status})
|
return await self.update_model(db, user_id, {'status': status})
|
||||||
|
|
||||||
async def set_multi_login(self, db: AsyncSession, user_id: int, multi_login: bool) -> int:
|
async def set_multi_login(self, db: AsyncSession, user_id: int, multi_login: bool) -> int:
|
||||||
"""
|
"""
|
||||||
设置用户多点登录
|
设置用户多端登录状态
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param multi_login:
|
:param multi_login: 是否允许多端登录
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
|
||||||
|
|
||||||
async def get_with_relation(self, db: AsyncSession, *, user_id: int = None, username: str = None) -> User | None:
|
async def get_with_relation(
|
||||||
|
self, db: AsyncSession, *, user_id: int | None = None, username: str | None = None
|
||||||
|
) -> User | None:
|
||||||
"""
|
"""
|
||||||
获取用户和(部门,角色,菜单,规则)
|
获取用户关联信息
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param user_id:
|
:param user_id: 用户 ID
|
||||||
:param username:
|
:param username: 用户名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(
|
stmt = select(self.model).options(
|
||||||
selectinload(self.model.dept),
|
selectinload(self.model.dept),
|
||||||
selectinload(self.model.roles).options(
|
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.rules)),
|
||||||
selectinload(Role.menus),
|
|
||||||
selectinload(Role.rules),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
filters = []
|
filters = []
|
||||||
if user_id:
|
if user_id:
|
||||||
filters.append(self.model.id == user_id)
|
filters.append(self.model.id == user_id)
|
||||||
if username:
|
if username:
|
||||||
filters.append(self.model.username == username)
|
filters.append(self.model.username == username)
|
||||||
user = await db.execute(stmt.where(*filters))
|
|
||||||
|
if filters:
|
||||||
|
stmt = stmt.where(and_(*filters))
|
||||||
|
|
||||||
|
user = await db.execute(stmt)
|
||||||
return user.scalars().first()
|
return user.scalars().first()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,37 +8,39 @@ from backend.app.admin.schema.user_social import CreateUserSocialParam
|
|||||||
from backend.common.enums import UserSocialType
|
from backend.common.enums import UserSocialType
|
||||||
|
|
||||||
|
|
||||||
class CRUDOUserSocial(CRUDPlus[UserSocial]):
|
class CRUDUserSocial(CRUDPlus[UserSocial]):
|
||||||
|
"""用户社交账号数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
|
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
|
||||||
"""
|
"""
|
||||||
获取用户社交账号绑定
|
获取用户社交账号绑定详情
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 用户 ID
|
||||||
:param source:
|
:param source: 社交账号类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, user_id=pk, source=source)
|
return await self.select_model_by_column(db, user_id=pk, source=source)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateUserSocialParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateUserSocialParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建用户社交账号绑定
|
创建用户社交账号绑定
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建用户社交账号绑定参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, social_id: int) -> int:
|
async def delete(self, db: AsyncSession, social_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除用户社交账号绑定
|
删除用户社交账号绑定
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param social_id:
|
:param social_id: 社交账号绑定 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, social_id)
|
return await self.delete_model(db, social_id)
|
||||||
|
|
||||||
|
|
||||||
user_social_dao: CRUDOUserSocial = CRUDOUserSocial(UserSocial)
|
user_social_dao: CRUDUserSocial = CRUDUserSocial(UserSocial)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.common.model import Base, id_key
|
|||||||
|
|
||||||
|
|
||||||
class Config(Base):
|
class Config(Base):
|
||||||
"""系统配置表"""
|
"""参数配置表"""
|
||||||
|
|
||||||
__tablename__ = 'sys_config'
|
__tablename__ = 'sys_config'
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_rule
|
from backend.app.admin.model.m2m import sys_role_data_rule
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import Role
|
||||||
|
|
||||||
|
|
||||||
class DataRule(Base):
|
class DataRule(Base):
|
||||||
"""数据权限规则表"""
|
"""数据权限规则表"""
|
||||||
@@ -23,4 +30,4 @@ class DataRule(Base):
|
|||||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||||
|
|
||||||
# 角色规则多对多
|
# 角色规则多对多
|
||||||
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules') # noqa: F821
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules')
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Union
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sqlalchemy import Boolean, ForeignKey, String
|
from sqlalchemy import Boolean, ForeignKey, String
|
||||||
from sqlalchemy.dialects.postgresql import INTEGER
|
from sqlalchemy.dialects.postgresql import INTEGER
|
||||||
@@ -8,6 +10,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import User
|
||||||
|
|
||||||
|
|
||||||
class Dept(Base):
|
class Dept(Base):
|
||||||
"""部门表"""
|
"""部门表"""
|
||||||
@@ -29,8 +34,8 @@ class Dept(Base):
|
|||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
|
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
|
||||||
)
|
)
|
||||||
parent: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='children', remote_side=[id])
|
parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||||
children: Mapped[list['Dept'] | None] = relationship(init=False, back_populates='parent')
|
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
|
||||||
|
|
||||||
# 部门用户一对多
|
# 部门用户一对多
|
||||||
users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821
|
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from sqlalchemy import ForeignKey, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
@@ -7,9 +11,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import DictType
|
||||||
|
|
||||||
|
|
||||||
class DictData(Base):
|
class DictData(Base):
|
||||||
"""字典数据"""
|
"""字典数据表"""
|
||||||
|
|
||||||
__tablename__ = 'sys_dict_data'
|
__tablename__ = 'sys_dict_data'
|
||||||
|
|
||||||
@@ -26,4 +33,4 @@ class DictData(Base):
|
|||||||
type_id: Mapped[int] = mapped_column(
|
type_id: Mapped[int] = mapped_column(
|
||||||
ForeignKey('sys_dict_type.id', ondelete='CASCADE'), default=0, comment='字典类型关联ID'
|
ForeignKey('sys_dict_type.id', ondelete='CASCADE'), default=0, comment='字典类型关联ID'
|
||||||
)
|
)
|
||||||
type: Mapped['DictType'] = relationship(init=False, back_populates='datas') # noqa: F821
|
type: Mapped[DictType] = relationship(init=False, back_populates='datas')
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
@@ -7,9 +11,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import DictData
|
||||||
|
|
||||||
|
|
||||||
class DictType(Base):
|
class DictType(Base):
|
||||||
"""字典类型"""
|
"""字典类型表"""
|
||||||
|
|
||||||
__tablename__ = 'sys_dict_type'
|
__tablename__ = 'sys_dict_type'
|
||||||
|
|
||||||
@@ -22,4 +29,4 @@ class DictType(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 字典类型一对多
|
# 字典类型一对多
|
||||||
datas: Mapped[list['DictData']] = relationship(init=False, back_populates='type') # noqa: F821
|
datas: Mapped[list[DictData]] = relationship(init=False, back_populates='type')
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Union
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from sqlalchemy import ForeignKey, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
@@ -10,6 +12,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
from backend.app.admin.model.m2m import sys_role_menu
|
from backend.app.admin.model.m2m import sys_role_menu
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import Role
|
||||||
|
|
||||||
|
|
||||||
class Menu(Base):
|
class Menu(Base):
|
||||||
"""菜单表"""
|
"""菜单表"""
|
||||||
@@ -36,8 +41,8 @@ class Menu(Base):
|
|||||||
parent_id: Mapped[int | None] = mapped_column(
|
parent_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
|
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
|
||||||
)
|
)
|
||||||
parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id])
|
parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||||
children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent')
|
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
|
||||||
|
|
||||||
# 菜单角色多对多
|
# 菜单角色多对多
|
||||||
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_menu, back_populates='menus') # noqa: F821
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import DateTime, String
|
from sqlalchemy import DateTime, String
|
||||||
from sqlalchemy.dialects.mysql import JSON, LONGTEXT, TEXT
|
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
|
||||||
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from backend.common.model import DataClassBase, id_key
|
from backend.common.model import DataClassBase, id_key
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
@@ -8,6 +12,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
from backend.app.admin.model.m2m import sys_role_data_rule, sys_role_menu, sys_user_role
|
from backend.app.admin.model.m2m import sys_role_data_rule, sys_role_menu, sys_user_role
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import DataRule, Menu, User
|
||||||
|
|
||||||
|
|
||||||
class Role(Base):
|
class Role(Base):
|
||||||
"""角色表"""
|
"""角色表"""
|
||||||
@@ -22,10 +29,10 @@ class Role(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 角色用户多对多
|
# 角色用户多对多
|
||||||
users: Mapped[list['User']] = relationship(init=False, secondary=sys_user_role, back_populates='roles') # noqa: F821
|
users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles')
|
||||||
|
|
||||||
# 角色菜单多对多
|
# 角色菜单多对多
|
||||||
menus: Mapped[list['Menu']] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') # noqa: F821
|
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
|
||||||
|
|
||||||
# 角色数据权限规则多对多
|
# 角色数据权限规则多对多
|
||||||
rules: Mapped[list['DataRule']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles') # noqa: F821
|
rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles')
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Union
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import VARBINARY, Boolean, DateTime, ForeignKey, String
|
from sqlalchemy import VARBINARY, Boolean, DateTime, ForeignKey, String
|
||||||
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
|
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
|
||||||
@@ -12,6 +14,9 @@ from backend.common.model import Base, id_key
|
|||||||
from backend.database.db import uuid4_str
|
from backend.database.db import uuid4_str
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import Dept, Role, UserSocial
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
"""用户表"""
|
"""用户表"""
|
||||||
@@ -31,7 +36,7 @@ class User(Base):
|
|||||||
is_staff: Mapped[bool] = mapped_column(
|
is_staff: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
||||||
)
|
)
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='用户账号状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||||
is_multi_login: Mapped[bool] = mapped_column(
|
is_multi_login: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
||||||
)
|
)
|
||||||
@@ -48,10 +53,10 @@ class User(Base):
|
|||||||
dept_id: Mapped[int | None] = mapped_column(
|
dept_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
|
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
|
||||||
)
|
)
|
||||||
dept: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='users') # noqa: F821
|
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||||
|
|
||||||
# 用户社交信息一对多
|
# 用户社交信息一对多
|
||||||
socials: Mapped[list['UserSocial']] = relationship(init=False, back_populates='user') # noqa: F821
|
socials: Mapped[list[UserSocial]] = relationship(init=False, back_populates='user')
|
||||||
|
|
||||||
# 用户角色多对多
|
# 用户角色多对多
|
||||||
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_user_role, back_populates='users') # noqa: F821
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Union
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from sqlalchemy import ForeignKey, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import User
|
||||||
|
|
||||||
|
|
||||||
class UserSocial(Base):
|
class UserSocial(Base):
|
||||||
"""用户社交表(OAuth2)"""
|
"""用户社交表(OAuth2)"""
|
||||||
@@ -25,4 +30,4 @@ class UserSocial(Base):
|
|||||||
user_id: Mapped[int | None] = mapped_column(
|
user_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
|
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
|
||||||
)
|
)
|
||||||
user: Mapped[Union['User', None]] = relationship(init=False, back_populates='socials') # noqa: F821
|
user: Mapped[User | None] = relationship(init=False, back_populates='socials')
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class GetCaptchaDetail(SchemaBase):
|
class GetCaptchaDetail(SchemaBase):
|
||||||
|
"""验证码详情"""
|
||||||
|
|
||||||
image_type: str = Field(description='图片类型')
|
image_type: str = Field(description='图片类型')
|
||||||
image: str = Field(description='图片内容')
|
image: str = Field(description='图片内容')
|
||||||
|
|||||||
@@ -2,37 +2,43 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class SaveBuiltInConfigParam(SchemaBase):
|
class SaveBuiltInConfigParam(SchemaBase):
|
||||||
name: str
|
"""保存内置参数配置参数"""
|
||||||
key: str
|
|
||||||
value: str
|
name: str = Field(description='参数配置名称')
|
||||||
|
key: str = Field(description='参数配置键名')
|
||||||
|
value: str = Field(description='参数配置值')
|
||||||
|
|
||||||
|
|
||||||
class ConfigSchemaBase(SchemaBase):
|
class ConfigSchemaBase(SchemaBase):
|
||||||
name: str
|
"""参数配置基础模型"""
|
||||||
type: str | None
|
|
||||||
key: str
|
name: str = Field(description='参数配置名称')
|
||||||
value: str
|
type: str | None = Field(None, description='参数配置类型')
|
||||||
is_frontend: bool
|
key: str = Field(description='参数配置键名')
|
||||||
remark: str | None
|
value: str = Field(description='参数配置值')
|
||||||
|
is_frontend: bool = Field(description='是否前端参数配置')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateConfigParam(ConfigSchemaBase):
|
class CreateConfigParam(ConfigSchemaBase):
|
||||||
pass
|
"""创建参数配置参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateConfigParam(ConfigSchemaBase):
|
class UpdateConfigParam(ConfigSchemaBase):
|
||||||
pass
|
"""更新参数配置参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetConfigDetail(ConfigSchemaBase):
|
class GetConfigDetail(ConfigSchemaBase):
|
||||||
|
"""参数配置详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='参数配置 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|||||||
@@ -9,28 +9,33 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class DataRuleSchemaBase(SchemaBase):
|
class DataRuleSchemaBase(SchemaBase):
|
||||||
name: str
|
"""数据规则基础模型"""
|
||||||
model: str
|
|
||||||
column: str
|
name: str = Field(description='规则名称')
|
||||||
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR)
|
model: str = Field(description='模型名称')
|
||||||
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq)
|
column: str = Field(description='字段名称')
|
||||||
value: str
|
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR, description='操作符(AND/OR)')
|
||||||
|
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq, description='表达式类型')
|
||||||
|
value: str = Field(description='规则值')
|
||||||
|
|
||||||
|
|
||||||
class CreateDataRuleParam(DataRuleSchemaBase):
|
class CreateDataRuleParam(DataRuleSchemaBase):
|
||||||
pass
|
"""创建数据规则参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateDataRuleParam(DataRuleSchemaBase):
|
class UpdateDataRuleParam(DataRuleSchemaBase):
|
||||||
pass
|
"""更新数据规则参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetDataRuleDetail(DataRuleSchemaBase):
|
class GetDataRuleDetail(DataRuleSchemaBase):
|
||||||
|
"""数据规则详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='规则 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int:
|
||||||
|
"""计算哈希值"""
|
||||||
return hash(self.name)
|
return hash(self.name)
|
||||||
|
|||||||
@@ -9,27 +9,31 @@ from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class DeptSchemaBase(SchemaBase):
|
class DeptSchemaBase(SchemaBase):
|
||||||
name: str
|
"""部门基础模型"""
|
||||||
parent_id: int | None = Field(default=None, description='部门父级ID')
|
|
||||||
sort: int = Field(default=0, ge=0, description='排序')
|
name: str = Field(description='部门名称')
|
||||||
leader: str | None = None
|
parent_id: int | None = Field(None, description='部门父级 ID')
|
||||||
phone: CustomPhoneNumber | None = None
|
sort: int = Field(0, ge=0, description='排序')
|
||||||
email: CustomEmailStr | None = None
|
leader: str | None = Field(None, description='负责人')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
|
||||||
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
|
|
||||||
|
|
||||||
class CreateDeptParam(DeptSchemaBase):
|
class CreateDeptParam(DeptSchemaBase):
|
||||||
pass
|
"""创建部门参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateDeptParam(DeptSchemaBase):
|
class UpdateDeptParam(DeptSchemaBase):
|
||||||
pass
|
"""更新部门参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetDeptDetail(DeptSchemaBase):
|
class GetDeptDetail(DeptSchemaBase):
|
||||||
|
"""部门详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='部门 ID')
|
||||||
del_flag: bool
|
del_flag: bool = Field(description='是否删除')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|||||||
@@ -10,26 +10,35 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class DictDataSchemaBase(SchemaBase):
|
class DictDataSchemaBase(SchemaBase):
|
||||||
type_id: int
|
"""字典数据基础模型"""
|
||||||
label: str
|
|
||||||
value: str
|
type_id: int = Field(description='字典类型 ID')
|
||||||
sort: int
|
label: str = Field(description='字典标签')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
value: str = Field(description='字典值')
|
||||||
remark: str | None = None
|
sort: int = Field(description='排序')
|
||||||
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateDictDataParam(DictDataSchemaBase):
|
class CreateDictDataParam(DictDataSchemaBase):
|
||||||
pass
|
"""创建字典数据参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateDictDataParam(DictDataSchemaBase):
|
class UpdateDictDataParam(DictDataSchemaBase):
|
||||||
pass
|
"""更新字典数据参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetDictDataDetail(DictDataSchemaBase):
|
class GetDictDataDetail(DictDataSchemaBase):
|
||||||
|
"""字典数据详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='字典数据 ID')
|
||||||
type: GetDictTypeDetail | None = None
|
created_time: datetime = Field(description='创建时间')
|
||||||
created_time: datetime
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
updated_time: datetime | None = None
|
|
||||||
|
|
||||||
|
class GetDictDataWithRelation(DictDataSchemaBase):
|
||||||
|
"""字典数据关联详情"""
|
||||||
|
|
||||||
|
type: GetDictTypeDetail | None = Field(None, description='字典类型信息')
|
||||||
|
|||||||
@@ -9,23 +9,27 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class DictTypeSchemaBase(SchemaBase):
|
class DictTypeSchemaBase(SchemaBase):
|
||||||
name: str
|
"""字典类型基础模型"""
|
||||||
code: str
|
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
name: str = Field(description='字典名称')
|
||||||
remark: str | None = None
|
code: str = Field(description='字典编码')
|
||||||
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateDictTypeParam(DictTypeSchemaBase):
|
class CreateDictTypeParam(DictTypeSchemaBase):
|
||||||
pass
|
"""创建字典类型参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateDictTypeParam(DictTypeSchemaBase):
|
class UpdateDictTypeParam(DictTypeSchemaBase):
|
||||||
pass
|
"""更新字典类型参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetDictTypeDetail(DictTypeSchemaBase):
|
class GetDictTypeDetail(DictTypeSchemaBase):
|
||||||
|
"""字典类型详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='字典类型 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|||||||
@@ -2,37 +2,41 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class LoginLogSchemaBase(SchemaBase):
|
class LoginLogSchemaBase(SchemaBase):
|
||||||
user_uuid: str
|
"""登录日志基础模型"""
|
||||||
username: str
|
|
||||||
status: int
|
user_uuid: str = Field(description='用户 UUID')
|
||||||
ip: str
|
username: str = Field(description='用户名')
|
||||||
country: str | None
|
status: int = Field(description='登录状态')
|
||||||
region: str | None
|
ip: str = Field(description='IP 地址')
|
||||||
city: str | None
|
country: str | None = Field(None, description='国家')
|
||||||
user_agent: str
|
region: str | None = Field(None, description='地区')
|
||||||
browser: str | None
|
city: str | None = Field(None, description='城市')
|
||||||
os: str | None
|
user_agent: str = Field(description='用户代理')
|
||||||
device: str | None
|
browser: str | None = Field(None, description='浏览器')
|
||||||
msg: str
|
os: str | None = Field(None, description='操作系统')
|
||||||
login_time: datetime
|
device: str | None = Field(None, description='设备')
|
||||||
|
msg: str = Field(description='消息')
|
||||||
|
login_time: datetime = Field(description='登录时间')
|
||||||
|
|
||||||
|
|
||||||
class CreateLoginLogParam(LoginLogSchemaBase):
|
class CreateLoginLogParam(LoginLogSchemaBase):
|
||||||
pass
|
"""创建登录日志参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateLoginLogParam(LoginLogSchemaBase):
|
class UpdateLoginLogParam(LoginLogSchemaBase):
|
||||||
pass
|
"""更新登录日志参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetLoginLogDetail(LoginLogSchemaBase):
|
class GetLoginLogDetail(LoginLogSchemaBase):
|
||||||
|
"""登录日志详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='日志 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
|
|||||||
@@ -9,32 +9,36 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class MenuSchemaBase(SchemaBase):
|
class MenuSchemaBase(SchemaBase):
|
||||||
title: str
|
"""菜单基础模型"""
|
||||||
name: str
|
|
||||||
parent_id: int | None = Field(default=None, description='菜单父级ID')
|
title: str = Field(description='菜单标题')
|
||||||
sort: int = Field(default=0, ge=0, description='排序')
|
name: str = Field(description='菜单名称')
|
||||||
icon: str | None = None
|
parent_id: int | None = Field(None, description='菜单父级 ID')
|
||||||
path: str | None = None
|
sort: int = Field(0, ge=0, description='排序')
|
||||||
menu_type: MenuType = Field(default=MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
|
icon: str | None = Field(None, description='图标')
|
||||||
component: str | None = None
|
path: str | None = Field(None, description='路由路径')
|
||||||
perms: str | None = None
|
menu_type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
component: str | None = Field(None, description='组件路径')
|
||||||
display: StatusType = Field(default=StatusType.enable)
|
perms: str | None = Field(None, description='权限标识')
|
||||||
cache: StatusType = Field(default=StatusType.enable)
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
remark: str | None = None
|
display: StatusType = Field(StatusType.enable, description='是否显示')
|
||||||
|
cache: StatusType = Field(StatusType.enable, description='是否缓存')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateMenuParam(MenuSchemaBase):
|
class CreateMenuParam(MenuSchemaBase):
|
||||||
pass
|
"""创建菜单参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateMenuParam(MenuSchemaBase):
|
class UpdateMenuParam(MenuSchemaBase):
|
||||||
pass
|
"""更新菜单参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetMenuDetail(MenuSchemaBase):
|
class GetMenuDetail(MenuSchemaBase):
|
||||||
|
"""菜单详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='菜单 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
@@ -9,37 +10,41 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class OperaLogSchemaBase(SchemaBase):
|
class OperaLogSchemaBase(SchemaBase):
|
||||||
trace_id: str
|
"""操作日志基础模型"""
|
||||||
username: str | None = None
|
|
||||||
method: str
|
trace_id: str = Field(description='追踪 ID')
|
||||||
title: str
|
username: str | None = Field(None, description='用户名')
|
||||||
path: str
|
method: str = Field(description='请求方法')
|
||||||
ip: str
|
title: str = Field(description='操作标题')
|
||||||
country: str | None = None
|
path: str = Field(description='请求路径')
|
||||||
region: str | None = None
|
ip: str = Field(description='IP 地址')
|
||||||
city: str | None = None
|
country: str | None = Field(None, description='国家')
|
||||||
user_agent: str
|
region: str | None = Field(None, description='地区')
|
||||||
os: str | None = None
|
city: str | None = Field(None, description='城市')
|
||||||
browser: str | None = None
|
user_agent: str = Field(description='用户代理')
|
||||||
device: str | None = None
|
os: str | None = Field(None, description='操作系统')
|
||||||
args: dict | None = None
|
browser: str | None = Field(None, description='浏览器')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
device: str | None = Field(None, description='设备')
|
||||||
code: str
|
args: dict[str, Any] | None = Field(None, description='请求参数')
|
||||||
msg: str | None = None
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
cost_time: float
|
code: str = Field(description='状态码')
|
||||||
opera_time: datetime
|
msg: str | None = Field(None, description='消息')
|
||||||
|
cost_time: float = Field(description='耗时')
|
||||||
|
opera_time: datetime = Field(description='操作时间')
|
||||||
|
|
||||||
|
|
||||||
class CreateOperaLogParam(OperaLogSchemaBase):
|
class CreateOperaLogParam(OperaLogSchemaBase):
|
||||||
pass
|
"""创建操作日志参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateOperaLogParam(OperaLogSchemaBase):
|
class UpdateOperaLogParam(OperaLogSchemaBase):
|
||||||
pass
|
"""更新操作日志参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetOperaLogDetail(OperaLogSchemaBase):
|
class GetOperaLogDetail(OperaLogSchemaBase):
|
||||||
|
"""操作日志详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='日志 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
|
|||||||
@@ -11,32 +11,45 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class RoleSchemaBase(SchemaBase):
|
class RoleSchemaBase(SchemaBase):
|
||||||
name: str
|
"""角色基础模型"""
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
|
||||||
remark: str | None = None
|
name: str = Field(description='角色名称')
|
||||||
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
class CreateRoleParam(RoleSchemaBase):
|
class CreateRoleParam(RoleSchemaBase):
|
||||||
pass
|
"""创建角色参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleParam(RoleSchemaBase):
|
class UpdateRoleParam(RoleSchemaBase):
|
||||||
pass
|
"""更新角色参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleMenuParam(SchemaBase):
|
class UpdateRoleMenuParam(SchemaBase):
|
||||||
menus: list[int]
|
"""更新角色菜单参数"""
|
||||||
|
|
||||||
|
menus: list[int] = Field(description='菜单 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class UpdateRoleRuleParam(SchemaBase):
|
class UpdateRoleRuleParam(SchemaBase):
|
||||||
rules: list[int]
|
"""更新角色规则参数"""
|
||||||
|
|
||||||
|
rules: list[int] = Field(description='数据规则 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class GetRoleDetail(RoleSchemaBase):
|
class GetRoleDetail(RoleSchemaBase):
|
||||||
|
"""角色详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='角色 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
menus: list[GetMenuDetail | None] = []
|
|
||||||
rules: list[GetDataRuleDetail | None] = []
|
|
||||||
|
class GetRoleWithRelationDetail(GetRoleDetail):
|
||||||
|
"""角色关联详情"""
|
||||||
|
|
||||||
|
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
|
||||||
|
rules: list[GetDataRuleDetail | None] = Field([], description='数据规则详情列表')
|
||||||
|
|||||||
@@ -2,44 +2,56 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from backend.app.admin.schema.user import GetUserInfoNoRelationDetail
|
from pydantic import Field
|
||||||
|
|
||||||
|
from backend.app.admin.schema.user import GetUserInfoDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class GetSwaggerToken(SchemaBase):
|
class GetSwaggerToken(SchemaBase):
|
||||||
access_token: str
|
"""Swagger 认证令牌"""
|
||||||
token_type: str = 'Bearer'
|
|
||||||
user: GetUserInfoNoRelationDetail
|
access_token: str = Field(description='访问令牌')
|
||||||
|
token_type: str = Field('Bearer', description='令牌类型')
|
||||||
|
user: GetUserInfoDetail = Field(description='用户信息')
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenBase(SchemaBase):
|
class AccessTokenBase(SchemaBase):
|
||||||
access_token: str
|
"""访问令牌基础模型"""
|
||||||
access_token_expire_time: datetime
|
|
||||||
session_uuid: str
|
access_token: str = Field(description='访问令牌')
|
||||||
|
access_token_expire_time: datetime = Field(description='令牌过期时间')
|
||||||
|
session_uuid: str = Field(description='会话 UUID')
|
||||||
|
|
||||||
|
|
||||||
class GetNewToken(AccessTokenBase):
|
class GetNewToken(AccessTokenBase):
|
||||||
pass
|
"""获取新令牌"""
|
||||||
|
|
||||||
|
|
||||||
class GetLoginToken(AccessTokenBase):
|
class GetLoginToken(AccessTokenBase):
|
||||||
user: GetUserInfoNoRelationDetail
|
"""获取登录令牌"""
|
||||||
|
|
||||||
|
user: GetUserInfoDetail = Field(description='用户信息')
|
||||||
|
|
||||||
|
|
||||||
class KickOutToken(SchemaBase):
|
class KickOutToken(SchemaBase):
|
||||||
session_uuid: str
|
"""踢出令牌"""
|
||||||
|
|
||||||
|
session_uuid: str = Field(description='会话 UUID')
|
||||||
|
|
||||||
|
|
||||||
class GetTokenDetail(SchemaBase):
|
class GetTokenDetail(SchemaBase):
|
||||||
id: int
|
"""令牌详情"""
|
||||||
session_uuid: str
|
|
||||||
username: str
|
id: int = Field(description='用户 ID')
|
||||||
nickname: str
|
session_uuid: str = Field(description='会话 UUID')
|
||||||
ip: str
|
username: str = Field(description='用户名')
|
||||||
os: str
|
nickname: str = Field(description='昵称')
|
||||||
browser: str
|
ip: str = Field(description='IP 地址')
|
||||||
device: str
|
os: str = Field(description='操作系统')
|
||||||
status: StatusType
|
browser: str = Field(description='浏览器')
|
||||||
last_login_time: str
|
device: str = Field(description='设备')
|
||||||
expire_time: datetime
|
status: StatusType = Field(description='状态')
|
||||||
|
last_login_time: str = Field(description='最后登录时间')
|
||||||
|
expire_time: datetime = Field(description='过期时间')
|
||||||
|
|||||||
@@ -7,84 +7,112 @@ from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
|
|||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from backend.app.admin.schema.dept import GetDeptDetail
|
from backend.app.admin.schema.dept import GetDeptDetail
|
||||||
from backend.app.admin.schema.role import GetRoleDetail
|
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.schema import CustomPhoneNumber, SchemaBase
|
from backend.common.schema import CustomPhoneNumber, SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class AuthSchemaBase(SchemaBase):
|
class AuthSchemaBase(SchemaBase):
|
||||||
username: str
|
"""用户认证基础模型"""
|
||||||
password: str | None
|
|
||||||
|
username: str = Field(description='用户名')
|
||||||
|
password: str | None = Field(description='密码')
|
||||||
|
|
||||||
|
|
||||||
class AuthLoginParam(AuthSchemaBase):
|
class AuthLoginParam(AuthSchemaBase):
|
||||||
captcha: str
|
"""用户登录参数"""
|
||||||
|
|
||||||
|
captcha: str = Field(description='验证码')
|
||||||
|
|
||||||
|
|
||||||
class RegisterUserParam(AuthSchemaBase):
|
class RegisterUserParam(AuthSchemaBase):
|
||||||
nickname: str | None = None
|
"""用户注册参数"""
|
||||||
email: EmailStr = Field(examples=['user@example.com'])
|
|
||||||
|
nickname: str | None = Field(None, description='昵称')
|
||||||
|
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||||
|
|
||||||
|
|
||||||
class AddUserParam(AuthSchemaBase):
|
class AddUserParam(AuthSchemaBase):
|
||||||
dept_id: int
|
"""添加用户参数"""
|
||||||
roles: list[int]
|
|
||||||
nickname: str | None = None
|
dept_id: int = Field(description='部门 ID')
|
||||||
email: EmailStr = Field(examples=['user@example.com'])
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
|
nickname: str | None = Field(None, description='昵称')
|
||||||
|
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordParam(SchemaBase):
|
||||||
|
"""重置密码参数"""
|
||||||
|
|
||||||
|
old_password: str = Field(description='旧密码')
|
||||||
|
new_password: str = Field(description='新密码')
|
||||||
|
confirm_password: str = Field(description='确认密码')
|
||||||
|
|
||||||
|
|
||||||
class UserInfoSchemaBase(SchemaBase):
|
class UserInfoSchemaBase(SchemaBase):
|
||||||
dept_id: int | None = None
|
"""用户信息基础模型"""
|
||||||
username: str
|
|
||||||
nickname: str
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
email: EmailStr = Field(examples=['user@example.com'])
|
username: str = Field(description='用户名')
|
||||||
phone: CustomPhoneNumber | None = None
|
nickname: str = Field(description='昵称')
|
||||||
|
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||||
|
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserParam(UserInfoSchemaBase):
|
class UpdateUserParam(UserInfoSchemaBase):
|
||||||
pass
|
"""更新用户参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserRoleParam(SchemaBase):
|
class UpdateUserRoleParam(SchemaBase):
|
||||||
roles: list[int]
|
"""更新用户角色参数"""
|
||||||
|
|
||||||
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class AvatarParam(SchemaBase):
|
class AvatarParam(SchemaBase):
|
||||||
|
"""更新头像参数"""
|
||||||
|
|
||||||
url: HttpUrl = Field(description='头像 http 地址')
|
url: HttpUrl = Field(description='头像 http 地址')
|
||||||
|
|
||||||
|
|
||||||
class GetUserInfoNoRelationDetail(UserInfoSchemaBase):
|
class GetUserInfoDetail(UserInfoSchemaBase):
|
||||||
|
"""用户信息详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
dept_id: int | None = None
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
id: int
|
id: int = Field(description='用户 ID')
|
||||||
uuid: str
|
uuid: str = Field(description='用户 UUID')
|
||||||
avatar: str | None = None
|
avatar: str | None = Field(None, description='头像')
|
||||||
status: StatusType = Field(default=StatusType.enable)
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
is_superuser: bool
|
is_superuser: bool = Field(description='是否超级管理员')
|
||||||
is_staff: bool
|
is_staff: bool = Field(description='是否管理员')
|
||||||
is_multi_login: bool
|
is_multi_login: bool = Field(description='是否允许多端登录')
|
||||||
join_time: datetime = None
|
join_time: datetime = Field(description='加入时间')
|
||||||
last_login_time: datetime | None = None
|
last_login_time: datetime | None = Field(None, description='最后登录时间')
|
||||||
|
|
||||||
|
|
||||||
class GetUserInfoDetail(GetUserInfoNoRelationDetail):
|
class GetUserInfoWithRelationDetail(GetUserInfoDetail):
|
||||||
|
"""用户信息关联详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
dept: GetDeptDetail | None = None
|
dept: GetDeptDetail | None = Field(None, description='部门信息')
|
||||||
roles: list[GetRoleDetail]
|
roles: list[GetRoleWithRelationDetail] = Field(description='角色列表')
|
||||||
|
|
||||||
|
|
||||||
class GetCurrentUserInfoDetail(GetUserInfoDetail):
|
class GetCurrentUserInfoWithRelationDetail(GetUserInfoWithRelationDetail):
|
||||||
|
"""当前用户信息关联详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
dept: str | None = None
|
dept: str | None = Field(None, description='部门名称')
|
||||||
roles: list[str]
|
roles: list[str] = Field(description='角色名称列表')
|
||||||
|
|
||||||
@model_validator(mode='before')
|
@model_validator(mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def handel(cls, data: Any) -> Self:
|
def handel(cls, data: Any) -> Self:
|
||||||
"""处理部门和角色"""
|
"""处理部门和角色数据"""
|
||||||
dept = data['dept']
|
dept = data['dept']
|
||||||
if dept:
|
if dept:
|
||||||
data['dept'] = dept['name']
|
data['dept'] = dept['name']
|
||||||
@@ -92,13 +120,3 @@ class GetCurrentUserInfoDetail(GetUserInfoDetail):
|
|||||||
if roles:
|
if roles:
|
||||||
data['roles'] = [role['name'] for role in roles]
|
data['roles'] = [role['name'] for role in roles]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class CurrentUserIns(GetUserInfoDetail):
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordParam(SchemaBase):
|
|
||||||
old_password: str
|
|
||||||
new_password: str
|
|
||||||
confirm_password: str
|
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
from backend.common.enums import UserSocialType
|
from backend.common.enums import UserSocialType
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class UserSocialSchemaBase(SchemaBase):
|
class UserSocialSchemaBase(SchemaBase):
|
||||||
source: UserSocialType
|
"""用户社交基础模型"""
|
||||||
open_id: str | None = None
|
|
||||||
uid: str | None = None
|
source: UserSocialType = Field(description='社交平台')
|
||||||
union_id: str | None = None
|
open_id: str | None = Field(None, description='开放平台 ID')
|
||||||
scope: str | None = None
|
uid: str | None = Field(None, description='用户 ID')
|
||||||
code: str | None = None
|
union_id: str | None = Field(None, description='开放平台唯一 ID')
|
||||||
|
scope: str | None = Field(None, description='授权范围')
|
||||||
|
code: str | None = Field(None, description='授权码')
|
||||||
|
|
||||||
|
|
||||||
class CreateUserSocialParam(UserSocialSchemaBase):
|
class CreateUserSocialParam(UserSocialSchemaBase):
|
||||||
user_id: int
|
"""创建用户社交参数"""
|
||||||
|
|
||||||
|
user_id: int = Field(description='用户 ID')
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserSocialParam(SchemaBase):
|
class UpdateUserSocialParam(SchemaBase):
|
||||||
pass
|
"""更新用户社交参数"""
|
||||||
|
|||||||
@@ -30,8 +30,18 @@ from backend.utils.timezone import timezone
|
|||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
|
"""认证服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
|
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
|
||||||
|
"""
|
||||||
|
验证用户名和密码
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param username: 用户名
|
||||||
|
:param password: 密码
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
user = await user_dao.get_by_username(db, username)
|
user = await user_dao.get_by_username(db, username)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户名或密码有误')
|
raise errors.NotFoundError(msg='用户名或密码有误')
|
||||||
@@ -42,6 +52,12 @@ class AuthService:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
|
||||||
|
"""
|
||||||
|
Swagger 文档登录
|
||||||
|
|
||||||
|
:param obj: 登录凭证
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
user = await self.user_verify(db, obj.username, obj.password)
|
user = await self.user_verify(db, obj.username, obj.password)
|
||||||
await user_dao.update_login_time(db, obj.username)
|
await user_dao.update_login_time(db, obj.username)
|
||||||
@@ -56,6 +72,15 @@ class AuthService:
|
|||||||
async def login(
|
async def login(
|
||||||
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
|
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
|
||||||
) -> GetLoginToken:
|
) -> GetLoginToken:
|
||||||
|
"""
|
||||||
|
用户登录
|
||||||
|
|
||||||
|
:param request: 请求对象
|
||||||
|
:param response: 响应对象
|
||||||
|
:param obj: 登录参数
|
||||||
|
:param background_tasks: 后台任务
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
user = None
|
user = None
|
||||||
try:
|
try:
|
||||||
@@ -133,6 +158,12 @@ class AuthService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def new_token(*, request: Request) -> GetNewToken:
|
async def new_token(*, request: Request) -> GetNewToken:
|
||||||
|
"""
|
||||||
|
获取新的访问令牌
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
if not refresh_token:
|
if not refresh_token:
|
||||||
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
|
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
|
||||||
@@ -168,6 +199,13 @@ class AuthService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def logout(*, request: Request, response: Response) -> None:
|
async def logout(*, request: Request, response: Response) -> None:
|
||||||
|
"""
|
||||||
|
用户登出
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param response: FastAPI 响应对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
token = get_token(request)
|
token = get_token(request)
|
||||||
token_payload = jwt_decode(token)
|
token_payload = jwt_decode(token)
|
||||||
user_id = token_payload.id
|
user_id = token_payload.id
|
||||||
|
|||||||
@@ -17,25 +17,46 @@ from backend.database.db import async_db_session
|
|||||||
|
|
||||||
|
|
||||||
class ConfigService:
|
class ConfigService:
|
||||||
|
"""参数配置服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_built_in_config(type: str) -> Sequence[Config]:
|
async def get_built_in_config(type: str) -> Sequence[Config]:
|
||||||
|
"""
|
||||||
|
获取内置参数配置
|
||||||
|
|
||||||
|
:param type: 参数配置类型
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
return await config_dao.get_by_type(db, type)
|
return await config_dao.get_by_type(db, type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def save_built_in_config(objs: list[SaveBuiltInConfigParam], type: str) -> None:
|
async def save_built_in_config(objs: list[SaveBuiltInConfigParam], type: str) -> None:
|
||||||
|
"""
|
||||||
|
保存内置参数配置
|
||||||
|
|
||||||
|
:param objs: 参数配置参数列表
|
||||||
|
:param type: 参数配置类型
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
config = await config_dao.get_by_key_and_type(db, obj.key, type)
|
config = await config_dao.get_by_key_and_type(db, obj.key, type)
|
||||||
if config is None:
|
if config is None:
|
||||||
if await config_dao.get_by_key(db, obj.key, built_in=True):
|
if await config_dao.get_by_key(db, obj.key):
|
||||||
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
|
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
|
||||||
await config_dao.create_model(db, obj, flush=True, type=type)
|
await config_dao.create_model(db, obj, flush=True, type=type)
|
||||||
else:
|
else:
|
||||||
await config_dao.update_model(db, config.id, obj, type=type)
|
await config_dao.update_model(db, config.id, obj, type=type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(pk) -> Config | dict:
|
async def get(pk: int) -> Config:
|
||||||
|
"""
|
||||||
|
获取参数配置详情
|
||||||
|
|
||||||
|
:param pk: 参数配置 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
config = await config_dao.get(db, pk)
|
config = await config_dao.get(db, pk)
|
||||||
if not config:
|
if not config:
|
||||||
@@ -43,11 +64,24 @@ class ConfigService:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str = None, type: str = None) -> Select:
|
async def get_select(*, name: str | None = None, type: str | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取参数配置列表查询条件
|
||||||
|
|
||||||
|
:param name: 参数配置名称
|
||||||
|
:param type: 参数配置类型
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await config_dao.get_list(name=name, type=type)
|
return await config_dao.get_list(name=name, type=type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateConfigParam) -> None:
|
async def create(*, obj: CreateConfigParam) -> None:
|
||||||
|
"""
|
||||||
|
创建参数配置
|
||||||
|
|
||||||
|
:param obj: 参数配置创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
if obj.type in admin_settings.CONFIG_BUILT_IN_TYPES:
|
if obj.type in admin_settings.CONFIG_BUILT_IN_TYPES:
|
||||||
raise errors.ForbiddenError(msg='非法类型参数')
|
raise errors.ForbiddenError(msg='非法类型参数')
|
||||||
@@ -58,15 +92,32 @@ class ConfigService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateConfigParam) -> int:
|
async def update(*, pk: int, obj: UpdateConfigParam) -> int:
|
||||||
|
"""
|
||||||
|
更新参数配置
|
||||||
|
|
||||||
|
:param pk: 参数配置 ID
|
||||||
|
:param obj: 参数配置更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
config = await config_dao.get(db, pk)
|
config = await config_dao.get(db, pk)
|
||||||
if not config:
|
if not config:
|
||||||
raise errors.NotFoundError(msg='参数配置不存在')
|
raise errors.NotFoundError(msg='参数配置不存在')
|
||||||
|
if config.key != obj.key:
|
||||||
|
config = await config_dao.get_by_key(db, obj.key)
|
||||||
|
if config:
|
||||||
|
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
|
||||||
count = await config_dao.update(db, pk, obj)
|
count = await config_dao.update(db, pk, obj)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除参数配置
|
||||||
|
|
||||||
|
:param pk: 参数配置 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await config_dao.delete(db, pk)
|
count = await config_dao.delete(db, pk)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -17,8 +17,16 @@ from backend.utils.import_parse import dynamic_import_data_model
|
|||||||
|
|
||||||
|
|
||||||
class DataRuleService:
|
class DataRuleService:
|
||||||
|
"""数据权限规则服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> DataRule:
|
async def get(*, pk: int) -> DataRule:
|
||||||
|
"""
|
||||||
|
获取数据规则详情
|
||||||
|
|
||||||
|
:param pk: 规则 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
data_rule = await data_rule_dao.get(db, pk)
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
if not data_rule:
|
if not data_rule:
|
||||||
@@ -27,6 +35,12 @@ class DataRuleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_role_rules(*, pk: int) -> list[int]:
|
async def get_role_rules(*, pk: int) -> list[int]:
|
||||||
|
"""
|
||||||
|
获取角色的数据规则列表
|
||||||
|
|
||||||
|
:param pk: 角色 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get_with_relation(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
@@ -36,33 +50,50 @@ class DataRuleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_models() -> list[str]:
|
async def get_models() -> list[str]:
|
||||||
|
"""获取所有数据模型"""
|
||||||
return list(settings.DATA_PERMISSION_MODELS.keys())
|
return list(settings.DATA_PERMISSION_MODELS.keys())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_columns(model: str) -> list[str]:
|
async def get_columns(model: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
获取数据模型的字段列表
|
||||||
|
|
||||||
|
:param model: 模型名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if model not in settings.DATA_PERMISSION_MODELS:
|
if model not in settings.DATA_PERMISSION_MODELS:
|
||||||
raise errors.NotFoundError(msg='数据模型不存在')
|
raise errors.NotFoundError(msg='数据模型不存在')
|
||||||
try:
|
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
|
||||||
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
raise errors.ServerError(msg=f'数据模型 {model} 动态导入失败,请联系系统超级管理员')
|
|
||||||
model_columns = [
|
model_columns = [
|
||||||
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
|
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
|
||||||
]
|
]
|
||||||
return model_columns
|
return model_columns
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str = None) -> Select:
|
async def get_select(*, name: str | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取数据规则列表查询条件
|
||||||
|
|
||||||
|
:param name: 规则名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await data_rule_dao.get_list(name=name)
|
return await data_rule_dao.get_list(name=name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> Sequence[DataRule]:
|
async def get_all() -> Sequence[DataRule]:
|
||||||
|
"""获取所有数据规则"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
data_rules = await data_rule_dao.get_all(db)
|
data_rules = await data_rule_dao.get_all(db)
|
||||||
return data_rules
|
return data_rules
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDataRuleParam) -> None:
|
async def create(*, obj: CreateDataRuleParam) -> None:
|
||||||
|
"""
|
||||||
|
创建数据规则
|
||||||
|
|
||||||
|
:param obj: 规则创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
data_rule = await data_rule_dao.get_by_name(db, obj.name)
|
data_rule = await data_rule_dao.get_by_name(db, obj.name)
|
||||||
if data_rule:
|
if data_rule:
|
||||||
@@ -71,6 +102,13 @@ class DataRuleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
|
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
|
||||||
|
"""
|
||||||
|
更新数据规则
|
||||||
|
|
||||||
|
:param pk: 规则 ID
|
||||||
|
:param obj: 规则更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
data_rule = await data_rule_dao.get(db, pk)
|
data_rule = await data_rule_dao.get(db, pk)
|
||||||
if not data_rule:
|
if not data_rule:
|
||||||
@@ -80,6 +118,13 @@ class DataRuleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, request: Request, pk: list[int]) -> int:
|
async def delete(*, request: Request, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除数据规则
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 规则 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await data_rule_dao.delete(db, pk)
|
count = await data_rule_dao.delete(db, pk)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
||||||
|
|||||||
@@ -15,8 +15,16 @@ from backend.utils.build_tree import get_tree_data
|
|||||||
|
|
||||||
|
|
||||||
class DeptService:
|
class DeptService:
|
||||||
|
"""部门服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> Dept:
|
async def get(*, pk: int) -> Dept:
|
||||||
|
"""
|
||||||
|
获取部门详情
|
||||||
|
|
||||||
|
:param pk: 部门 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
dept = await dept_dao.get(db, pk)
|
dept = await dept_dao.get(db, pk)
|
||||||
if not dept:
|
if not dept:
|
||||||
@@ -27,6 +35,15 @@ class DeptService:
|
|||||||
async def get_dept_tree(
|
async def get_dept_tree(
|
||||||
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None
|
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取部门树形结构
|
||||||
|
|
||||||
|
:param name: 部门名称
|
||||||
|
:param leader: 部门负责人
|
||||||
|
:param phone: 联系电话
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status)
|
dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status)
|
||||||
tree_data = get_tree_data(dept_select)
|
tree_data = get_tree_data(dept_select)
|
||||||
@@ -34,6 +51,12 @@ class DeptService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDeptParam) -> None:
|
async def create(*, obj: CreateDeptParam) -> None:
|
||||||
|
"""
|
||||||
|
创建部门
|
||||||
|
|
||||||
|
:param obj: 部门创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dept = await dept_dao.get_by_name(db, obj.name)
|
dept = await dept_dao.get_by_name(db, obj.name)
|
||||||
if dept:
|
if dept:
|
||||||
@@ -46,6 +69,13 @@ class DeptService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDeptParam) -> int:
|
async def update(*, pk: int, obj: UpdateDeptParam) -> int:
|
||||||
|
"""
|
||||||
|
更新部门
|
||||||
|
|
||||||
|
:param pk: 部门 ID
|
||||||
|
:param obj: 部门更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dept = await dept_dao.get(db, pk)
|
dept = await dept_dao.get(db, pk)
|
||||||
if not dept:
|
if not dept:
|
||||||
@@ -64,8 +94,16 @@ class DeptService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, request: Request, pk: int) -> int:
|
async def delete(*, request: Request, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
删除部门
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 部门 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dept_user = await dept_dao.get_with_relation(db, pk)
|
dept = await dept_dao.get_with_relation(db, pk)
|
||||||
|
dept_user = dept.users
|
||||||
if dept_user:
|
if dept_user:
|
||||||
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
|
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
|
||||||
children = await dept_dao.get_children(db, pk)
|
children = await dept_dao.get_children(db, pk)
|
||||||
|
|||||||
@@ -11,8 +11,16 @@ from backend.database.db import async_db_session
|
|||||||
|
|
||||||
|
|
||||||
class DictDataService:
|
class DictDataService:
|
||||||
|
"""字典数据服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> DictData:
|
async def get(*, pk: int) -> DictData:
|
||||||
|
"""
|
||||||
|
获取字典数据详情
|
||||||
|
|
||||||
|
:param pk: 字典数据 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
dict_data = await dict_data_dao.get_with_relation(db, pk)
|
dict_data = await dict_data_dao.get_with_relation(db, pk)
|
||||||
if not dict_data:
|
if not dict_data:
|
||||||
@@ -20,11 +28,25 @@ class DictDataService:
|
|||||||
return dict_data
|
return dict_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, label: str = None, value: str = None, status: int = None) -> Select:
|
async def get_select(*, label: str | None = None, value: str | None = None, status: int | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取字典数据列表查询条件
|
||||||
|
|
||||||
|
:param label: 字典数据标签
|
||||||
|
:param value: 字典数据键值
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await dict_data_dao.get_list(label=label, value=value, status=status)
|
return await dict_data_dao.get_list(label=label, value=value, status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDictDataParam) -> None:
|
async def create(*, obj: CreateDictDataParam) -> None:
|
||||||
|
"""
|
||||||
|
创建字典数据
|
||||||
|
|
||||||
|
:param obj: 字典数据创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dict_data = await dict_data_dao.get_by_label(db, obj.label)
|
dict_data = await dict_data_dao.get_by_label(db, obj.label)
|
||||||
if dict_data:
|
if dict_data:
|
||||||
@@ -36,6 +58,13 @@ class DictDataService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDictDataParam) -> int:
|
async def update(*, pk: int, obj: UpdateDictDataParam) -> int:
|
||||||
|
"""
|
||||||
|
更新字典数据
|
||||||
|
|
||||||
|
:param pk: 字典数据 ID
|
||||||
|
:param obj: 字典数据更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dict_data = await dict_data_dao.get(db, pk)
|
dict_data = await dict_data_dao.get(db, pk)
|
||||||
if not dict_data:
|
if not dict_data:
|
||||||
@@ -51,6 +80,12 @@ class DictDataService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除字典数据
|
||||||
|
|
||||||
|
:param pk: 字典数据 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await dict_data_dao.delete(db, pk)
|
count = await dict_data_dao.delete(db, pk)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -9,12 +9,28 @@ from backend.database.db import async_db_session
|
|||||||
|
|
||||||
|
|
||||||
class DictTypeService:
|
class DictTypeService:
|
||||||
|
"""字典类型服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str = None, code: str = None, status: int = None) -> Select:
|
async def get_select(*, name: str | None = None, code: str | None = None, status: int | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取字典类型列表查询条件
|
||||||
|
|
||||||
|
:param name: 字典类型名称
|
||||||
|
:param code: 字典类型编码
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await dict_type_dao.get_list(name=name, code=code, status=status)
|
return await dict_type_dao.get_list(name=name, code=code, status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateDictTypeParam) -> None:
|
async def create(*, obj: CreateDictTypeParam) -> None:
|
||||||
|
"""
|
||||||
|
创建字典类型
|
||||||
|
|
||||||
|
:param obj: 字典类型创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dict_type = await dict_type_dao.get_by_code(db, obj.code)
|
dict_type = await dict_type_dao.get_by_code(db, obj.code)
|
||||||
if dict_type:
|
if dict_type:
|
||||||
@@ -23,6 +39,13 @@ class DictTypeService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateDictTypeParam) -> int:
|
async def update(*, pk: int, obj: UpdateDictTypeParam) -> int:
|
||||||
|
"""
|
||||||
|
更新字典类型
|
||||||
|
|
||||||
|
:param pk: 字典类型 ID
|
||||||
|
:param obj: 字典类型更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
dict_type = await dict_type_dao.get(db, pk)
|
dict_type = await dict_type_dao.get(db, pk)
|
||||||
if not dict_type:
|
if not dict_type:
|
||||||
@@ -35,6 +58,12 @@ class DictTypeService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除字典类型
|
||||||
|
|
||||||
|
:param pk: 字典类型 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await dict_type_dao.delete(db, pk)
|
count = await dict_type_dao.delete(db, pk)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -13,8 +13,18 @@ from backend.database.db import async_db_session
|
|||||||
|
|
||||||
|
|
||||||
class LoginLogService:
|
class LoginLogService:
|
||||||
|
"""登录日志服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, username: str, status: int, ip: str) -> Select:
|
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取登录日志列表查询条件
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:param status: 状态
|
||||||
|
:param ip: IP 地址
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await login_log_dao.get_list(username=username, status=status, ip=ip)
|
return await login_log_dao.get_list(username=username, status=status, ip=ip)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -28,8 +38,20 @@ class LoginLogService:
|
|||||||
status: int,
|
status: int,
|
||||||
msg: str,
|
msg: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
创建登录日志
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param user_uuid: 用户 UUID
|
||||||
|
:param username: 用户名
|
||||||
|
:param login_time: 登录时间
|
||||||
|
:param status: 状态
|
||||||
|
:param msg: 消息
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
obj_in = CreateLoginLogParam(
|
obj = CreateLoginLogParam(
|
||||||
user_uuid=user_uuid,
|
user_uuid=user_uuid,
|
||||||
username=username,
|
username=username,
|
||||||
status=status,
|
status=status,
|
||||||
@@ -44,18 +66,25 @@ class LoginLogService:
|
|||||||
msg=msg,
|
msg=msg,
|
||||||
login_time=login_time,
|
login_time=login_time,
|
||||||
)
|
)
|
||||||
await login_log_dao.create(db, obj_in)
|
await login_log_dao.create(db, obj)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f'登录日志创建失败: {e}')
|
log.error(f'登录日志创建失败: {e}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除登录日志
|
||||||
|
|
||||||
|
:param pk: 日志 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await login_log_dao.delete(db, pk)
|
count = await login_log_dao.delete(db, pk)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete_all() -> int:
|
async def delete_all() -> int:
|
||||||
|
"""清空所有登录日志"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await login_log_dao.delete_all(db)
|
count = await login_log_dao.delete_all(db)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -16,8 +16,16 @@ from backend.utils.build_tree import get_tree_data
|
|||||||
|
|
||||||
|
|
||||||
class MenuService:
|
class MenuService:
|
||||||
|
"""菜单服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> Menu:
|
async def get(*, pk: int) -> Menu:
|
||||||
|
"""
|
||||||
|
获取菜单详情
|
||||||
|
|
||||||
|
:param pk: 菜单 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
menu = await menu_dao.get(db, menu_id=pk)
|
menu = await menu_dao.get(db, menu_id=pk)
|
||||||
if not menu:
|
if not menu:
|
||||||
@@ -26,6 +34,13 @@ class MenuService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]:
|
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取菜单树形结构
|
||||||
|
|
||||||
|
:param title: 菜单标题
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
menu_select = await menu_dao.get_all(db, title=title, status=status)
|
menu_select = await menu_dao.get_all(db, title=title, status=status)
|
||||||
menu_tree = get_tree_data(menu_select)
|
menu_tree = get_tree_data(menu_select)
|
||||||
@@ -33,6 +48,12 @@ class MenuService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_role_menu_tree(*, pk: int) -> list[dict[str, Any]]:
|
async def get_role_menu_tree(*, pk: int) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取角色的菜单树形结构
|
||||||
|
|
||||||
|
:param pk: 角色 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get_with_relation(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
@@ -44,6 +65,12 @@ class MenuService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
|
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取用户的菜单树形结构
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
roles = request.user.roles
|
roles = request.user.roles
|
||||||
menu_ids = []
|
menu_ids = []
|
||||||
@@ -57,6 +84,12 @@ class MenuService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateMenuParam) -> None:
|
async def create(*, obj: CreateMenuParam) -> None:
|
||||||
|
"""
|
||||||
|
创建菜单
|
||||||
|
|
||||||
|
:param obj: 菜单创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
title = await menu_dao.get_by_title(db, obj.title)
|
title = await menu_dao.get_by_title(db, obj.title)
|
||||||
if title:
|
if title:
|
||||||
@@ -69,6 +102,13 @@ class MenuService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateMenuParam) -> int:
|
async def update(*, pk: int, obj: UpdateMenuParam) -> int:
|
||||||
|
"""
|
||||||
|
更新菜单
|
||||||
|
|
||||||
|
:param pk: 菜单 ID
|
||||||
|
:param obj: 菜单更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
menu = await menu_dao.get(db, pk)
|
menu = await menu_dao.get(db, pk)
|
||||||
if not menu:
|
if not menu:
|
||||||
@@ -87,6 +127,13 @@ class MenuService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, request: Request, pk: int) -> int:
|
async def delete(*, request: Request, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
删除菜单
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 菜单 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
children = await menu_dao.get_children(db, pk)
|
children = await menu_dao.get_children(db, pk)
|
||||||
if children:
|
if children:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fast_captcha import text_captcha
|
from fast_captcha import text_captcha
|
||||||
from fastapi import BackgroundTasks, Request, Response
|
from fastapi import BackgroundTasks, Request, Response
|
||||||
|
|
||||||
@@ -20,15 +22,27 @@ from backend.utils.timezone import timezone
|
|||||||
|
|
||||||
|
|
||||||
class OAuth2Service:
|
class OAuth2Service:
|
||||||
|
"""OAuth2 认证服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create_with_login(
|
async def create_with_login(
|
||||||
*,
|
*,
|
||||||
request: Request,
|
request: Request,
|
||||||
response: Response,
|
response: Response,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
user: dict,
|
user: dict[str, Any],
|
||||||
social: UserSocialType,
|
social: UserSocialType,
|
||||||
) -> GetLoginToken | None:
|
) -> GetLoginToken | None:
|
||||||
|
"""
|
||||||
|
创建 OAuth2 用户并登录
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param response: FastAPI 响应对象
|
||||||
|
:param background_tasks: FastAPI 后台任务
|
||||||
|
:param user: OAuth2 用户信息
|
||||||
|
:param social: 社交平台类型
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
# 获取 OAuth2 平台用户信息
|
# 获取 OAuth2 平台用户信息
|
||||||
social_id = user.get('id')
|
social_id = user.get('id')
|
||||||
@@ -37,7 +51,7 @@ class OAuth2Service:
|
|||||||
social_username = user.get('login')
|
social_username = user.get('login')
|
||||||
social_nickname = user.get('name')
|
social_nickname = user.get('name')
|
||||||
social_email = user.get('email')
|
social_email = user.get('email')
|
||||||
if social == UserSocialType.linuxdo: # 不提供明文邮箱的平台
|
if social == UserSocialType.linux_do: # 不提供明文邮箱的平台
|
||||||
social_email = f'{social_username}@linux.do'
|
social_email = f'{social_username}@linux.do'
|
||||||
if not social_email:
|
if not social_email:
|
||||||
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
|
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
|
||||||
|
|||||||
@@ -8,23 +8,46 @@ from backend.database.db import async_db_session
|
|||||||
|
|
||||||
|
|
||||||
class OperaLogService:
|
class OperaLogService:
|
||||||
|
"""操作日志服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取操作日志列表查询条件
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:param status: 状态
|
||||||
|
:param ip: IP 地址
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
|
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj_in: CreateOperaLogParam):
|
async def create(*, obj: CreateOperaLogParam) -> None:
|
||||||
|
"""
|
||||||
|
创建操作日志
|
||||||
|
|
||||||
|
:param obj: 操作日志创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
await opera_log_dao.create(db, obj_in)
|
await opera_log_dao.create(db, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: list[int]) -> int:
|
async def delete(*, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除操作日志
|
||||||
|
|
||||||
|
:param pk: 日志 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await opera_log_dao.delete(db, pk)
|
count = await opera_log_dao.delete(db, pk)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete_all() -> int:
|
async def delete_all() -> int:
|
||||||
|
"""清空所有操作日志"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await opera_log_dao.delete_all(db)
|
count = await opera_log_dao.delete_all(db)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -22,8 +22,16 @@ from backend.database.redis import redis_client
|
|||||||
|
|
||||||
|
|
||||||
class RoleService:
|
class RoleService:
|
||||||
|
"""角色服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> Role:
|
async def get(*, pk: int) -> Role:
|
||||||
|
"""
|
||||||
|
获取角色详情
|
||||||
|
|
||||||
|
:param pk: 角色 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get_with_relation(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
@@ -32,22 +40,42 @@ class RoleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> Sequence[Role]:
|
async def get_all() -> Sequence[Role]:
|
||||||
|
"""获取所有角色"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
roles = await role_dao.get_all(db)
|
roles = await role_dao.get_all(db)
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_by_user(*, pk: int) -> Sequence[Role]:
|
async def get_by_user(*, pk: int) -> Sequence[Role]:
|
||||||
|
"""
|
||||||
|
获取用户的角色列表
|
||||||
|
|
||||||
|
:param pk: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
roles = await role_dao.get_by_user(db, user_id=pk)
|
roles = await role_dao.get_by_user(db, user_id=pk)
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str = None, status: int = None) -> Select:
|
async def get_select(*, name: str | None = None, status: int | None = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取角色列表查询条件
|
||||||
|
|
||||||
|
:param name: 角色名称
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await role_dao.get_list(name=name, status=status)
|
return await role_dao.get_list(name=name, status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateRoleParam) -> None:
|
async def create(*, obj: CreateRoleParam) -> None:
|
||||||
|
"""
|
||||||
|
创建角色
|
||||||
|
|
||||||
|
:param obj: 角色创建参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
role = await role_dao.get_by_name(db, obj.name)
|
role = await role_dao.get_by_name(db, obj.name)
|
||||||
if role:
|
if role:
|
||||||
@@ -56,6 +84,13 @@ class RoleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateRoleParam) -> int:
|
async def update(*, pk: int, obj: UpdateRoleParam) -> int:
|
||||||
|
"""
|
||||||
|
更新角色
|
||||||
|
|
||||||
|
:param pk: 角色 ID
|
||||||
|
:param obj: 角色更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
role = await role_dao.get(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
@@ -69,6 +104,14 @@ class RoleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_role_menu(*, request: Request, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
|
async def update_role_menu(*, request: Request, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
|
||||||
|
"""
|
||||||
|
更新角色菜单
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 角色 ID
|
||||||
|
:param menu_ids: 菜单 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
role = await role_dao.get(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
@@ -84,6 +127,14 @@ class RoleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_role_rule(*, request: Request, pk: int, rule_ids: UpdateRoleRuleParam) -> int:
|
async def update_role_rule(*, request: Request, pk: int, rule_ids: UpdateRoleRuleParam) -> int:
|
||||||
|
"""
|
||||||
|
更新角色数据权限
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 角色 ID
|
||||||
|
:param rule_ids: 权限规则 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
role = await role_dao.get(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
@@ -99,6 +150,13 @@ class RoleService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, request: Request, pk: list[int]) -> int:
|
async def delete(*, request: Request, pk: list[int]) -> int:
|
||||||
|
"""
|
||||||
|
删除角色
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 角色 ID 列表
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await role_dao.delete(db, pk)
|
count = await role_dao.delete(db, pk)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
||||||
|
|||||||
@@ -25,8 +25,16 @@ from backend.database.redis import redis_client
|
|||||||
|
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
|
"""用户服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def register(*, obj: RegisterUserParam) -> None:
|
async def register(*, obj: RegisterUserParam) -> None:
|
||||||
|
"""
|
||||||
|
注册新用户
|
||||||
|
|
||||||
|
:param obj: 用户注册参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
if not obj.password:
|
if not obj.password:
|
||||||
raise errors.ForbiddenError(msg='密码为空')
|
raise errors.ForbiddenError(msg='密码为空')
|
||||||
@@ -44,6 +52,13 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def add(*, request: Request, obj: AddUserParam) -> None:
|
async def add(*, request: Request, obj: AddUserParam) -> None:
|
||||||
|
"""
|
||||||
|
添加新用户
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param obj: 用户添加参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
username = await user_dao.get_by_username(db, obj.username)
|
username = await user_dao.get_by_username(db, obj.username)
|
||||||
@@ -69,13 +84,20 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int:
|
async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int:
|
||||||
|
"""
|
||||||
|
重置用户密码
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param obj: 密码重置参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
user = await user_dao.get(db, request.user.id)
|
user = await user_dao.get(db, request.user.id)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if not password_verify(obj.old_password, user.password):
|
if not password_verify(obj.old_password, user.password):
|
||||||
raise errors.ForbiddenError(msg='原密码错误')
|
raise errors.ForbiddenError(msg='原密码错误')
|
||||||
np1 = obj.new_password
|
if obj.new_password != obj.confirm_password:
|
||||||
np2 = obj.confirm_password
|
|
||||||
if np1 != np2:
|
|
||||||
raise errors.ForbiddenError(msg='密码输入不一致')
|
raise errors.ForbiddenError(msg='密码输入不一致')
|
||||||
new_pwd = get_hash_password(obj.new_password, user.salt)
|
new_pwd = get_hash_password(obj.new_password, user.salt)
|
||||||
count = await user_dao.reset_password(db, request.user.id, new_pwd)
|
count = await user_dao.reset_password(db, request.user.id, new_pwd)
|
||||||
@@ -90,6 +112,12 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_userinfo(*, username: str) -> User:
|
async def get_userinfo(*, username: str) -> User:
|
||||||
|
"""
|
||||||
|
获取用户信息
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
user = await user_dao.get_with_relation(db, username=username)
|
user = await user_dao.get_with_relation(db, username=username)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -98,10 +126,17 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
|
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
|
||||||
|
"""
|
||||||
|
更新用户信息
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param username: 用户名
|
||||||
|
:param obj: 用户更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser and request.user.username != username:
|
||||||
if request.user.username != username:
|
raise errors.ForbiddenError(msg='你只能修改自己的信息')
|
||||||
raise errors.ForbiddenError(msg='你只能修改自己的信息')
|
|
||||||
input_user = await user_dao.get_with_relation(db, username=username)
|
input_user = await user_dao.get_with_relation(db, username=username)
|
||||||
if not input_user:
|
if not input_user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
@@ -123,10 +158,17 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_roles(*, request: Request, username: str, obj: UpdateUserRoleParam) -> None:
|
async def update_roles(*, request: Request, username: str, obj: UpdateUserRoleParam) -> None:
|
||||||
|
"""
|
||||||
|
更新用户角色
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param username: 用户名
|
||||||
|
:param obj: 角色更新参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser and request.user.username != username:
|
||||||
if request.user.username != username:
|
raise errors.AuthorizationError
|
||||||
raise errors.AuthorizationError
|
|
||||||
input_user = await user_dao.get_with_relation(db, username=username)
|
input_user = await user_dao.get_with_relation(db, username=username)
|
||||||
if not input_user:
|
if not input_user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
@@ -139,10 +181,17 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_avatar(*, request: Request, username: str, avatar: AvatarParam) -> int:
|
async def update_avatar(*, request: Request, username: str, avatar: AvatarParam) -> int:
|
||||||
|
"""
|
||||||
|
更新用户头像
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param username: 用户名
|
||||||
|
:param avatar: 头像参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser and request.user.username != username:
|
||||||
if request.user.username != username:
|
raise errors.AuthorizationError
|
||||||
raise errors.AuthorizationError
|
|
||||||
input_user = await user_dao.get_by_username(db, username)
|
input_user = await user_dao.get_by_username(db, username)
|
||||||
if not input_user:
|
if not input_user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
@@ -152,96 +201,138 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select:
|
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select:
|
||||||
|
"""
|
||||||
|
获取用户列表查询条件
|
||||||
|
|
||||||
|
:param dept: 部门 ID
|
||||||
|
:param username: 用户名
|
||||||
|
:param phone: 手机号
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
|
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_permission(*, request: Request, pk: int) -> int:
|
async def update_permission(*, request: Request, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
更新用户权限
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
if not await user_dao.get(db, pk):
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
else:
|
if pk == request.user.id:
|
||||||
if pk == request.user.id:
|
raise errors.ForbiddenError(msg='非法操作')
|
||||||
raise errors.ForbiddenError(msg='非法操作')
|
super_status = await user_dao.get_super(db, pk)
|
||||||
super_status = await user_dao.get_super(db, pk)
|
count = await user_dao.set_super(db, pk, not super_status)
|
||||||
count = await user_dao.set_super(db, pk, False if super_status else True)
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_staff(*, request: Request, pk: int) -> int:
|
async def update_staff(*, request: Request, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
更新用户职员状态
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
if not await user_dao.get(db, pk):
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
else:
|
if pk == request.user.id:
|
||||||
if pk == request.user.id:
|
raise errors.ForbiddenError(msg='非法操作')
|
||||||
raise errors.ForbiddenError(msg='非法操作')
|
staff_status = await user_dao.get_staff(db, pk)
|
||||||
staff_status = await user_dao.get_staff(db, pk)
|
count = await user_dao.set_staff(db, pk, not staff_status)
|
||||||
count = await user_dao.set_staff(db, pk, False if staff_status else True)
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_status(*, request: Request, pk: int) -> int:
|
async def update_status(*, request: Request, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
更新用户状态
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
if not await user_dao.get(db, pk):
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
else:
|
if pk == request.user.id:
|
||||||
if pk == request.user.id:
|
raise errors.ForbiddenError(msg='非法操作')
|
||||||
raise errors.ForbiddenError(msg='非法操作')
|
status = await user_dao.get_status(db, pk)
|
||||||
status = await user_dao.get_status(db, pk)
|
count = await user_dao.set_status(db, pk, 0 if status == 1 else 1)
|
||||||
count = await user_dao.set_status(db, pk, False if status else True)
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
|
return count
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_multi_login(*, request: Request, pk: int) -> int:
|
async def update_multi_login(*, request: Request, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
更新用户多端登录状态
|
||||||
|
|
||||||
|
:param request: FastAPI 请求对象
|
||||||
|
:param pk: 用户 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
if not await user_dao.get(db, pk):
|
user = await user_dao.get(db, pk)
|
||||||
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
user_id = request.user.id
|
||||||
|
multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login
|
||||||
|
count = await user_dao.set_multi_login(db, pk, not multi_login)
|
||||||
|
# 删除当前用户缓存
|
||||||
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
||||||
|
token = get_token(request)
|
||||||
|
token_payload = jwt_decode(token)
|
||||||
|
latest_multi_login = await user_dao.get_multi_login(db, pk)
|
||||||
|
# 超级用户修改自身时,除当前 token 外,其他 token 失效
|
||||||
|
if pk == user_id:
|
||||||
|
if not latest_multi_login:
|
||||||
|
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}'
|
||||||
|
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}')
|
||||||
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
if refresh_token:
|
||||||
|
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}'
|
||||||
|
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{refresh_token}')
|
||||||
|
# 超级用户修改他人时,其他 token 将全部失效
|
||||||
else:
|
else:
|
||||||
user_id = request.user.id
|
if not latest_multi_login:
|
||||||
multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login
|
key_prefix = [f'{settings.TOKEN_REDIS_PREFIX}:{pk}']
|
||||||
count = await user_dao.set_multi_login(db, pk, False if multi_login else True)
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
|
if refresh_token:
|
||||||
token = get_token(request)
|
key_prefix.append(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}')
|
||||||
token_payload = jwt_decode(token)
|
for prefix in key_prefix:
|
||||||
latest_multi_login = await user_dao.get_multi_login(db, pk)
|
await redis_client.delete_prefix(prefix)
|
||||||
# 超级用户修改自身时,除当前token外,其他token失效
|
return count
|
||||||
if pk == user_id:
|
|
||||||
if not latest_multi_login:
|
|
||||||
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}'
|
|
||||||
await redis_client.delete_prefix(
|
|
||||||
key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}'
|
|
||||||
)
|
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
|
||||||
if refresh_token:
|
|
||||||
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}'
|
|
||||||
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{refresh_token}')
|
|
||||||
# 超级用户修改他人时,其他token将全部失效
|
|
||||||
else:
|
|
||||||
if not latest_multi_login:
|
|
||||||
key_prefix = [f'{settings.TOKEN_REDIS_PREFIX}:{pk}']
|
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
|
||||||
if refresh_token:
|
|
||||||
key_prefix.append(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}')
|
|
||||||
for prefix in key_prefix:
|
|
||||||
await redis_client.delete_prefix(prefix)
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, username: str) -> int:
|
async def delete(*, username: str) -> int:
|
||||||
|
"""
|
||||||
|
删除用户
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
input_user = await user_dao.get_by_username(db, username)
|
user = await user_dao.get_by_username(db, username)
|
||||||
if not input_user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
count = await user_dao.delete(db, input_user.id)
|
count = await user_dao.delete(db, user.id)
|
||||||
key_prefix = [
|
key_prefix = [
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{input_user.id}',
|
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{input_user.id}',
|
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||||
]
|
]
|
||||||
for key in key_prefix:
|
for key in key_prefix:
|
||||||
await redis_client.delete_prefix(key)
|
await redis_client.delete_prefix(key)
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
from backend.database.db import create_database_url, create_engine_and_session
|
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)
|
TEST_SQLALCHEMY_DATABASE_URL = create_database_url(unittest=True)
|
||||||
|
|
||||||
_, async_test_db_session = create_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL)
|
_, async_test_db_session = create_async_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL)
|
||||||
|
|
||||||
|
|
||||||
async def override_get_db() -> AsyncSession:
|
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
"""session 生成器"""
|
"""session 生成器"""
|
||||||
session = async_test_db_session()
|
async with async_test_db_session() as session:
|
||||||
try:
|
|
||||||
yield session
|
yield session
|
||||||
except Exception as se:
|
|
||||||
await session.rollback()
|
|
||||||
raise se
|
|
||||||
finally:
|
|
||||||
await session.close()
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get('/tables', summary='获取数据库表')
|
@router.get('/tables', summary='获取数据库表')
|
||||||
async def get_all_tables(
|
async def get_all_tables(
|
||||||
table_schema: Annotated[str, Query(..., description='数据库名')] = 'fba',
|
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
|
||||||
) -> ResponseSchemaModel[list[str]]:
|
) -> ResponseSchemaModel[list[str]]:
|
||||||
data = await gen_service.get_tables(table_schema=table_schema)
|
data = await gen_service.get_tables(table_schema=table_schema)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
@@ -38,13 +38,13 @@ async def import_table(obj: ImportParam) -> ResponseModel:
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth])
|
@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth])
|
||||||
async def preview_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
|
async def preview_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
|
||||||
data = await gen_service.preview(pk=pk)
|
data = await gen_service.preview(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
|
@router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
|
||||||
async def generate_path(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseSchemaModel[list[str]]:
|
async def generate_path(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
|
||||||
data = await gen_service.get_generate_path(pk=pk)
|
data = await gen_service.get_generate_path(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
@@ -58,13 +58,13 @@ async def generate_path(pk: Annotated[int, Path(..., description='业务ID')]) -
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def generate_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseModel:
|
async def generate_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseModel:
|
||||||
await gen_service.generate(pk=pk)
|
await gen_service.generate(pk=pk)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
|
@router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
|
||||||
async def download_code(pk: Annotated[int, Path(..., description='业务ID')]):
|
async def download_code(pk: Annotated[int, Path(description='业务 ID')]):
|
||||||
bio = await gen_service.download(pk=pk)
|
bio = await gen_service.download(pk=pk)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
bio,
|
bio,
|
||||||
|
|||||||
@@ -16,29 +16,29 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
|
|||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.utils.serializers import select_as_dict, select_list_serialize
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
|
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
|
||||||
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
|
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
|
||||||
businesses = await gen_business_service.get_all()
|
data = await gen_business_service.get_all()
|
||||||
data = select_list_serialize(businesses)
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_business(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetGenBusinessDetail]:
|
async def get_business(
|
||||||
business = await gen_business_service.get(pk=pk)
|
pk: Annotated[int, Path(description='业务 ID')],
|
||||||
data = GetGenBusinessDetail(**select_as_dict(business))
|
) -> ResponseSchemaModel[GetGenBusinessDetail]:
|
||||||
|
data = await gen_business_service.get(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
|
||||||
async def get_business_all_models(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetGenModelDetail]]:
|
async def get_business_all_models(
|
||||||
models = await gen_model_service.get_by_business(business_id=pk)
|
pk: Annotated[int, Path(description='业务 ID')],
|
||||||
data = select_list_serialize(models)
|
) -> ResponseSchemaModel[list[GetGenModelDetail]]:
|
||||||
|
data = await gen_model_service.get_by_business(business_id=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,9 @@ async def create_business(obj: CreateGenBusinessParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessParam) -> ResponseModel:
|
async def update_business(
|
||||||
|
pk: Annotated[int, Path(description='业务 ID')], obj: UpdateGenBusinessParam
|
||||||
|
) -> ResponseModel:
|
||||||
count = await gen_business_service.update(pk=pk, obj=obj)
|
count = await gen_business_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -79,7 +81,7 @@ async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessP
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_business(pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def delete_business(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseModel:
|
||||||
count = await gen_business_service.delete(pk=pk)
|
count = await gen_business_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
|
|||||||
from backend.common.security.jwt import DependsJwtAuth
|
from backend.common.security.jwt import DependsJwtAuth
|
||||||
from backend.common.security.permission import RequestPermission
|
from backend.common.security.permission import RequestPermission
|
||||||
from backend.common.security.rbac import DependsRBAC
|
from backend.common.security.rbac import DependsRBAC
|
||||||
from backend.utils.serializers import select_as_dict
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -22,9 +21,8 @@ async def get_model_types() -> ResponseSchemaModel[list[str]]:
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_model(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetGenModelDetail]:
|
async def get_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseSchemaModel[GetGenModelDetail]:
|
||||||
model = await gen_model_service.get(pk=pk)
|
data = await gen_model_service.get(pk=pk)
|
||||||
data = GetGenModelDetail(**select_as_dict(model))
|
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,7 +47,7 @@ async def create_model(obj: CreateGenModelParam) -> ResponseModel:
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam) -> ResponseModel:
|
async def update_model(pk: Annotated[int, Path(description='模型 ID')], obj: UpdateGenModelParam) -> ResponseModel:
|
||||||
count = await gen_model_service.update(pk=pk, obj=obj)
|
count = await gen_model_service.update(pk=pk, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -64,7 +62,7 @@ async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam)
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_model(pk: Annotated[int, Path(...)]) -> ResponseModel:
|
async def delete_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseModel:
|
||||||
count = await gen_model_service.delete(pk=pk)
|
count = await gen_model_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from pydantic_settings import BaseSettings
|
|||||||
|
|
||||||
|
|
||||||
class GeneratorSettings(BaseSettings):
|
class GeneratorSettings(BaseSettings):
|
||||||
"""Admin Settings"""
|
"""代码生成配置"""
|
||||||
|
|
||||||
# 模版目录
|
# 模版
|
||||||
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
|
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
|
||||||
|
|
||||||
# 代码下载
|
# 代码下载
|
||||||
@@ -17,7 +17,7 @@ class GeneratorSettings(BaseSettings):
|
|||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_generator_settings() -> GeneratorSettings:
|
def get_generator_settings() -> GeneratorSettings:
|
||||||
"""获取 generator 配置"""
|
"""获取代码生成配置"""
|
||||||
return GeneratorSettings()
|
return GeneratorSettings()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,17 @@ from backend.core.conf import settings
|
|||||||
|
|
||||||
|
|
||||||
class CRUDGen:
|
class CRUDGen:
|
||||||
|
"""代码生成 CRUD 类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
|
async def get_all_tables(db: AsyncSession, table_schema: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
获取所有表名
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param table_schema: 数据库 schema 名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
sql = """
|
sql = """
|
||||||
SELECT table_name AS table_name FROM information_schema.tables
|
SELECT table_name AS table_name FROM information_schema.tables
|
||||||
@@ -30,6 +39,13 @@ class CRUDGen:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]:
|
async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]:
|
||||||
|
"""
|
||||||
|
获取表信息
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param table_name: 表名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
sql = """
|
sql = """
|
||||||
SELECT table_name AS table_name, table_comment AS table_comment FROM information_schema.tables
|
SELECT table_name AS table_name, table_comment AS table_comment FROM information_schema.tables
|
||||||
@@ -51,6 +67,14 @@ class CRUDGen:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all_columns(db: AsyncSession, table_schema: str, table_name: str) -> Sequence[Row[tuple]]:
|
async def get_all_columns(db: AsyncSession, table_schema: str, table_name: str) -> Sequence[Row[tuple]]:
|
||||||
|
"""
|
||||||
|
获取所有列信息
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param table_schema: 数据库 schema 名称
|
||||||
|
:param table_name: 表名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if settings.DATABASE_TYPE == 'mysql':
|
if settings.DATABASE_TYPE == 'mysql':
|
||||||
sql = """
|
sql = """
|
||||||
SELECT column_name AS column_name,
|
SELECT column_name AS column_name,
|
||||||
|
|||||||
@@ -10,61 +10,64 @@ from backend.app.generator.schema.gen_business import CreateGenBusinessParam, Up
|
|||||||
|
|
||||||
|
|
||||||
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
|
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
|
||||||
|
"""代码生成业务 CRUD 类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> GenBusiness | None:
|
async def get(self, db: AsyncSession, pk: int) -> GenBusiness | None:
|
||||||
"""
|
"""
|
||||||
获取代码生成业务表
|
获取代码生成业务
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 代码生成业务 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, pk)
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
async def get_by_name(self, db: AsyncSession, name: str) -> GenBusiness | None:
|
async def get_by_name(self, db: AsyncSession, name: str) -> GenBusiness | None:
|
||||||
"""
|
"""
|
||||||
通过 name 获取代码生成业务表
|
通过 name 获取代码生成业务
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param name:
|
:param name: 表名
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, table_name_en=name)
|
return await self.select_model_by_column(db, table_name_en=name)
|
||||||
|
|
||||||
async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]:
|
async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]:
|
||||||
"""
|
"""
|
||||||
获取所有代码生成业务表
|
获取所有代码生成业务
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateGenBusinessParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateGenBusinessParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建代码生成业务表
|
创建代码生成业务
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建代码生成业务参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in)
|
await self.create_model(db, obj)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenBusinessParam) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenBusinessParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新代码生成业务表
|
更新代码生成业务
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 代码生成业务 ID
|
||||||
:param obj_in:
|
:param obj: 更新代码生成业务参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in)
|
return await self.update_model(db, pk, obj)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: int) -> int:
|
async def delete(self, db: AsyncSession, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除代码生成业务表
|
删除代码生成业务
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 代码生成业务 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, pk)
|
return await self.delete_model(db, pk)
|
||||||
|
|||||||
@@ -10,53 +10,57 @@ from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGe
|
|||||||
|
|
||||||
|
|
||||||
class CRUDGenModel(CRUDPlus[GenModel]):
|
class CRUDGenModel(CRUDPlus[GenModel]):
|
||||||
|
"""代码生成模型 CRUD 类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int) -> GenModel | None:
|
async def get(self, db: AsyncSession, pk: int) -> GenModel | None:
|
||||||
"""
|
"""
|
||||||
获取代码生成模型列
|
获取代码生成模型列
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param pk: 代码生成模型 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_model(db, pk)
|
return await self.select_model(db, pk)
|
||||||
|
|
||||||
async def get_all_by_business_id(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
|
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
|
||||||
"""
|
"""
|
||||||
获取所有代码生成模型列
|
获取所有代码生成模型列
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param business_id:
|
:param business_id: 业务 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
|
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj_in: CreateGenModelParam, pd_type: str | None = None) -> None:
|
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
创建代码生成模型表
|
创建代码生成模型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param obj_in:
|
:param obj: 创建代码生成模型参数
|
||||||
:param pd_type:
|
:param pd_type: Pydantic 类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await self.create_model(db, obj_in, pd_type=pd_type)
|
await self.create_model(db, obj, pd_type=pd_type)
|
||||||
|
|
||||||
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenModelParam, pd_type: str | None = None) -> int:
|
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None = None) -> int:
|
||||||
"""
|
"""
|
||||||
更细代码生成模型表
|
更新代码生成模型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 代码生成模型 ID
|
||||||
:param obj_in:
|
:param obj: 更新代码生成模型参数
|
||||||
:param pd_type:
|
:param pd_type: Pydantic 类型
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, pk, obj_in, pd_type=pd_type)
|
return await self.update_model(db, pk, obj, pd_type=pd_type)
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, pk: int) -> int:
|
async def delete(self, db: AsyncSession, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
删除代码生成模型表
|
删除代码生成模型
|
||||||
|
|
||||||
:param db:
|
:param db: 数据库会话
|
||||||
:param pk:
|
:param pk: 代码生成模型 ID
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.delete_model(db, pk)
|
return await self.delete_model(db, pk)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
@@ -7,6 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
|
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.generator.model import GenModel
|
||||||
|
|
||||||
|
|
||||||
class GenBusiness(Base):
|
class GenBusiness(Base):
|
||||||
"""代码生成业务表"""
|
"""代码生成业务表"""
|
||||||
@@ -28,4 +33,4 @@ class GenBusiness(Base):
|
|||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
||||||
)
|
)
|
||||||
# 代码生成业务模型一对多
|
# 代码生成业务模型一对多
|
||||||
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business') # noqa: F821
|
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Union
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from sqlalchemy import ForeignKey, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
@@ -9,6 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||||||
|
|
||||||
from backend.common.model import DataClassBase, id_key
|
from backend.common.model import DataClassBase, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.generator.model import GenBusiness
|
||||||
|
|
||||||
|
|
||||||
class GenModel(DataClassBase):
|
class GenModel(DataClassBase):
|
||||||
"""代码生成模型表"""
|
"""代码生成模型表"""
|
||||||
@@ -32,4 +35,4 @@ class GenModel(DataClassBase):
|
|||||||
gen_business_id: Mapped[int] = mapped_column(
|
gen_business_id: Mapped[int] = mapped_column(
|
||||||
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
|
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
|
||||||
)
|
)
|
||||||
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model') # noqa: F821
|
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model')
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from backend.common.schema import SchemaBase
|
|||||||
|
|
||||||
|
|
||||||
class ImportParam(SchemaBase):
|
class ImportParam(SchemaBase):
|
||||||
|
"""导入参数"""
|
||||||
|
|
||||||
app: str = Field(description='应用名称,用于代码生成到指定 app')
|
app: str = Field(description='应用名称,用于代码生成到指定 app')
|
||||||
table_name: str = Field(description='数据库表名')
|
table_name: str = Field(description='数据库表名')
|
||||||
table_schema: str = Field(description='数据库名')
|
table_schema: str = Field(description='数据库名')
|
||||||
|
|||||||
@@ -5,41 +5,44 @@ from datetime import datetime
|
|||||||
from pydantic import ConfigDict, Field, model_validator
|
from pydantic import ConfigDict, Field, model_validator
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from backend.app.generator.schema.gen_model import GetGenModelDetail
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class GenBusinessSchemaBase(SchemaBase):
|
class GenBusinessSchemaBase(SchemaBase):
|
||||||
app_name: str
|
"""代码生成业务基础模型"""
|
||||||
table_name_en: str
|
|
||||||
table_name_zh: str
|
app_name: str = Field(description='应用名称(英文)')
|
||||||
table_simple_name_zh: str
|
table_name_en: str = Field(description='表名称(英文)')
|
||||||
table_comment: str | None = None
|
table_name_zh: str = Field(description='表名称(中文)')
|
||||||
schema_name: str | None = None
|
table_simple_name_zh: str = Field(description='表名称(中文简称)')
|
||||||
default_datetime_column: bool = Field(default=True)
|
table_comment: str | None = Field(None, description='表描述')
|
||||||
api_version: str = Field(default='v1')
|
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
|
||||||
gen_path: str | None = None
|
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
|
||||||
remark: str | None = None
|
api_version: str = Field('v1', description='代码生成 api 版本')
|
||||||
|
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
|
||||||
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
@model_validator(mode='after')
|
@model_validator(mode='after')
|
||||||
def check_schema_name(self) -> Self:
|
def check_schema_name(self) -> Self:
|
||||||
|
"""检查并设置 schema 名称"""
|
||||||
if self.schema_name is None:
|
if self.schema_name is None:
|
||||||
self.schema_name = self.table_name_en
|
self.schema_name = self.table_name_en
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class CreateGenBusinessParam(GenBusinessSchemaBase):
|
class CreateGenBusinessParam(GenBusinessSchemaBase):
|
||||||
pass
|
"""创建代码生成业务参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateGenBusinessParam(GenBusinessSchemaBase):
|
class UpdateGenBusinessParam(GenBusinessSchemaBase):
|
||||||
pass
|
"""更新代码生成业务参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetGenBusinessDetail(GenBusinessSchemaBase):
|
class GetGenBusinessDetail(GenBusinessSchemaBase):
|
||||||
|
"""获取代码生成业务详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='主键 ID')
|
||||||
created_time: datetime
|
created_time: datetime = Field(description='创建时间')
|
||||||
updated_time: datetime | None = None
|
updated_time: datetime | None = Field(None, description='更新时间')
|
||||||
gen_model: list[GetGenModelDetail] | None = None
|
|
||||||
|
|||||||
@@ -7,32 +7,37 @@ from backend.utils.type_conversion import sql_type_to_sqlalchemy
|
|||||||
|
|
||||||
|
|
||||||
class GenModelSchemaBase(SchemaBase):
|
class GenModelSchemaBase(SchemaBase):
|
||||||
name: str
|
"""代码生成模型基础模型"""
|
||||||
comment: str | None = None
|
|
||||||
type: str
|
name: str = Field(description='列名称')
|
||||||
default: str | None = None
|
comment: str | None = Field(None, description='列描述')
|
||||||
sort: int
|
type: str = Field(description='SQLA 模型列类型')
|
||||||
length: int
|
default: str | None = Field(None, description='列默认值')
|
||||||
is_pk: bool = Field(default=False)
|
sort: int = Field(description='列排序')
|
||||||
is_nullable: bool = Field(default=False)
|
length: int = Field(description='列长度')
|
||||||
gen_business_id: int | None = Field(ge=1)
|
is_pk: bool = Field(False, description='是否主键')
|
||||||
|
is_nullable: bool = Field(False, description='是否可为空')
|
||||||
|
gen_business_id: int = Field(description='代码生成业务ID')
|
||||||
|
|
||||||
@field_validator('type')
|
@field_validator('type')
|
||||||
@classmethod
|
@classmethod
|
||||||
def type_update(cls, v):
|
def type_update(cls, v: str) -> str:
|
||||||
|
"""更新列类型"""
|
||||||
return sql_type_to_sqlalchemy(v)
|
return sql_type_to_sqlalchemy(v)
|
||||||
|
|
||||||
|
|
||||||
class CreateGenModelParam(GenModelSchemaBase):
|
class CreateGenModelParam(GenModelSchemaBase):
|
||||||
pass
|
"""创建代码生成模型参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateGenModelParam(GenModelSchemaBase):
|
class UpdateGenModelParam(GenModelSchemaBase):
|
||||||
pass
|
"""更新代码生成模型参数"""
|
||||||
|
|
||||||
|
|
||||||
class GetGenModelDetail(GenModelSchemaBase):
|
class GetGenModelDetail(GenModelSchemaBase):
|
||||||
|
"""获取代码生成模型详情"""
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int = Field(description='主键 ID')
|
||||||
pd_type: str
|
pd_type: str = Field(description='列类型对应的 pydantic 类型')
|
||||||
|
|||||||
@@ -10,8 +10,16 @@ from backend.database.db import async_db_session
|
|||||||
|
|
||||||
|
|
||||||
class GenBusinessService:
|
class GenBusinessService:
|
||||||
|
"""代码生成业务服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> GenBusiness:
|
async def get(*, pk: int) -> GenBusiness:
|
||||||
|
"""
|
||||||
|
获取指定 ID 的业务
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
business = await gen_business_dao.get(db, pk)
|
business = await gen_business_dao.get(db, pk)
|
||||||
if not business:
|
if not business:
|
||||||
@@ -20,12 +28,18 @@ class GenBusinessService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> Sequence[GenBusiness]:
|
async def get_all() -> Sequence[GenBusiness]:
|
||||||
|
"""获取所有业务"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
businesses = await gen_business_dao.get_all(db)
|
return await gen_business_dao.get_all(db)
|
||||||
return businesses
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateGenBusinessParam) -> None:
|
async def create(*, obj: CreateGenBusinessParam) -> None:
|
||||||
|
"""
|
||||||
|
创建业务
|
||||||
|
|
||||||
|
:param obj: 创建业务参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
business = await gen_business_dao.get_by_name(db, obj.table_name_en)
|
business = await gen_business_dao.get_by_name(db, obj.table_name_en)
|
||||||
if business:
|
if business:
|
||||||
@@ -34,15 +48,26 @@ class GenBusinessService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateGenBusinessParam) -> int:
|
async def update(*, pk: int, obj: UpdateGenBusinessParam) -> int:
|
||||||
|
"""
|
||||||
|
更新业务
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:param obj: 更新业务参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await gen_business_dao.update(db, pk, obj)
|
return await gen_business_dao.update(db, pk, obj)
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: int) -> int:
|
async def delete(*, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
删除业务
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await gen_business_dao.delete(db, pk)
|
return await gen_business_dao.delete(db, pk)
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
gen_business_service: GenBusinessService = GenBusinessService()
|
gen_business_service: GenBusinessService = GenBusinessService()
|
||||||
|
|||||||
@@ -12,50 +12,85 @@ from backend.utils.type_conversion import sql_type_to_pydantic
|
|||||||
|
|
||||||
|
|
||||||
class GenModelService:
|
class GenModelService:
|
||||||
|
"""代码生成模型服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get(*, pk: int) -> GenModel:
|
async def get(*, pk: int) -> GenModel:
|
||||||
|
"""
|
||||||
|
获取指定 ID 的模型
|
||||||
|
|
||||||
|
:param pk: 模型 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
gen_model = await gen_model_dao.get(db, pk)
|
model = await gen_model_dao.get(db, pk)
|
||||||
return gen_model
|
if not model:
|
||||||
|
raise errors.NotFoundError(msg='代码生成模型不存在')
|
||||||
|
return model
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_types() -> list[str]:
|
async def get_types() -> list[str]:
|
||||||
|
"""获取所有 MySQL 列类型"""
|
||||||
types = GenModelMySQLColumnType.get_member_keys()
|
types = GenModelMySQLColumnType.get_member_keys()
|
||||||
types.sort()
|
types.sort()
|
||||||
return types
|
return types
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_by_business(*, business_id: int) -> Sequence[GenModel]:
|
async def get_by_business(*, business_id: int) -> Sequence[GenModel]:
|
||||||
|
"""
|
||||||
|
获取指定业务的所有模型
|
||||||
|
|
||||||
|
:param business_id: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
gen_models = await gen_model_dao.get_all_by_business_id(db, business_id)
|
return await gen_model_dao.get_all_by_business(db, business_id)
|
||||||
return gen_models
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(*, obj: CreateGenModelParam) -> None:
|
async def create(*, obj: CreateGenModelParam) -> None:
|
||||||
|
"""
|
||||||
|
创建模型
|
||||||
|
|
||||||
|
:param obj: 创建模型参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
gen_models = await gen_model_dao.get_all_by_business_id(db, obj.gen_business_id)
|
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
|
||||||
if obj.name in [gen_model.name for gen_model in gen_models]:
|
if obj.name in [gen_model.name for gen_model in gen_models]:
|
||||||
raise errors.ForbiddenError(msg='禁止添加相同列到同一模型表')
|
raise errors.ForbiddenError(msg='禁止添加相同列到同一模型表')
|
||||||
|
|
||||||
pd_type = sql_type_to_pydantic(obj.type)
|
pd_type = sql_type_to_pydantic(obj.type)
|
||||||
await gen_model_dao.create(db, obj, pd_type=pd_type)
|
await gen_model_dao.create(db, obj, pd_type=pd_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, pk: int, obj: UpdateGenModelParam) -> int:
|
async def update(*, pk: int, obj: UpdateGenModelParam) -> int:
|
||||||
|
"""
|
||||||
|
更新模型
|
||||||
|
|
||||||
|
:param pk: 模型 ID
|
||||||
|
:param obj: 更新模型参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
model = await gen_model_dao.get(db, pk)
|
model = await gen_model_dao.get(db, pk)
|
||||||
if obj.name != model.name:
|
if obj.name != model.name:
|
||||||
gen_models = await gen_model_dao.get_all_by_business_id(db, obj.gen_business_id)
|
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
|
||||||
if obj.name in [gen_model.name for gen_model in gen_models]:
|
if obj.name in [gen_model.name for gen_model in gen_models]:
|
||||||
raise errors.ForbiddenError(msg='模型列名已存在')
|
raise errors.ForbiddenError(msg='模型列名已存在')
|
||||||
|
|
||||||
pd_type = sql_type_to_pydantic(obj.type)
|
pd_type = sql_type_to_pydantic(obj.type)
|
||||||
count = await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
|
return await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(*, pk: int) -> int:
|
async def delete(*, pk: int) -> int:
|
||||||
|
"""
|
||||||
|
删除模型
|
||||||
|
|
||||||
|
:param pk: 模型 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
count = await gen_model_dao.delete(db, pk)
|
return await gen_model_dao.delete(db, pk)
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
gen_model_service: GenModelService = GenModelService()
|
gen_model_service: GenModelService = GenModelService()
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import os.path
|
|||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
|
|
||||||
@@ -20,27 +19,43 @@ from backend.app.generator.schema.gen_business import CreateGenBusinessParam
|
|||||||
from backend.app.generator.schema.gen_model import CreateGenModelParam
|
from backend.app.generator.schema.gen_model import CreateGenModelParam
|
||||||
from backend.app.generator.service.gen_model_service import gen_model_service
|
from backend.app.generator.service.gen_model_service import gen_model_service
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.core.path_conf import BasePath
|
from backend.core.path_conf import BASE_PATH
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
from backend.utils.gen_template import gen_template
|
from backend.utils.gen_template import gen_template
|
||||||
from backend.utils.type_conversion import sql_type_to_pydantic
|
from backend.utils.type_conversion import sql_type_to_pydantic
|
||||||
|
|
||||||
|
|
||||||
class GenService:
|
class GenService:
|
||||||
|
"""代码生成服务类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_tables(*, table_schema: str) -> Sequence[str]:
|
async def get_tables(*, table_schema: str) -> list[str]:
|
||||||
|
"""
|
||||||
|
获取指定 schema 下的所有表名
|
||||||
|
|
||||||
|
:param table_schema: 数据库 schema 名称
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
return await gen_dao.get_all_tables(db, table_schema)
|
return await gen_dao.get_all_tables(db, table_schema)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def import_business_and_model(*, obj: ImportParam) -> None:
|
async def import_business_and_model(*, obj: ImportParam) -> None:
|
||||||
|
"""
|
||||||
|
导入业务和模型数据
|
||||||
|
|
||||||
|
:param obj: 导入参数对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
table_info = await gen_dao.get_table(db, obj.table_name)
|
table_info = await gen_dao.get_table(db, obj.table_name)
|
||||||
if not table_info:
|
if not table_info:
|
||||||
raise errors.NotFoundError(msg='数据库表不存在')
|
raise errors.NotFoundError(msg='数据库表不存在')
|
||||||
|
|
||||||
business_info = await gen_business_dao.get_by_name(db, obj.table_name)
|
business_info = await gen_business_dao.get_by_name(db, obj.table_name)
|
||||||
if business_info:
|
if business_info:
|
||||||
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
|
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
|
||||||
|
|
||||||
table_name = table_info[0]
|
table_name = table_info[0]
|
||||||
business_data = {
|
business_data = {
|
||||||
'app_name': obj.app,
|
'app_name': obj.app,
|
||||||
@@ -52,6 +67,7 @@ class GenService:
|
|||||||
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump())
|
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump())
|
||||||
db.add(new_business)
|
db.add(new_business)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
column_info = await gen_dao.get_all_columns(db, obj.table_schema, table_name)
|
column_info = await gen_dao.get_all_columns(db, obj.table_schema, table_name)
|
||||||
for column in column_info:
|
for column in column_info:
|
||||||
column_type = column[-1].split('(')[0].upper()
|
column_type = column[-1].split('(')[0].upper()
|
||||||
@@ -70,20 +86,34 @@ class GenService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
|
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
渲染模板代码
|
||||||
|
|
||||||
|
:param business: 业务对象
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
gen_models = await gen_model_service.get_by_business(business_id=business.id)
|
gen_models = await gen_model_service.get_by_business(business_id=business.id)
|
||||||
if not gen_models:
|
if not gen_models:
|
||||||
raise errors.NotFoundError(msg='代码生成模型表为空')
|
raise errors.NotFoundError(msg='代码生成模型表为空')
|
||||||
|
|
||||||
gen_vars = gen_template.get_vars(business, gen_models)
|
gen_vars = gen_template.get_vars(business, gen_models)
|
||||||
tpl_code_map = {}
|
return {
|
||||||
for tpl_path in gen_template.get_template_paths():
|
tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars)
|
||||||
tpl_code_map[tpl_path] = await gen_template.get_template(tpl_path).render_async(**gen_vars)
|
for tpl_path in gen_template.get_template_paths()
|
||||||
return tpl_code_map
|
}
|
||||||
|
|
||||||
async def preview(self, *, pk: int) -> dict[str, bytes]:
|
async def preview(self, *, pk: int) -> dict[str, bytes]:
|
||||||
|
"""
|
||||||
|
预览生成的代码
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
business = await gen_business_dao.get(db, pk)
|
business = await gen_business_dao.get(db, pk)
|
||||||
if not business:
|
if not business:
|
||||||
raise errors.NotFoundError(msg='业务不存在')
|
raise errors.NotFoundError(msg='业务不存在')
|
||||||
|
|
||||||
tpl_code_map = await self.render_tpl_code(business=business)
|
tpl_code_map = await self.render_tpl_code(business=business)
|
||||||
return {
|
return {
|
||||||
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8')
|
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8')
|
||||||
@@ -92,42 +122,50 @@ class GenService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_generate_path(*, pk: int) -> list[str]:
|
async def get_generate_path(*, pk: int) -> list[str]:
|
||||||
|
"""
|
||||||
|
获取代码生成路径
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
business = await gen_business_dao.get(db, pk)
|
business = await gen_business_dao.get(db, pk)
|
||||||
if not business:
|
if not business:
|
||||||
raise errors.NotFoundError(msg='业务不存在')
|
raise errors.NotFoundError(msg='业务不存在')
|
||||||
gen_path = business.gen_path
|
|
||||||
if not gen_path:
|
gen_path = business.gen_path or 'fba-backend-app-path'
|
||||||
# 伪加密路径
|
|
||||||
gen_path = 'current-backend-app-path'
|
|
||||||
target_files = gen_template.get_code_gen_paths(business)
|
target_files = gen_template.get_code_gen_paths(business)
|
||||||
code_gen_paths = []
|
return [os.path.join(gen_path, *target_file.split('/')[1:]) for target_file in target_files]
|
||||||
for target_file in target_files:
|
|
||||||
code_gen_paths.append(os.path.join(gen_path, *target_file.split('/')[1:]))
|
|
||||||
return code_gen_paths
|
|
||||||
|
|
||||||
async def generate(self, *, pk: int) -> None:
|
async def generate(self, *, pk: int) -> None:
|
||||||
|
"""
|
||||||
|
生成代码文件
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
business = await gen_business_dao.get(db, pk)
|
business = await gen_business_dao.get(db, pk)
|
||||||
if not business:
|
if not business:
|
||||||
raise errors.NotFoundError(msg='业务不存在')
|
raise errors.NotFoundError(msg='业务不存在')
|
||||||
|
|
||||||
tpl_code_map = await self.render_tpl_code(business=business)
|
tpl_code_map = await self.render_tpl_code(business=business)
|
||||||
gen_path = business.gen_path
|
gen_path = business.gen_path or os.path.join(BASE_PATH, 'app')
|
||||||
if not gen_path:
|
|
||||||
gen_path = os.path.join(BasePath, 'app')
|
|
||||||
for tpl_path, code in tpl_code_map.items():
|
for tpl_path, code in tpl_code_map.items():
|
||||||
code_filepath = os.path.join(
|
code_filepath = os.path.join(
|
||||||
gen_path,
|
gen_path,
|
||||||
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:],
|
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:],
|
||||||
)
|
)
|
||||||
code_folder = Path(str(code_filepath)).parent
|
code_folder = Path(str(code_filepath)).parent
|
||||||
if not code_folder.exists():
|
code_folder.mkdir(parents=True, exist_ok=True)
|
||||||
code_folder.mkdir(parents=True, exist_ok=True)
|
|
||||||
# 写入 init 文件
|
# 写入 init 文件
|
||||||
init_filepath = code_folder.joinpath('__init__.py')
|
init_filepath = code_folder.joinpath('__init__.py')
|
||||||
if not init_filepath.exists():
|
if not init_filepath.exists():
|
||||||
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
||||||
await f.write(gen_template.init_content)
|
await f.write(gen_template.init_content)
|
||||||
|
|
||||||
if 'api' in str(code_folder):
|
if 'api' in str(code_folder):
|
||||||
# api __init__.py
|
# api __init__.py
|
||||||
api_init_filepath = code_folder.parent.joinpath('__init__.py')
|
api_init_filepath = code_folder.parent.joinpath('__init__.py')
|
||||||
@@ -136,12 +174,14 @@ class GenService:
|
|||||||
await f.write(gen_template.init_content)
|
await f.write(gen_template.init_content)
|
||||||
# app __init__.py
|
# app __init__.py
|
||||||
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py')
|
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py')
|
||||||
if not app_init_filepath:
|
if not app_init_filepath.exists():
|
||||||
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
|
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
|
||||||
await f.write(gen_template.init_content)
|
await f.write(gen_template.init_content)
|
||||||
# 写入代码文件呢
|
|
||||||
|
# 写入代码文件
|
||||||
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
|
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
|
||||||
await f.write(code)
|
await f.write(code)
|
||||||
|
|
||||||
# model init 文件补充
|
# model init 文件补充
|
||||||
if code_folder.name == 'model':
|
if code_folder.name == 'model':
|
||||||
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
|
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
|
||||||
@@ -151,33 +191,42 @@ class GenService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def download(self, *, pk: int) -> io.BytesIO:
|
async def download(self, *, pk: int) -> io.BytesIO:
|
||||||
|
"""
|
||||||
|
下载生成的代码
|
||||||
|
|
||||||
|
:param pk: 业务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
business = await gen_business_dao.get(db, pk)
|
business = await gen_business_dao.get(db, pk)
|
||||||
if not business:
|
if not business:
|
||||||
raise errors.NotFoundError(msg='业务不存在')
|
raise errors.NotFoundError(msg='业务不存在')
|
||||||
|
|
||||||
bio = io.BytesIO()
|
bio = io.BytesIO()
|
||||||
zf = zipfile.ZipFile(bio, 'w')
|
with zipfile.ZipFile(bio, 'w') as zf:
|
||||||
tpl_code_map = await self.render_tpl_code(business=business)
|
tpl_code_map = await self.render_tpl_code(business=business)
|
||||||
for tpl_path, code in tpl_code_map.items():
|
for tpl_path, code in tpl_code_map.items():
|
||||||
# 写入代码文件
|
# 写入代码文件
|
||||||
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
|
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
|
||||||
zf.writestr(new_code_path, code)
|
zf.writestr(new_code_path, code)
|
||||||
# 写入 init 文件
|
|
||||||
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py')
|
# 写入 init 文件
|
||||||
if 'model' not in new_code_path.split('/'):
|
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py')
|
||||||
zf.writestr(init_filepath, gen_template.init_content)
|
if 'model' not in new_code_path.split('/'):
|
||||||
else:
|
zf.writestr(init_filepath, gen_template.init_content)
|
||||||
zf.writestr(
|
else:
|
||||||
init_filepath,
|
zf.writestr(
|
||||||
f'{gen_template.init_content}'
|
init_filepath,
|
||||||
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
|
f'{gen_template.init_content}'
|
||||||
f'import {to_pascal(business.table_name_en)}\n',
|
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
|
||||||
)
|
f'import {to_pascal(business.table_name_en)}\n',
|
||||||
if 'api' in new_code_path:
|
)
|
||||||
# api __init__.py
|
|
||||||
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py')
|
if 'api' in new_code_path:
|
||||||
zf.writestr(api_init_filepath, gen_template.init_content)
|
# api __init__.py
|
||||||
zf.close()
|
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py')
|
||||||
|
zf.writestr(api_init_filepath, gen_template.init_content)
|
||||||
|
|
||||||
bio.seek(0)
|
bio.seek(0)
|
||||||
return bio
|
return bio
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ from fastapi import APIRouter
|
|||||||
from backend.app.task.api.v1.task import router as task_router
|
from backend.app.task.api.v1.task import router as task_router
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
|
|
||||||
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
|
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH, tags=['任务'])
|
||||||
|
|
||||||
v1.include_router(task_router, prefix='/tasks', tags=['任务'])
|
v1.include_router(task_router, prefix='/tasks')
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async def get_all_tasks() -> ResponseSchemaModel[list[str]]:
|
|||||||
description='此接口被视为作废,建议使用 flower 查看任务详情',
|
description='此接口被视为作废,建议使用 flower 查看任务详情',
|
||||||
dependencies=[DependsJwtAuth],
|
dependencies=[DependsJwtAuth],
|
||||||
)
|
)
|
||||||
async def get_task_detail(tid: Annotated[str, Path(description='任务ID')]) -> ResponseSchemaModel[TaskResult]:
|
async def get_task_detail(tid: Annotated[str, Path(description='任务 UUID')]) -> ResponseSchemaModel[TaskResult]:
|
||||||
status = task_service.get_detail(tid=tid)
|
status = task_service.get_detail(tid=tid)
|
||||||
return response_base.success(data=status)
|
return response_base.success(data=status)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ async def get_task_detail(tid: Annotated[str, Path(description='任务ID')]) ->
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def revoke_task(tid: Annotated[str, Path(description='任务ID')]) -> ResponseModel:
|
async def revoke_task(tid: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
|
||||||
task_service.revoke(tid=tid)
|
task_service.revoke(tid=tid)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|||||||
+40
-37
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import celery
|
import celery
|
||||||
import celery_aio_pool
|
import celery_aio_pool
|
||||||
|
|
||||||
@@ -9,8 +11,39 @@ from backend.core.conf import settings
|
|||||||
__all__ = ['celery_app']
|
__all__ = ['celery_app']
|
||||||
|
|
||||||
|
|
||||||
|
def get_broker_url() -> str:
|
||||||
|
"""获取消息代理 URL"""
|
||||||
|
if task_settings.CELERY_BROKER == 'redis':
|
||||||
|
return (
|
||||||
|
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
|
||||||
|
f'{settings.REDIS_PORT}/{task_settings.CELERY_BROKER_REDIS_DATABASE}'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f'amqp://{task_settings.RABBITMQ_USERNAME}:{task_settings.RABBITMQ_PASSWORD}@'
|
||||||
|
f'{task_settings.RABBITMQ_HOST}:{task_settings.RABBITMQ_PORT}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_result_backend() -> str:
|
||||||
|
"""获取结果后端 URL"""
|
||||||
|
return (
|
||||||
|
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
|
||||||
|
f'{settings.REDIS_PORT}/{task_settings.CELERY_BACKEND_REDIS_DATABASE}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_result_backend_transport_options() -> dict[str, Any]:
|
||||||
|
"""获取结果后端传输选项"""
|
||||||
|
return {
|
||||||
|
'global_keyprefix': task_settings.CELERY_BACKEND_REDIS_PREFIX,
|
||||||
|
'retry_policy': {
|
||||||
|
'timeout': task_settings.CELERY_BACKEND_REDIS_TIMEOUT,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def init_celery() -> celery.Celery:
|
def init_celery() -> celery.Celery:
|
||||||
"""初始化 celery 应用"""
|
"""初始化 Celery 应用"""
|
||||||
|
|
||||||
# TODO: Update this work if celery version >= 6.0.0
|
# TODO: Update this work if celery version >= 6.0.0
|
||||||
# https://github.com/fastapi-practices/fastapi_best_architecture/issues/321
|
# https://github.com/fastapi-practices/fastapi_best_architecture/issues/321
|
||||||
@@ -18,54 +51,24 @@ def init_celery() -> celery.Celery:
|
|||||||
celery.app.trace.build_tracer = celery_aio_pool.build_async_tracer
|
celery.app.trace.build_tracer = celery_aio_pool.build_async_tracer
|
||||||
celery.app.trace.reset_worker_optimizations()
|
celery.app.trace.reset_worker_optimizations()
|
||||||
|
|
||||||
# Celery Schedule Tasks
|
|
||||||
# https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html
|
|
||||||
beat_schedule = task_settings.CELERY_SCHEDULE
|
|
||||||
|
|
||||||
# Celery Config
|
|
||||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html
|
|
||||||
broker_url = (
|
|
||||||
(
|
|
||||||
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
|
|
||||||
f'{settings.REDIS_PORT}/{task_settings.CELERY_BROKER_REDIS_DATABASE}'
|
|
||||||
)
|
|
||||||
if task_settings.CELERY_BROKER == 'redis'
|
|
||||||
else (
|
|
||||||
f'amqp://{task_settings.RABBITMQ_USERNAME}:{task_settings.RABBITMQ_PASSWORD}@'
|
|
||||||
f'{task_settings.RABBITMQ_HOST}:{task_settings.RABBITMQ_PORT}'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result_backend = (
|
|
||||||
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
|
|
||||||
f'{settings.REDIS_PORT}/{task_settings.CELERY_BACKEND_REDIS_DATABASE}'
|
|
||||||
)
|
|
||||||
result_backend_transport_options = {
|
|
||||||
'global_keyprefix': f'{task_settings.CELERY_BACKEND_REDIS_PREFIX}',
|
|
||||||
'retry_policy': {
|
|
||||||
'timeout': task_settings.CELERY_BACKEND_REDIS_TIMEOUT,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
app = celery.Celery(
|
app = celery.Celery(
|
||||||
'fba_celery',
|
'fba_celery',
|
||||||
enable_utc=False,
|
enable_utc=False,
|
||||||
timezone=settings.DATETIME_TIMEZONE,
|
timezone=settings.DATETIME_TIMEZONE,
|
||||||
beat_schedule=beat_schedule,
|
beat_schedule=task_settings.CELERY_SCHEDULE,
|
||||||
broker_url=broker_url,
|
broker_url=get_broker_url(),
|
||||||
broker_connection_retry_on_startup=True,
|
broker_connection_retry_on_startup=True,
|
||||||
result_backend=result_backend,
|
result_backend=get_result_backend(),
|
||||||
result_backend_transport_options=result_backend_transport_options,
|
result_backend_transport_options=get_result_backend_transport_options(),
|
||||||
task_cls='app.task.celery_task.base:TaskBase',
|
task_cls='app.task.celery_task.base:TaskBase',
|
||||||
task_track_started=True,
|
task_track_started=True,
|
||||||
# TODO: Update this work if celery version >= 6.0.0
|
|
||||||
worker_pool=celery_aio_pool.pool.AsyncIOPool,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load task modules
|
# 自动发现任务
|
||||||
app.autodiscover_tasks(task_settings.CELERY_TASK_PACKAGES)
|
app.autodiscover_tasks(task_settings.CELERY_TASK_PACKAGES)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
# 创建 celery 实例
|
# 创建 Celery 实例
|
||||||
celery_app: celery.Celery = init_celery()
|
celery_app: celery.Celery = init_celery()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
@@ -9,16 +10,37 @@ from backend.common.socketio.actions import task_notification
|
|||||||
|
|
||||||
|
|
||||||
class TaskBase(Task):
|
class TaskBase(Task):
|
||||||
"""任务基类"""
|
"""Celery 任务基类"""
|
||||||
|
|
||||||
autoretry_for = (SQLAlchemyError,)
|
autoretry_for = (SQLAlchemyError,)
|
||||||
max_retries = task_settings.CELERY_TASK_MAX_RETRIES
|
max_retries = task_settings.CELERY_TASK_MAX_RETRIES
|
||||||
|
|
||||||
async def before_start(self, task_id, args, kwargs):
|
async def before_start(self, task_id: str, args, kwargs) -> None:
|
||||||
|
"""
|
||||||
|
任务开始前执行钩子
|
||||||
|
|
||||||
|
:param task_id: 任务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
await task_notification(msg=f'任务 {task_id} 开始执行')
|
await task_notification(msg=f'任务 {task_id} 开始执行')
|
||||||
|
|
||||||
async def on_success(self, retval, task_id, args, kwargs):
|
async def on_success(self, retval: Any, task_id: str, args, kwargs) -> None:
|
||||||
|
"""
|
||||||
|
任务成功后执行钩子
|
||||||
|
|
||||||
|
:param retval: 任务返回值
|
||||||
|
:param task_id: 任务 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
await task_notification(msg=f'任务 {task_id} 执行成功')
|
await task_notification(msg=f'任务 {task_id} 执行成功')
|
||||||
|
|
||||||
async def on_failure(self, exc, task_id, args, kwargs, einfo):
|
async def on_failure(self, exc: Exception, task_id: str, args, kwargs, einfo) -> None:
|
||||||
|
"""
|
||||||
|
任务失败后执行钩子
|
||||||
|
|
||||||
|
:param exc: 异常对象
|
||||||
|
:param task_id: 任务 ID
|
||||||
|
:param einfo: 异常信息
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
await task_notification(msg=f'任务 {task_id} 执行失败')
|
await task_notification(msg=f'任务 {task_id} 执行失败')
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ from backend.app.task.celery import celery_app
|
|||||||
|
|
||||||
@celery_app.task(name='task_demo_async')
|
@celery_app.task(name='task_demo_async')
|
||||||
async def task_demo_async() -> str:
|
async def task_demo_async() -> str:
|
||||||
|
"""异步示例任务,模拟耗时操作"""
|
||||||
await sleep(20)
|
await sleep(20)
|
||||||
return 'test async'
|
return 'test async'
|
||||||
|
|||||||
+15
-12
@@ -1,35 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
from backend.core.path_conf import BasePath
|
from backend.core.path_conf import BASE_PATH
|
||||||
|
|
||||||
|
|
||||||
class TaskSettings(BaseSettings):
|
class TaskSettings(BaseSettings):
|
||||||
"""Task Settings"""
|
"""Celery 任务配置"""
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
|
model_config = SettingsConfigDict(env_file=f'{BASE_PATH}/.env', env_file_encoding='utf-8', extra='ignore')
|
||||||
|
|
||||||
# Env Config
|
# .env 环境
|
||||||
ENVIRONMENT: Literal['dev', 'pro']
|
ENVIRONMENT: Literal['dev', 'pro']
|
||||||
|
|
||||||
# Env Celery
|
# .env Redis 配置
|
||||||
CELERY_BROKER_REDIS_DATABASE: int # 仅在 dev 模式时生效
|
CELERY_BROKER_REDIS_DATABASE: int
|
||||||
CELERY_BACKEND_REDIS_DATABASE: int
|
CELERY_BACKEND_REDIS_DATABASE: int
|
||||||
|
|
||||||
# Env Rabbitmq
|
# .env RabbitMQ 配置
|
||||||
# docker run -d --hostname fba-mq --name fba-mq -p 5672:5672 -p 15672:15672 rabbitmq:latest
|
# docker run -d --hostname fba-mq --name fba-mq -p 5672:5672 -p 15672:15672 rabbitmq:latest
|
||||||
RABBITMQ_HOST: str
|
RABBITMQ_HOST: str
|
||||||
RABBITMQ_PORT: int
|
RABBITMQ_PORT: int
|
||||||
RABBITMQ_USERNAME: str
|
RABBITMQ_USERNAME: str
|
||||||
RABBITMQ_PASSWORD: str
|
RABBITMQ_PASSWORD: str
|
||||||
|
|
||||||
# Celery
|
# Celery 基础配置
|
||||||
CELERY_BROKER: Literal['rabbitmq', 'redis'] = 'redis'
|
CELERY_BROKER: Literal['rabbitmq', 'redis'] = 'redis'
|
||||||
CELERY_BACKEND_REDIS_PREFIX: str = 'fba:celery:'
|
CELERY_BACKEND_REDIS_PREFIX: str = 'fba:celery:'
|
||||||
CELERY_BACKEND_REDIS_TIMEOUT: int = 5
|
CELERY_BACKEND_REDIS_TIMEOUT: int = 5
|
||||||
@@ -38,7 +38,9 @@ class TaskSettings(BaseSettings):
|
|||||||
'app.task.celery_task.db_log',
|
'app.task.celery_task.db_log',
|
||||||
]
|
]
|
||||||
CELERY_TASK_MAX_RETRIES: int = 5
|
CELERY_TASK_MAX_RETRIES: int = 5
|
||||||
CELERY_SCHEDULE: dict = {
|
|
||||||
|
# Celery 定时任务配置
|
||||||
|
CELERY_SCHEDULE: dict[str, dict[str, Any]] = {
|
||||||
'exec-every-10-seconds': {
|
'exec-every-10-seconds': {
|
||||||
'task': 'task_demo_async',
|
'task': 'task_demo_async',
|
||||||
'schedule': 10,
|
'schedule': 10,
|
||||||
@@ -55,7 +57,8 @@ class TaskSettings(BaseSettings):
|
|||||||
|
|
||||||
@model_validator(mode='before')
|
@model_validator(mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_celery_broker(cls, values):
|
def validate_celery_broker(cls, values: Any) -> Any:
|
||||||
|
"""生产环境强制使用 RabbitMQ 作为消息代理"""
|
||||||
if values['ENVIRONMENT'] == 'pro':
|
if values['ENVIRONMENT'] == 'pro':
|
||||||
values['CELERY_BROKER'] = 'rabbitmq'
|
values['CELERY_BROKER'] = 'rabbitmq'
|
||||||
return values
|
return values
|
||||||
@@ -63,7 +66,7 @@ class TaskSettings(BaseSettings):
|
|||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_task_settings() -> TaskSettings:
|
def get_task_settings() -> TaskSettings:
|
||||||
"""获取 task 配置"""
|
"""获取 Celery 任务配置"""
|
||||||
return TaskSettings()
|
return TaskSettings()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from backend.common.schema import SchemaBase
|
from backend.common.schema import SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class RunParam(SchemaBase):
|
class RunParam(SchemaBase):
|
||||||
|
"""任务运行参数"""
|
||||||
|
|
||||||
name: str = Field(description='任务名称')
|
name: str = Field(description='任务名称')
|
||||||
args: list | None = Field(default=None, description='任务函数位置参数')
|
args: list[Any] | None = Field(None, description='任务函数位置参数')
|
||||||
kwargs: dict | None = Field(default=None, description='任务函数关键字参数')
|
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
|
||||||
|
|
||||||
|
|
||||||
class TaskResult(SchemaBase):
|
class TaskResult(SchemaBase):
|
||||||
result: str
|
"""任务执行结果"""
|
||||||
traceback: str
|
|
||||||
status: str
|
result: str = Field(description='任务执行结果')
|
||||||
name: str
|
traceback: str = Field(description='错误堆栈信息')
|
||||||
args: list | None
|
status: str = Field(description='任务状态')
|
||||||
kwargs: dict | None
|
name: str = Field(description='任务名称')
|
||||||
worker: str
|
args: list[Any] | None = Field(None, description='任务函数位置参数')
|
||||||
retries: int | None
|
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
|
||||||
queue: str | None
|
worker: str = Field(description='执行任务的 worker')
|
||||||
|
retries: int | None = Field(None, description='重试次数')
|
||||||
|
queue: str | None = Field(None, description='任务队列')
|
||||||
|
|||||||
@@ -13,14 +13,21 @@ from backend.common.exception.errors import NotFoundError
|
|||||||
class TaskService:
|
class TaskService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_list() -> list[str]:
|
async def get_list() -> list[str]:
|
||||||
|
"""获取所有已注册的 Celery 任务列表"""
|
||||||
registered_tasks = await run_in_threadpool(celery_app.control.inspect().registered)
|
registered_tasks = await run_in_threadpool(celery_app.control.inspect().registered)
|
||||||
if not registered_tasks:
|
if not registered_tasks:
|
||||||
raise errors.ForbiddenError(msg='celery 服务未启动')
|
raise errors.ForbiddenError(msg='Celery 服务未启动')
|
||||||
tasks = list(registered_tasks.values())[0]
|
tasks = list(registered_tasks.values())[0]
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_detail(*, tid: str) -> TaskResult:
|
def get_detail(*, tid: str) -> TaskResult:
|
||||||
|
"""
|
||||||
|
获取指定任务的详细信息
|
||||||
|
|
||||||
|
:param tid: 任务 UUID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
result = AsyncResult(id=tid, app=celery_app)
|
result = AsyncResult(id=tid, app=celery_app)
|
||||||
except NotRegistered:
|
except NotRegistered:
|
||||||
@@ -38,7 +45,13 @@ class TaskService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def revoke(*, tid: str):
|
def revoke(*, tid: str) -> None:
|
||||||
|
"""
|
||||||
|
撤销指定的任务
|
||||||
|
|
||||||
|
:param tid: 任务 UUID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
result = AsyncResult(id=tid, app=celery_app)
|
result = AsyncResult(id=tid, app=celery_app)
|
||||||
except NotRegistered:
|
except NotRegistered:
|
||||||
@@ -47,6 +60,12 @@ class TaskService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run(*, obj: RunParam) -> str:
|
def run(*, obj: RunParam) -> str:
|
||||||
|
"""
|
||||||
|
运行指定的任务
|
||||||
|
|
||||||
|
:param obj: 任务运行参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
task: AsyncResult = celery_app.send_task(name=obj.name, args=obj.args, kwargs=obj.kwargs)
|
task: AsyncResult = celery_app.send_task(name=obj.name, args=obj.args, kwargs=obj.kwargs)
|
||||||
return task.task_id
|
return task.task_id
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
FROM python:3.10-slim
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-c"]
|
|
||||||
|
|
||||||
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 supervisor \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
# 某些包可能存在同步不及时导致安装失败的情况,可更改为官方源:https://pypi.org/simple
|
|
||||||
&& pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \
|
|
||||||
&& pip install -r backend/requirements.txt -i https://mirrors.aliyun.com/pypi/simple \
|
|
||||||
&& pip install gunicorn aio_pika wait-for-it -i https://mirrors.aliyun.com/pypi/simple
|
|
||||||
|
|
||||||
ENV TZ="Asia/Shanghai"
|
|
||||||
|
|
||||||
RUN mkdir -p /var/log/fastapi_server
|
|
||||||
|
|
||||||
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
|
|
||||||
|
|
||||||
COPY deploy/backend/fastapi_server.conf /etc/supervisor/conf.d/
|
|
||||||
|
|
||||||
EXPOSE 8001
|
|
||||||
|
|
||||||
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# work && beat
|
# work && beat
|
||||||
celery -A app.task.celery worker -l info -B
|
celery -A app.task.celery worker -l info -P gevent -c 100 &
|
||||||
|
|
||||||
|
# beat
|
||||||
|
celery -A app.task.celery beat -l info &
|
||||||
|
|
||||||
# flower
|
# flower
|
||||||
celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456
|
celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user