Compare commits

...
31 Commits
Author SHA1 Message Date
Wu Clan e492cec7d7 Optimize codes and comments with cursor (#550) 2025-03-28 10:08:32 +08:00
PoetryL 92fe1e7554 Fix the query for the sub department (#557) 2025-03-27 23:30:12 +08:00
Wu Clan 7b080600f6 Update volumes of redis in docker compose (#556) 2025-03-26 18:58:06 +08:00
Wu Clan 71f9667bc0 Fix the celery env in docker compose (#555) 2025-03-26 18:34:41 +08:00
Thank CAT 2bf548f8b9 Fix filters for opera log query list (#554) 2025-03-25 09:19:28 +08:00
Wu Clan 15fd4545aa Optimize schemas with model relationships (#552) 2025-03-20 18:46:13 +08:00
Wu Clan 7f6c4ad6da Fix return schema of the config api (#551) 2025-03-20 16:27:44 +08:00
Wu Clan 2be12b4ea4 Fix async install plugin dependencies for windows (#549) 2025-03-17 23:39:10 +08:00
Wu Clan baebdd9870 Optimize dependencies to reduce package size (#548) 2025-03-17 18:41:58 +08:00
Wu Clan d1648bb709 Update git and docker ignore files (#547)
* Update git and docker ignore files

* add __pycache__ dir

* add .python-version
2025-03-17 13:20:21 +08:00
Wu Clan a4a394a18d Optimize the dynamic import of data models (#546) 2025-03-16 15:13:09 +08:00
Wu Clan 951fdc902a Optimize the return of relationship interfaces (#545)
* Optimize the return of relationship interfaces

* Fix gen business detail schema
2025-03-13 18:30:05 +08:00
Wu Clan 18ffa06d70 Updated the backend contribution guide (#544) 2025-03-12 18:15:10 +08:00
Wu Clan d28f152d78 Update the changelog for v1.0.3 (#543) 2025-03-12 14:23:38 +08:00
Wu Clan 1e3e9ea84a Update docker scripts and nginx conf (#542) 2025-03-11 23:26:53 +08:00
Huyuwei 5b42e59d65 Add Aliyun mirror to PyPI index in pyproject.toml (#541)
* Add Aliyun mirror to PyPI index in pyproject.toml

* Update uv.lock

* fix end of files

* Remove UV_INDEX environment variable from Dockerfile
2025-03-11 16:48:30 +08:00
Huyuwei 1882467554 Fix Dockerfile mounts for dependency installation (#540) 2025-03-10 15:34:11 +08:00
Huyuwei f1cd48e9d0 Fix typos in Dockerfile comments (#539) 2025-03-10 15:33:42 +08:00
Wu Clan d1fd84617b Refactor toml and dependencies file dir (#538)
* Refactor toml and dependencies file dir

* fix ci
2025-03-09 23:38:20 +08:00
Wu Clan 03fcf82b26 Update docker scripts in backend README (#537) 2025-03-04 18:34:07 +08:00
Wu Clan f75e1bf3df Update the changelog for v1.0.2 (#536) 2025-03-01 16:01:41 +08:00
Wu Clan 554a6f59bd Add plugin related interfaces (#535)
* Add plugin related interfaces

* Update interface auth

* Add build plugin zip service
2025-03-01 15:58:41 +08:00
Wu Clan 68d41cf028 Add log module root and output levels (#534) 2025-02-27 15:38:41 +08:00
Wu Clan 5defeca590 Fix celery async task worker pool (#533)
* Fix celery async task worker pool

* Update celery start scripts

* fix lint
2025-02-27 15:38:10 +08:00
Wu Clan 4d94418124 Update the changelog for v1.0.1 (#532) 2025-02-26 15:53:25 +08:00
Huyuwei 6c258f056a Optimize Dockerfile for faster builds (#526) 2025-02-26 14:45:48 +08:00
Wu Clan 87af1f1319 Fix the sql script for init data (#530) 2025-02-25 21:42:03 +08:00
Wu Clan 3f9badd4b9 Add sqlalchemy connection pool config (#529) 2025-02-25 15:46:10 +08:00
Wu Clan 06de770c4d Add async attrs for sqla mapped base (#528) 2025-02-25 15:43:35 +08:00
Huyuwei b5948441ac Add missing volume config for docker deploy (#525) 2025-02-25 15:43:13 +08:00
Wu Clan acbab69f12 Update the changelog for v1.0.0 (#524) 2025-02-24 21:39:49 +08:00
172 changed files with 6576 additions and 4756 deletions
+9
View File
@@ -0,0 +1,9 @@
__pycache__/
.git/
.github/
.idea/
.vscode/
venv/
.venv/
.ruff_cache/
.pytest_cache/
+2 -4
View File
@@ -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
+7
View File
@@ -1,2 +1,9 @@
.idea/ .idea/
.vscode/ .vscode/
.cursor/
.DS_Store
venv/
.venv/
.python-version
.ruff_cache/
.pytest_cache/
-7
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+33 -33
View File
@@ -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
View File
@@ -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/)
## 赞助我们 ## 赞助我们
-4
View File
@@ -1,4 +0,0 @@
__pycache__/
venv/
.venv/
.python-version
-6
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)
+6 -6
View File
@@ -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()
+6 -6
View File
@@ -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()
+4 -1
View File
@@ -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)
+4 -4
View File
@@ -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}')
+3 -1
View File
@@ -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=['系统插件'])
+15 -15
View File
@@ -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()
+19 -14
View File
@@ -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()
+9 -11
View File
@@ -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()
+19 -12
View File
@@ -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()
+9 -7
View File
@@ -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()
+8 -9
View File
@@ -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()
+100
View File
@@ -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'},
)
+22 -19
View File
@@ -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()
+28 -24
View File
@@ -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()
+30 -27
View File
@@ -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
View File
@@ -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')
# OAuth2https://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()
+35 -37
View File
@@ -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(
+36 -30
View File
@@ -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)
+42 -37
View File
@@ -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)
+38 -33
View File
@@ -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)
+24 -22
View File
@@ -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)
+13 -11
View File
@@ -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)
+41 -38
View File
@@ -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)
+15 -13
View File
@@ -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)
+56 -52
View File
@@ -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)
+102 -90
View File
@@ -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()
+14 -12
View File
@@ -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)
+1 -1
View File
@@ -9,7 +9,7 @@ from backend.common.model import Base, id_key
class Config(Base): class Config(Base):
"""系统配置表""" """参数配置表"""
__tablename__ = 'sys_config' __tablename__ = 'sys_config'
+8 -1
View File
@@ -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')
+9 -4
View File
@@ -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')
+9 -2
View File
@@ -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')
+9 -2
View File
@@ -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')
+9 -4
View File
@@ -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')
+2 -1
View File
@@ -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
+10 -3
View File
@@ -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')
+10 -5
View File
@@ -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')
+7 -2
View File
@@ -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')
+2
View File
@@ -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='图片内容')
+21 -15
View File
@@ -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='更新时间')
+17 -12
View File
@@ -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)
+17 -13
View File
@@ -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='更新时间')
+21 -12
View File
@@ -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='字典类型信息')
+13 -9
View File
@@ -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='更新时间')
+22 -18
View File
@@ -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='创建时间')
+22 -18
View File
@@ -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='更新时间')
+28 -23
View File
@@ -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='创建时间')
+25 -12
View File
@@ -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='数据规则详情列表')
+33 -21
View File
@@ -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='过期时间')
+63 -45
View File
@@ -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
+14 -8
View File
@@ -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 """更新用户社交参数"""
+38
View File
@@ -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
+54 -3
View File
@@ -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
+50 -5
View File
@@ -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}')
+39 -1
View File
@@ -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)
+36 -1
View File
@@ -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
+30 -1
View File
@@ -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
+32 -3
View File
@@ -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
+47
View File
@@ -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:
+16 -2
View File
@@ -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} 账户未绑定邮箱')
+25 -2
View File
@@ -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
+59 -1
View File
@@ -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}')
+161 -70
View File
@@ -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)
+7 -11
View File
@@ -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()
+5 -5
View File
@@ -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,
+13 -11
View File
@@ -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()
+4 -6
View File
@@ -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()
+3 -3
View File
@@ -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()
+25 -1
View File
@@ -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,
+24 -21
View File
@@ -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)
+23 -19
View File
@@ -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)
+6 -1
View File
@@ -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')
+5 -2
View File
@@ -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')
+2
View File
@@ -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='数据库名')
+20 -17
View File
@@ -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
+19 -14
View File
@@ -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()
+93 -44
View File
@@ -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
+2 -2
View File
@@ -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')
+2 -2
View File
@@ -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
View File
@@ -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()
+26 -4
View File
@@ -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} 执行失败')
+1
View File
@@ -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
View File
@@ -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()
+17 -11
View File
@@ -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='任务队列')
+21 -2
View File
@@ -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
-30
View File
@@ -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"]
+4 -1
View File
@@ -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