Compare commits

...
196 Commits
Author SHA1 Message Date
Wu Clan eeb709c6aa Update the version number to 1.8.0 (#771) 2025-08-15 20:06:21 +08:00
Wu Clan a6bbf2971d Update the menu title in SQL scripts (#770) 2025-08-15 19:58:46 +08:00
Dylan cd48bb4210 Add i18n support for response message (#753)
* feat: i18n support

* Optimize i18n

* Update the locale in the code

* Update the zh-CN file

* Update the en-US file

* Update the reload filter

* Update locale success plugin value

* Fix lint

* Update pydantic error message translation

* Update to minimal implementation

* Fix minimal missing code
2025-08-15 19:57:10 +08:00
Wu Clan 4500dd0128 Add a standalone email sending plugin (#769) 2025-08-13 18:09:42 +08:00
Wu Clan 2b6d8222ad Update the content layout of the config file (#768) 2025-08-13 18:07:02 +08:00
Wu Clan 1b47ab7e83 Optimize the timezone datetime return encoder (#767)
* Optimize the timezone datetime return encoder

* Update the default datetime conversion
2025-08-13 11:16:58 +08:00
Wu Clan bd804e0a38 Update the description for the run file (#766) 2025-08-12 23:28:43 +08:00
Wu Clan 8e8af2032a Optimize naming and preview in code generation (#764) 2025-08-12 16:42:25 +08:00
IAseven e09062eb39 Optimize the opera log storage logic through queue (#750)
*  feat: 操作日志中间件添加批量插入功能

* Delete GEMINI.md

* 🌈 style: 修复格式化错误

* 🐞 fix: 通过asyncio.wait_for兼容py3.10中asyncio.timeout不存在

* 🦄 refactor: 重新组织操作日志批量插入代码逻辑

* 优化代码实现

* 恢复默认配置

* 恢复默认 .gitignore 文件

* 更新队列批处理逻辑
2025-08-07 17:36:10 +08:00
Wu Clan 8c00492e44 Update the naming of table creation function (#760) 2025-08-07 16:57:57 +08:00
Wu Clan 0237d4c7b1 Update log output config and format (#759) 2025-08-07 16:46:56 +08:00
Wu Clan fe3a3b4e86 Optimize the data sort logic of tree nodes (#758) 2025-08-06 18:23:09 +08:00
Wu Clan 65ec721a1c Add business pagination in the code generator (#757) 2025-08-05 21:04:27 +08:00
Wu Clan 4eb76ad6ea Update the opera log desensitization method (#756)
* Update the opera log desensitization method

* Update request args function return
2025-08-05 18:38:49 +08:00
Wu Clan 1f8687155a Fix message format in validation exception handler (#755) 2025-08-05 18:11:02 +08:00
Wu Clan 8591d4e592 Refactor task routes and add control routes (#749) 2025-08-04 13:18:07 +08:00
Wu Clan dedf4e7bae Refactor code generation files and routes (#748)
* Refactor code generation files and routes

* Fix lint
2025-08-04 13:16:41 +08:00
Wu Clan 1e4aa88487 Fix the kwargs params of schedule task (#747) 2025-08-04 13:15:48 +08:00
Wu Clan 0dd745b7a7 Add schedule task demo that contains params (#746) 2025-07-31 18:29:54 +08:00
Wu Clan 6b2402f212 Add some interfaces for user profiles (#745) 2025-07-31 16:46:02 +08:00
Wu Clan 24a487eeea Simplify the plugin status update logic (#744) 2025-07-30 11:31:37 +08:00
Wu Clan 4f574189c7 Fix the error trigger when model auto import (#743) 2025-07-29 22:52:34 +08:00
Wu Clan b559a74cea Add update support for user email and phone (#742) 2025-07-29 22:52:08 +08:00
Wu Clan 83dcdbe59d Update the OAuth2 login password policy (#741)
* Update the OAuth2 login password policy

* Update the crud pwd

* Update the reset pwd service
2025-07-29 22:51:31 +08:00
Wu Clan d64f7c2911 Fix the opera log field encryption (#739) 2025-07-25 19:23:36 +08:00
Wu Clan 53e64bce37 Add auth whitelist regular expression config (#738) 2025-07-24 21:34:24 +08:00
Wu Clan 00a781357b Fix celery CLI option to required (#737) 2025-07-24 21:32:57 +08:00
Wu Clan d7f87ed0ad Update the default cache period for userinfo (#734) 2025-07-21 21:31:55 +08:00
Wu Clan bda9b1d463 Add distributed lock for scheduled task (#732)
* Add distributed lock for scheduled task

* Add the task to extend lock

* Fix the close
2025-07-21 12:29:12 +08:00
Wu Clan e0a106ec51 Simplify task crontab expression validation (#733) 2025-07-18 21:11:54 +08:00
Wu Clan 016361bd68 Update the changelog for v1.7.0 (#729) 2025-07-16 13:40:55 +08:00
Wu Clan f2d3c39425 Fix login and operation log clearing (#728) 2025-07-16 13:34:37 +08:00
Wu Clan e45d2d6fe7 Add CLI support for startup celery services (#724)
* Add CLI support for startup celery services

* update the cli file
2025-07-16 12:28:57 +08:00
Wu Clan 4ddf84fa22 Bump granian from 2.4.0 to 2.4.2 (#727) 2025-07-16 12:28:02 +08:00
Wu Clan 326a1883e8 Fix the parsing of execution task params (#725)
* Fix the parsing of execution task params

* Enhanced checksums
2025-07-16 12:10:33 +08:00
Wu Clan 9ff36d4498 Delete the default value of schema enum data (#723) 2025-07-15 10:43:58 +08:00
Wu Clan 98ef07ad32 Simplify celery task crontab config (#722) 2025-07-15 00:28:10 +08:00
Wu Clan 802b0d456a Optimize celery integrations and events (#721) 2025-07-14 19:17:24 +08:00
Wu Clan 6767f0e2e6 Update the celery task comment and name (#720) 2025-07-11 21:08:56 +08:00
Wu Clan d72a05c965 Fix the celery task scheduler query (#719) 2025-07-11 21:08:44 +08:00
Wu Clan ce3be1db8e Add support for celery dynamic tasks (#715)
* Add support for celery dynamic tasks

* Update the celery conf

* Update the celery task tables name

* Refactor the celery task-related interfaces

* Optimize auto-discovery tasks

* Remove redundant config

* Refine the business codes

* Optimize crontab validation returns

* Update dependencies in pyproject toml

* Fix some bugs

* Update dependencies

* Update the version to 1.7.0

* Fix update and delete event
2025-07-11 07:54:33 +08:00
Wu Clan e84ef04f15 Update the CLI startup service mode (#718) 2025-07-10 20:33:50 +08:00
Wu Clan adee3a2177 Simplify user permission database queries (#717) 2025-07-09 20:44:32 +08:00
Wu Clan 526e0aab9a Optimize the analysis of get plugins (#716)
* Optimize the analysis of get plugins

* update var
2025-07-08 12:20:13 +08:00
Wu Clan 2bbbbe764a Update the log output default style (#714)
* Update the log output default style

* Update the log file compression

* fix line

* Add log summary
2025-07-06 14:32:36 +08:00
Wu Clan ef5e921c08 Update the middleware logging accuracy (#713)
* Update the middleware logging accuracy

* Update the log style
2025-07-05 19:52:37 +08:00
Wu Clan a2fa59285a Update the granian env to command params (#712) 2025-07-04 19:46:40 +08:00
Wu Clan 494942e87e Add CLI support for execute sql scripts (#711)
* Add CLI support for execute sql scripts

* Update the arg helps
2025-07-02 20:19:29 +08:00
Wu Clan aa2b76673f Update the reload excludes for CLI run (#709) 2025-07-02 18:16:43 +08:00
Wu Clan 099880dd1c Update the refresh token verify mechanism (#710)
* Remove the logout interface auth dependency

* Update the refresh token check
2025-07-02 18:08:28 +08:00
Wu Clan d906a103af Fix the code generation delete schema template (#708) 2025-07-01 22:28:37 +08:00
Wu Clan 4ed49d2d13 Replace gunicorn deployment to granian (#705)
* Replace gunicorn deployment to granian

* Update env comments

* Fix the granian env
2025-07-01 21:33:49 +08:00
Wu Clan 7b5ae4696f Fix the code generation schema template (#706) 2025-07-01 18:18:47 +08:00
Wu Clan 54ea301152 Update the CLI to be executed async (#704) 2025-07-01 18:18:29 +08:00
Wu Clan 69c27232ac Update the changelog for v1.6.0 (#703) 2025-06-30 17:02:55 +08:00
Wu Clan f36dcb3f5c Update the version number to 1.6.0 (#702) 2025-06-30 17:00:19 +08:00
Wu Clan 63d088d62c Update the Dockerfile to adapt the CLI (#701) 2025-06-30 16:53:16 +08:00
Wu Clan a461f78224 Optimize the installation of plugin dependencies (#700)
* Optimize the installation of plugin dependencies

* Remove enumerate

* Update main params
2025-06-30 09:29:35 +08:00
Wu Clan c306432708 Update the help for CLI run worker (#699) 2025-06-29 20:06:45 +08:00
Wu Clan 97f778cc90 Add CLI support for plugin install (#698)
* Add CLI support for plugin install

* Fix some usage errors

* Update prompt information
2025-06-29 17:41:12 +08:00
Wu Clan 88695ac6ad Add custom CLI for service startup (#697)
* Add custom CLI for service startup

* Remove redundant scripts

* Fix lint
2025-06-28 14:56:19 +08:00
Wu Clan 3b04329b04 Add the test user to SQL scripts (#696) 2025-06-27 18:43:42 +08:00
Wu Clan bd4acf8121 Update the extension plugin config (#695) 2025-06-27 18:14:25 +08:00
Wu Clan b9a9b1efe6 Update the SQL to adapt frontend plugin (#694) 2025-06-26 21:58:35 +08:00
Wu Clan c050f58ee9 Fix the OAuth2 redirect route names (#693) 2025-06-26 21:50:22 +08:00
Wu Clan d47375ae41 Optimize dict create and update logic (#691)
* Optimize dict create and update logic

* Update version
2025-06-25 10:41:09 +08:00
Wu Clan 0602c6144d Update the changelog for v1.5.2 (#690) 2025-06-24 17:40:09 +08:00
Wu Clan c84f0658fb Update the dict pagination query parameters (#689)
* Update the dict pagination query parameters

* Update the dict pagination query parameters

* Update version
2025-06-24 17:34:18 +08:00
Wu Clan a2902bd23a Add built-in plugin missing files (#688) 2025-06-24 17:34:06 +08:00
Wu Clan 69a9b90711 Optimize the zip plug-in file name parsing (#687) 2025-06-24 17:33:57 +08:00
Wu Clan b96402e11d Simplify custom response status codes (#686) 2025-06-24 10:42:15 +08:00
Wu Clan 0bc6f6a719 Update the init test data for SQL scripts (#685)
* Update the init test data for SQL scripts

* Update plugin sql scripts

* fix snowflake id
2025-06-24 10:15:23 +08:00
Wu Clan ebd65c8246 Update dict data label column config (#684) 2025-06-23 22:33:34 +08:00
Wu Clan 234bf708b3 Fix the code with outdated system config (#683) 2025-06-23 22:23:06 +08:00
Dylan 6d5e741d94 Optimize api with semantic HTTP status codes (#681) 2025-06-23 22:18:17 +08:00
Wu Clan f9bfe8f510 Add dictionary type and datas queries (#679) 2025-06-23 10:58:55 +08:00
Wu Clan 408c866dda Update cache cleanup for logout interface (#678)
* Update cache cleanup for logout interface

* Add dependencies of interface
2025-06-22 23:41:48 +08:00
Wu Clan bac41a46f8 Optimize token detection and caching logic (#677) 2025-06-21 20:18:13 +08:00
Wu Clan 8638c26db1 Add the snowflake ID sql script (#675) 2025-06-20 20:31:44 +08:00
Wu Clan 319ba13df1 Optimize routes to better align with RESTful (#673)
* Optimize routes to better align with RESTful

* Add codes endpoint description

* Update jinja templates

* fix typo

* fix sql
2025-06-19 11:06:34 +08:00
Wu Clan 0d1f05d307 Fix some error class import (#672) 2025-06-17 15:30:35 +08:00
Wu Clan e6608d18ce Update the changelog for v1.5.1 (#671) 2025-06-16 21:38:13 +08:00
Dylan 7afd8415cd Add support for snowflake ID primary key (#670)
* fix: 修复PostgreSQL SQL语法错误,将反引号替换为双引号

* feat: 新增雪花算法ID实现

* 优化雪花算法和主键类型

* 修复错误引用

* 添加雪花详情链接

* feat: add snowflake ID parser method

* 修复独立执行异常

* 更新系统时间错误类
2025-06-16 13:34:27 +08:00
Dylan 845f2f0ff8 Fix the postgresql sql script syntax error (#669) 2025-06-15 16:38:48 +08:00
Wu Clan 11d7792c0f Bump sqlalchemy crud plus version to 1.10.0 (#668)
* Bump sqlalchemy crud plus version to 1.10.0

* Update filter style

* Update filters style
2025-06-14 12:35:19 +08:00
Wu Clan 6883ec34c6 Fix the sidebar menu type filtering (#667) 2025-06-13 16:50:53 +08:00
Wu Clan 4c45e7ff27 Update the changelog for v1.5.0 (#664) 2025-06-09 21:36:54 +08:00
Wu Clan 5621daea98 Update the version to 1.5.0 (#663) 2025-06-09 21:32:12 +08:00
Wu Clan 71d168dfea Add data permission in init test sql (#662) 2025-06-09 21:00:49 +08:00
Wu Clan 2d4365189d Update menu sort in init test sql (#661) 2025-06-09 20:27:56 +08:00
Wu Clan 0c28a46e60 Update auth failed default status code (#660)
* Update auth failed default status code

* update init sql
2025-06-09 19:58:06 +08:00
Wu Clan a4c7dae99c Update the plugin download interface permission (#659) 2025-06-09 19:10:48 +08:00
Wu Clan bfda0a226c Update code generate interface permission (#658) 2025-06-09 19:10:40 +08:00
Wu Clan c9bbb105a3 Update data scope and rule to m2m (#657) 2025-06-09 19:10:15 +08:00
Wu Clan b415dc8912 Add OAuth2 user to auto bind a role (#656) 2025-06-08 20:18:24 +08:00
Wu Clan 05b74d71a3 Simplify OAuth2 model and optimize auth service (#655) 2025-06-08 16:51:19 +08:00
Wu Clan 10c0c691a7 Update user email and phone operation logic (#654) 2025-06-08 16:43:51 +08:00
Wu Clan 0bc3786fc6 Update OAuth2 callback interface return (#653) 2025-06-06 20:35:25 +08:00
Wu Clan 8e041ccb1b Update the changelog for v1.4.3 (#651) 2025-06-02 20:43:12 +08:00
Wu Clan a82e9b7b9d Fix data permission condition for filter data (#650) 2025-06-01 00:51:31 +08:00
Wu Clan 7bab8cc636 Update default value for role filter scopes (#649) 2025-05-31 23:53:47 +08:00
Wu Clan 7659b63d88 Add data permission condition for filter data (#648) 2025-05-31 23:40:59 +08:00
Wu Clan 33ebc5723f Add get all data scope rules interface (#647) 2025-05-31 23:09:26 +08:00
Wu Clan 86d9efd785 Update the server startup time to string (#646) 2025-05-31 17:26:07 +08:00
Wu Clan 57ae7f03a6 Refactor routes to better align with RESTful (#645) 2025-05-31 17:01:44 +08:00
Wu Clan 6ee3983846 Fix the filter of query all menus (#642) 2025-05-30 22:48:30 +08:00
Wu Clan f05ed5cdda Fix the menu delete interface arg description (#641) 2025-05-30 18:58:33 +08:00
Wu Clan 187dfa5344 Fix the role update business variables (#640) 2025-05-30 18:56:50 +08:00
Wu Clan 918c8a9d33 Update the changelog for v1.4.2 (#639) 2025-05-29 18:12:45 +08:00
Wu Clan ccfe35ea85 Simplify the user info update business (#638) 2025-05-29 18:09:50 +08:00
yzbf 4a118c166a Fix overwriting existing init files in code generator (#637) 2025-05-28 20:17:18 +08:00
Huyuwei 5f3823cb6d Fix auto-increment id for postgres init data (#632)
* fix: Reset auto-increment values for all tables based on max id

* refactor: Remove auto-increment reset for mysql

* remove unused code
2025-05-28 13:01:44 +08:00
Wu Clan 19ef52da9b Optimize install and build of plugin zip (#636)
* Optimiz install and build of plugin zip

* fix import
2025-05-27 11:46:17 +08:00
Wu Clan 39c3339a8d Optimize role-related data processing performance (#635) 2025-05-26 20:04:13 +08:00
Wu Clan 9f52629dcb Update the captcha invalidation error class (#634) 2025-05-26 19:16:56 +08:00
Wu Clan 1cddd6e2d5 Update non-linked sidebar support (#633)
* Update non-linked sidebar support

* fix typing
2025-05-26 15:52:45 +08:00
Wu Clan ed10e6ff47 Update the changelog for v1.4.1 (#630) 2025-05-25 14:19:41 +08:00
Wu Clan 48574fb50a Update the token decode for logout interface (#629) 2025-05-25 14:16:36 +08:00
Wu Clan ee7fe3087d Update the token check for logout interface (#625) 2025-05-23 22:05:26 +08:00
Wu Clan 270ac3d941 Refactor the system token to online users (#624) 2025-05-23 20:18:47 +08:00
Wu Clan 2f76988076 Add the deepwiki badge to README (#623) 2025-05-23 12:09:04 +08:00
Wu Clan b275481742 Update the menu path and type columns (#622)
* Update the menu path and type columns

* Update build vben5 tree

* Update data
2025-05-22 21:22:24 +08:00
Wu Clan f3f1ceb595 Update the changelog for v1.4.0 (#621) 2025-05-22 14:41:03 +08:00
Wu Clan bcc764142b Update the OAuth2 module to plugin (#620)
* Update the OAuth2 module to plugin

* update create user social param
2025-05-22 14:36:23 +08:00
Wu Clan 9b0d9f8296 Update uv installation in docker deploy (#619) 2025-05-22 14:30:53 +08:00
Wu Clan 63ba7646c1 Update the build plugin api params (#615)
* Update the build plugin api params

* update seek

* fix lint
2025-05-20 19:45:22 +08:00
Wu Clan 1a7734185f Fix non-asyncio nested async IO (#610)
* Fix non-asyncio nested async IO

* Add redis init ping
2025-05-20 18:11:11 +08:00
Wu Clan 8541059d23 Update uninstall and build plugin api method (#614) 2025-05-20 18:10:29 +08:00
Wu Clan 61edc81333 Fix the plugin status update logic (#613) 2025-05-20 17:50:07 +08:00
Wu Clan 8998687adb Fix the task result schema param type (#611) 2025-05-20 15:10:15 +08:00
Wu Clan 85b0294da9 Update the new plugin status to changed (#607)
* Fix cache update after plugin uninstall

* Update new to changed
2025-05-17 09:21:11 +08:00
Wu Clan a8ecb4ae3b Add new plugin status check interface (#606) 2025-05-17 09:06:06 +08:00
Wu Clan a87ff74b56 Update the changelog for v1.3.0 (#605) 2025-05-16 14:16:59 +08:00
Wu Clan 3c5682988d Update the casbin RBAC module path (#604) 2025-05-16 11:49:18 +08:00
Wu Clan 1d32154498 Optimize the zip plugin install logic (#603) 2025-05-15 23:40:33 +08:00
Wu Clan f4290ddb8c Fix the fastapi cli startup event loop (#602) 2025-05-15 22:54:25 +08:00
Wu Clan 1e54b0ba42 Add plugin info config and interfaces (#601)
* Add plugin info config and interfaces

* Remove test data
2025-05-15 20:50:24 +08:00
Wu Clan 27b9cf4c64 Simplify apps and plugins config method (#600) 2025-05-13 13:16:06 +08:00
Wu Clan 87f29e52a1 Update the changelog for v1.2.0 (#598) 2025-05-05 19:46:41 +08:00
Wu Clan 16a0f9646d Update the SQL script for creat tables (#597) 2025-04-28 19:39:33 +08:00
Wu Clan 032364e48e Refactor the data rule to scope rule (#596)
* WIP

* update codes

* update codes

* update filter_data_permission

* Fix schema

* Fix issues
2025-04-28 18:15:52 +08:00
Wu Clan bc5d142920 Optimize the server information retrieval (#595)
* Optimize the server information retrieval

* Update import

* Fix typing
2025-04-28 15:41:53 +08:00
Wu Clan 9b3425d801 Update the default RBAC solution (#593) 2025-04-27 14:54:39 +08:00
Wu Clan 5842127c2b Update code generator table columns (#590)
* Update code generator table columns

* Update func doc
2025-04-24 20:27:08 +08:00
Wu Clan 193eba3c58 Update the changelog for v1.1.2 (#589) 2025-04-23 23:43:56 +08:00
Wu Clan 338e50e255 Update the routing style of the task app (#588)
* Update the routing style of the task app

* Fix prefix
2025-04-23 19:33:36 +08:00
Wu Clan 65500d7a53 Bump fastapi pagination from 0.12.34 to 0.13.0 (#587) 2025-04-23 13:04:09 +08:00
Wu Clan 0833cd5443 Optimize the userinfo cache cleaning logic (#586) 2025-04-23 12:40:27 +08:00
Wu Clan e8c844ead7 Fix cache cleanup when updating role menu (#585) 2025-04-21 17:58:01 +08:00
Wu Clan 1d5f2dc5f0 Fix the condition to query menu by title (#584) 2025-04-21 17:57:12 +08:00
Wu Clanandwu-clan 9a36154564 Update the changelog for v1.1.1 (#583)
This commit was created by changelog-from-release in 'Release changelog' CI workflow

Co-authored-by: wu-clan <wu-clan@users.noreply.github.com>
2025-04-18 23:19:01 +08:00
Wu Clan 5f8ae74ea5 Fix list query in the dict plugin (#582) 2025-04-18 23:16:03 +08:00
Wu Clan da8e7c1b01 Fix the plugin system route injection (#581) 2025-04-18 23:09:25 +08:00
Wu Clan f513b98f15 Update the changelog for v1.1.0 (#580) 2025-04-17 17:54:52 +08:00
Wu Clan 80cef6dde7 Update code generator file and table naming (#579)
* Update code generator file and table naming

* Update column table comment
2025-04-17 17:47:20 +08:00
Huyuwei 2d504f281e Fix avatar url type of update avatar (#575) 2025-04-17 15:02:19 +08:00
Wu Clan fc34c98f0a Update the code generator to plugin (#578)
* Update the code generator to plugin

* Fix get all tables return type
2025-04-17 11:06:43 +08:00
Wu Clan 1cc020efdd Update unique columns in dict models (#577) 2025-04-17 11:06:29 +08:00
Wu Clan bb33e99e33 Update casbin RBAC verify to dynamic import (#576) 2025-04-17 11:06:15 +08:00
Wu Clan 6c70b34ae6 Optimize the file structure of code generator (#574) 2025-04-15 19:54:18 +08:00
Wu Clan 960d9f6215 Update the default value for some functions (#573)
* Update the default value for some functions

* Fix lint
2025-04-14 14:22:05 +08:00
Wu Clan 085f08ec3b Update the changelog for v1.0.5 (#572) 2025-04-09 11:55:49 +08:00
Wu Clan 2a8722f56e Fix the log rule in gitignore (#571) 2025-04-09 11:53:55 +08:00
Wu Clan 4372b9bbc2 Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 (#570) 2025-04-09 11:49:04 +08:00
Wu Clan db81319dc4 Fix the failure hook of celery task (#569) 2025-04-08 12:09:29 +08:00
Wu Clan 644f7a0413 Fix the login password verification (#568)
* Fix the login password verification

* Update the check criteria
2025-04-07 11:04:39 +08:00
Wu Clan b93ff19ebe Update the number of pagination le (#565) 2025-04-03 19:32:26 +08:00
Wu Clan 4c007cb47d Update custom validation error messages (#566) 2025-04-03 19:32:11 +08:00
Wu Clan c9e9a45288 Update the vben5 tree data structure (#564) 2025-04-02 23:18:55 +08:00
Wu Clan d44a5491f5 Update menu and add vben5 compatibility (#563)
* Update menu and add vben5 compatibility

* Update sidebar api description
2025-04-02 22:20:19 +08:00
Wu Clan 289cf5b974 Update dict data and type to plugin (#562) 2025-04-02 14:04:06 +08:00
Wu Clan e207622a70 Update the system config to plugin (#561) 2025-04-02 13:43:24 +08:00
Wu Clan 38d9d9fda5 Add python 3.13 to GitHub ci (#560) 2025-03-28 14:22:45 +08:00
Wu Clan 996895e775 Bump dependencies and pre-commits (#559) 2025-03-28 14:22:20 +08:00
Wu Clan 0675e53f2a Update the changelog for v1.0.4 (#558) 2025-03-28 10:15:11 +08:00
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
339 changed files with 14606 additions and 10469 deletions
+9 -6
View File
@@ -1,6 +1,9 @@
**/__pycache__
**/venv
**/.venv
**/.python-version
**/.git
**/.github
__pycache__/
.git/
.github/
.idea/
.vscode/
venv/
.venv/
.ruff_cache/
.pytest_cache/
+4 -6
View File
@@ -12,25 +12,23 @@ jobs:
name: lint ${{ matrix.python-version }}
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12' ]
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v5
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: |
cd backend
uv sync --only-group lint
- name: Run lint
run: |
cd backend
source .venv/bin/activate
chmod 755 scripts/lint.sh
./scripts/lint.sh
chmod 755 backend/scripts/lint.sh
./backend/scripts/lint.sh
+7
View File
@@ -1,2 +1,9 @@
.idea/
.vscode/
.cursor/
.DS_Store
venv/
.venv/
.python-version
.ruff_cache/
.pytest_cache/
+4 -11
View File
@@ -2,13 +2,13 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: end-of-file-fixer
# - id: check-added-large-files
# - id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.5
rev: v0.11.9
hooks:
- id: ruff
args:
@@ -20,18 +20,11 @@ repos:
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.29
rev: 0.7.3
hooks:
- id: uv-lock
args:
- '--directory'
- 'backend'
files: backend/uv\.lock$
- id: uv-export
args:
- '--directory'
- 'backend'
- '-o'
- 'requirements.txt'
- '--no-hashes'
files: backend/uv\.lock$
+391 -1
View File
@@ -1,3 +1,375 @@
<a id="v1.7.0"></a>
# [v1.7.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.7.0) - 2025-07-16
## What's Changed
* Update the changelog for v1.6.0 by [@wu-clan](https://github.com/wu-clan) in [#703](https://github.com/fastapi-practices/fastapi_best_architecture/pull/703)
* Update the CLI to be executed async by [@wu-clan](https://github.com/wu-clan) in [#704](https://github.com/fastapi-practices/fastapi_best_architecture/pull/704)
* Fix the code generation schema template by [@wu-clan](https://github.com/wu-clan) in [#706](https://github.com/fastapi-practices/fastapi_best_architecture/pull/706)
* Replace gunicorn deployment to granian by [@wu-clan](https://github.com/wu-clan) in [#705](https://github.com/fastapi-practices/fastapi_best_architecture/pull/705)
* Fix the code generation delete schema template by [@wu-clan](https://github.com/wu-clan) in [#708](https://github.com/fastapi-practices/fastapi_best_architecture/pull/708)
* Update the refresh token verify mechanism by [@wu-clan](https://github.com/wu-clan) in [#710](https://github.com/fastapi-practices/fastapi_best_architecture/pull/710)
* Update the reload excludes for CLI run by [@wu-clan](https://github.com/wu-clan) in [#709](https://github.com/fastapi-practices/fastapi_best_architecture/pull/709)
* Add CLI support for execute sql scripts by [@wu-clan](https://github.com/wu-clan) in [#711](https://github.com/fastapi-practices/fastapi_best_architecture/pull/711)
* Update the granian env to command params by [@wu-clan](https://github.com/wu-clan) in [#712](https://github.com/fastapi-practices/fastapi_best_architecture/pull/712)
* Update the middleware logging accuracy by [@wu-clan](https://github.com/wu-clan) in [#713](https://github.com/fastapi-practices/fastapi_best_architecture/pull/713)
* Update the log output default style by [@wu-clan](https://github.com/wu-clan) in [#714](https://github.com/fastapi-practices/fastapi_best_architecture/pull/714)
* Optimize the analysis of get plugins by [@wu-clan](https://github.com/wu-clan) in [#716](https://github.com/fastapi-practices/fastapi_best_architecture/pull/716)
* Simplify user permission database queries by [@wu-clan](https://github.com/wu-clan) in [#717](https://github.com/fastapi-practices/fastapi_best_architecture/pull/717)
* Update the CLI startup service mode by [@wu-clan](https://github.com/wu-clan) in [#718](https://github.com/fastapi-practices/fastapi_best_architecture/pull/718)
* Add support for celery dynamic tasks by [@wu-clan](https://github.com/wu-clan) in [#715](https://github.com/fastapi-practices/fastapi_best_architecture/pull/715)
* Fix the celery task scheduler query by [@wu-clan](https://github.com/wu-clan) in [#719](https://github.com/fastapi-practices/fastapi_best_architecture/pull/719)
* Update the celery task comment and name by [@wu-clan](https://github.com/wu-clan) in [#720](https://github.com/fastapi-practices/fastapi_best_architecture/pull/720)
* Optimize celery integrations and events by [@wu-clan](https://github.com/wu-clan) in [#721](https://github.com/fastapi-practices/fastapi_best_architecture/pull/721)
* Simplify celery task crontab config by [@wu-clan](https://github.com/wu-clan) in [#722](https://github.com/fastapi-practices/fastapi_best_architecture/pull/722)
* Delete the default value of schema enum data by [@wu-clan](https://github.com/wu-clan) in [#723](https://github.com/fastapi-practices/fastapi_best_architecture/pull/723)
* Fix the parsing of execution task params by [@wu-clan](https://github.com/wu-clan) in [#725](https://github.com/fastapi-practices/fastapi_best_architecture/pull/725)
* Bump granian from 2.4.0 to 2.4.2 by [@wu-clan](https://github.com/wu-clan) in [#727](https://github.com/fastapi-practices/fastapi_best_architecture/pull/727)
* Add CLI support for startup celery services by [@wu-clan](https://github.com/wu-clan) in [#724](https://github.com/fastapi-practices/fastapi_best_architecture/pull/724)
* Fix login and operation log clearing by [@wu-clan](https://github.com/wu-clan) in [#728](https://github.com/fastapi-practices/fastapi_best_architecture/pull/728)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
[Changes][v1.7.0]
<a id="v1.6.0"></a>
# [v1.6.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.6.0) - 2025-06-30
## What's Changed
* Update changelog for v1.5.2 by [@wu-clan](https://github.com/wu-clan) in [#690](https://github.com/fastapi-practices/fastapi_best_architecture/pull/690)
* Optimize dict create and update logic by [@wu-clan](https://github.com/wu-clan) in [#691](https://github.com/fastapi-practices/fastapi_best_architecture/pull/691)
* Fix the OAuth2 redirect route names by [@wu-clan](https://github.com/wu-clan) in [#693](https://github.com/fastapi-practices/fastapi_best_architecture/pull/693)
* Update the SQL to adapt frontend plugin by [@wu-clan](https://github.com/wu-clan) in [#694](https://github.com/fastapi-practices/fastapi_best_architecture/pull/694)
* Update the extension plugin config by [@wu-clan](https://github.com/wu-clan) in [#695](https://github.com/fastapi-practices/fastapi_best_architecture/pull/695)
* Add the test user to SQL scripts by [@wu-clan](https://github.com/wu-clan) in [#696](https://github.com/fastapi-practices/fastapi_best_architecture/pull/696)
* Add custom CLI for service startup by [@wu-clan](https://github.com/wu-clan) in [#697](https://github.com/fastapi-practices/fastapi_best_architecture/pull/697)
* Add CLI support for plugin install by [@wu-clan](https://github.com/wu-clan) in [#698](https://github.com/fastapi-practices/fastapi_best_architecture/pull/698)
* Update the help for CLI run worker by [@wu-clan](https://github.com/wu-clan) in [#699](https://github.com/fastapi-practices/fastapi_best_architecture/pull/699)
* Optimize the installation of plugin dependencies by [@wu-clan](https://github.com/wu-clan) in [#700](https://github.com/fastapi-practices/fastapi_best_architecture/pull/700)
* Update the Dockerfile to adapt latest code by [@wu-clan](https://github.com/wu-clan) in [#701](https://github.com/fastapi-practices/fastapi_best_architecture/pull/701)
* Update the version number to 1.6.0 by [@wu-clan](https://github.com/wu-clan) in [#702](https://github.com/fastapi-practices/fastapi_best_architecture/pull/702)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
[Changes][v1.6.0]
<a id="v1.5.2"></a>
# [v1.5.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.2) - 2025-06-24
## What's Changed
* Update changelog for v1.5.1 by [@wu-clan](https://github.com/wu-clan) in [#671](https://github.com/fastapi-practices/fastapi_best_architecture/pull/671)
* Fix some error class import by [@wu-clan](https://github.com/wu-clan) in [#672](https://github.com/fastapi-practices/fastapi_best_architecture/pull/672)
* Optimize routes to better align with RESTful by [@wu-clan](https://github.com/wu-clan) in [#673](https://github.com/fastapi-practices/fastapi_best_architecture/pull/673)
* Add the snowflake ID sql script by [@wu-clan](https://github.com/wu-clan) in [#675](https://github.com/fastapi-practices/fastapi_best_architecture/pull/675)
* Optimize token detection and caching logic by [@wu-clan](https://github.com/wu-clan) in [#677](https://github.com/fastapi-practices/fastapi_best_architecture/pull/677)
* Update cache cleanup for logout interface by [@wu-clan](https://github.com/wu-clan) in [#678](https://github.com/fastapi-practices/fastapi_best_architecture/pull/678)
* Add dictionary type and datas queries by [@wu-clan](https://github.com/wu-clan) in [#679](https://github.com/fastapi-practices/fastapi_best_architecture/pull/679)
* Optimize api with semantic HTTP status codes by [@downdawn](https://github.com/downdawn) in [#681](https://github.com/fastapi-practices/fastapi_best_architecture/pull/681)
* Fix the code with outdated system config by [@wu-clan](https://github.com/wu-clan) in [#683](https://github.com/fastapi-practices/fastapi_best_architecture/pull/683)
* Update dict data label column config by [@wu-clan](https://github.com/wu-clan) in [#684](https://github.com/fastapi-practices/fastapi_best_architecture/pull/684)
* Update the init test data for SQL scripts by [@wu-clan](https://github.com/wu-clan) in [#685](https://github.com/fastapi-practices/fastapi_best_architecture/pull/685)
* Simplify custom response status codes by [@wu-clan](https://github.com/wu-clan) in [#686](https://github.com/fastapi-practices/fastapi_best_architecture/pull/686)
* Optimize the zip plug-in file name parsing by [@wu-clan](https://github.com/wu-clan) in [#687](https://github.com/fastapi-practices/fastapi_best_architecture/pull/687)
* Add built-in plugin missing files by [@wu-clan](https://github.com/wu-clan) in [#688](https://github.com/fastapi-practices/fastapi_best_architecture/pull/688)
* Update the dict pagination query parameters by [@wu-clan](https://github.com/wu-clan) in [#689](https://github.com/fastapi-practices/fastapi_best_architecture/pull/689)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
[Changes][v1.5.2]
<a id="v1.5.1"></a>
# [v1.5.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.1) - 2025-06-16
## What's Changed
* Update changelog for v1.5.0 by [@wu-clan](https://github.com/wu-clan) in [#664](https://github.com/fastapi-practices/fastapi_best_architecture/pull/664)
* Fix the sidebar menu type filtering by [@wu-clan](https://github.com/wu-clan) in [#667](https://github.com/fastapi-practices/fastapi_best_architecture/pull/667)
* Bump sqlalchemy crud plus version to 1.10.0 by [@wu-clan](https://github.com/wu-clan) in [#668](https://github.com/fastapi-practices/fastapi_best_architecture/pull/668)
* Fix the postgresql sql script syntax error by [@downdawn](https://github.com/downdawn) in [#669](https://github.com/fastapi-practices/fastapi_best_architecture/pull/669)
* Add Initial Snowflake ID Support by [@downdawn](https://github.com/downdawn) in [#670](https://github.com/fastapi-practices/fastapi_best_architecture/pull/670)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
[Changes][v1.5.1]
<a id="v1.5.0"></a>
# [v1.5.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.0) - 2025-06-09
## What's Changed
* Update changelog for v1.4.3 by [@wu-clan](https://github.com/wu-clan) in [#651](https://github.com/fastapi-practices/fastapi_best_architecture/pull/651)
* Update OAuth2 callback interface return by [@wu-clan](https://github.com/wu-clan) in [#653](https://github.com/fastapi-practices/fastapi_best_architecture/pull/653)
* Update user email and phone operation logic by [@wu-clan](https://github.com/wu-clan) in [#654](https://github.com/fastapi-practices/fastapi_best_architecture/pull/654)
* Simplify OAuth2 model and optimize auth service by [@wu-clan](https://github.com/wu-clan) in [#655](https://github.com/fastapi-practices/fastapi_best_architecture/pull/655)
* Add OAuth2 user to auto bind a role by [@wu-clan](https://github.com/wu-clan) in [#656](https://github.com/fastapi-practices/fastapi_best_architecture/pull/656)
* Update data scope and rule to m2m by [@wu-clan](https://github.com/wu-clan) in [#657](https://github.com/fastapi-practices/fastapi_best_architecture/pull/657)
* Update code generate interface permission by [@wu-clan](https://github.com/wu-clan) in [#658](https://github.com/fastapi-practices/fastapi_best_architecture/pull/658)
* Update the plugin download interface permission by [@wu-clan](https://github.com/wu-clan) in [#659](https://github.com/fastapi-practices/fastapi_best_architecture/pull/659)
* Update auth failed default status code by [@wu-clan](https://github.com/wu-clan) in [#660](https://github.com/fastapi-practices/fastapi_best_architecture/pull/660)
* Update menu sort in init test sql by [@wu-clan](https://github.com/wu-clan) in [#661](https://github.com/fastapi-practices/fastapi_best_architecture/pull/661)
* Add data permission in init test sql by [@wu-clan](https://github.com/wu-clan) in [#662](https://github.com/fastapi-practices/fastapi_best_architecture/pull/662)
* Update the version to 1.5.0 by [@wu-clan](https://github.com/wu-clan) in [#663](https://github.com/fastapi-practices/fastapi_best_architecture/pull/663)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.0
[Changes][v1.5.0]
<a id="v1.4.3"></a>
# [v1.4.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.3) - 2025-06-02
## What's Changed
* Update changelog for v1.4.2 by [@wu-clan](https://github.com/wu-clan) in [#639](https://github.com/fastapi-practices/fastapi_best_architecture/pull/639)
* Fix the role update business variables by [@wu-clan](https://github.com/wu-clan) in [#640](https://github.com/fastapi-practices/fastapi_best_architecture/pull/640)
* Fix the menu delete interface arg description by [@wu-clan](https://github.com/wu-clan) in [#641](https://github.com/fastapi-practices/fastapi_best_architecture/pull/641)
* Fix the filter of query all menus by [@wu-clan](https://github.com/wu-clan) in [#642](https://github.com/fastapi-practices/fastapi_best_architecture/pull/642)
* Refactor routes to better align with RESTful by [@wu-clan](https://github.com/wu-clan) in [#645](https://github.com/fastapi-practices/fastapi_best_architecture/pull/645)
* Update the server startup time to string by [@wu-clan](https://github.com/wu-clan) in [#646](https://github.com/fastapi-practices/fastapi_best_architecture/pull/646)
* Add get all data scope rules interface by [@wu-clan](https://github.com/wu-clan) in [#647](https://github.com/fastapi-practices/fastapi_best_architecture/pull/647)
* Add data permission condition for filter data by [@wu-clan](https://github.com/wu-clan) in [#648](https://github.com/fastapi-practices/fastapi_best_architecture/pull/648)
* Update default value for role filter scopes by [@wu-clan](https://github.com/wu-clan) in [#649](https://github.com/fastapi-practices/fastapi_best_architecture/pull/649)
* Fix data permission condition for filter data by [@wu-clan](https://github.com/wu-clan) in [#650](https://github.com/fastapi-practices/fastapi_best_architecture/pull/650)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
[Changes][v1.4.3]
<a id="v1.4.2"></a>
# [v1.4.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.2) - 2025-05-29
## What's Changed
* Update changelog for v1.4.1 by [@wu-clan](https://github.com/wu-clan) in [#630](https://github.com/fastapi-practices/fastapi_best_architecture/pull/630)
* Update non-linked sidebar support by [@wu-clan](https://github.com/wu-clan) in [#633](https://github.com/fastapi-practices/fastapi_best_architecture/pull/633)
* Update the captcha invalidation error class by [@wu-clan](https://github.com/wu-clan) in [#634](https://github.com/fastapi-practices/fastapi_best_architecture/pull/634)
* Optimize role-related data processing performance by [@wu-clan](https://github.com/wu-clan) in [#635](https://github.com/fastapi-practices/fastapi_best_architecture/pull/635)
* Optimize install and build of plugin zip by [@wu-clan](https://github.com/wu-clan) in [#636](https://github.com/fastapi-practices/fastapi_best_architecture/pull/636)
* Fix auto-increment id for postgres init data by [@huyuwei1996](https://github.com/huyuwei1996) in [#632](https://github.com/fastapi-practices/fastapi_best_architecture/pull/632)
* Fix: prevent overwriting existing init files in code generator by [@lin-wu-1990](https://github.com/lin-wu-1990) in [#637](https://github.com/fastapi-practices/fastapi_best_architecture/pull/637)
* Simplify the user info update business by [@wu-clan](https://github.com/wu-clan) in [#638](https://github.com/fastapi-practices/fastapi_best_architecture/pull/638)
## New Contributors
* [@lin-wu-1990](https://github.com/lin-wu-1990) made their first contribution in [#637](https://github.com/fastapi-practices/fastapi_best_architecture/pull/637)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
[Changes][v1.4.2]
<a id="v1.4.1"></a>
# [v1.4.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.1) - 2025-05-25
## What's Changed
* Update changelog for v1.4.0 by [@wu-clan](https://github.com/wu-clan) in [#621](https://github.com/fastapi-practices/fastapi_best_architecture/pull/621)
* Update the menu path and type columns by [@wu-clan](https://github.com/wu-clan) in [#622](https://github.com/fastapi-practices/fastapi_best_architecture/pull/622)
* Add the deepwiki badge to README by [@wu-clan](https://github.com/wu-clan) in [#623](https://github.com/fastapi-practices/fastapi_best_architecture/pull/623)
* Refactor the system token to online users by [@wu-clan](https://github.com/wu-clan) in [#624](https://github.com/fastapi-practices/fastapi_best_architecture/pull/624)
* Update the token check for logout interface by [@wu-clan](https://github.com/wu-clan) in [#625](https://github.com/fastapi-practices/fastapi_best_architecture/pull/625)
* Update the token decode for logout interface by [@wu-clan](https://github.com/wu-clan) in [#629](https://github.com/fastapi-practices/fastapi_best_architecture/pull/629)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
[Changes][v1.4.1]
<a id="v1.4.0"></a>
# [v1.4.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.0) - 2025-05-22
## What's Changed
* Update changelog for v1.3.0 by [@wu-clan](https://github.com/wu-clan) in [#605](https://github.com/fastapi-practices/fastapi_best_architecture/pull/605)
* Add new plugin status check interface by [@wu-clan](https://github.com/wu-clan) in [#606](https://github.com/fastapi-practices/fastapi_best_architecture/pull/606)
* Update the new plugin status to changed by [@wu-clan](https://github.com/wu-clan) in [#607](https://github.com/fastapi-practices/fastapi_best_architecture/pull/607)
* Fix the task result schema param type by [@wu-clan](https://github.com/wu-clan) in [#611](https://github.com/fastapi-practices/fastapi_best_architecture/pull/611)
* Fix the plugin status update logic by [@wu-clan](https://github.com/wu-clan) in [#613](https://github.com/fastapi-practices/fastapi_best_architecture/pull/613)
* Update uninstall and build plugin api method by [@wu-clan](https://github.com/wu-clan) in [#614](https://github.com/fastapi-practices/fastapi_best_architecture/pull/614)
* Fix non-asyncio nested async IO by [@wu-clan](https://github.com/wu-clan) in [#610](https://github.com/fastapi-practices/fastapi_best_architecture/pull/610)
* Update the build plugin api params by [@wu-clan](https://github.com/wu-clan) in [#615](https://github.com/fastapi-practices/fastapi_best_architecture/pull/615)
* Update uv installation in docker deploy by [@wu-clan](https://github.com/wu-clan) in [#619](https://github.com/fastapi-practices/fastapi_best_architecture/pull/619)
* Update the OAuth2 module to plugin by [@wu-clan](https://github.com/wu-clan) in [#620](https://github.com/fastapi-practices/fastapi_best_architecture/pull/620)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
[Changes][v1.4.0]
<a id="v1.3.0"></a>
# [v1.3.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.3.0) - 2025-05-16
## What's Changed
* Update changelog for v1.2.0 by [@wu-clan](https://github.com/wu-clan) in [#598](https://github.com/fastapi-practices/fastapi_best_architecture/pull/598)
* Simplify apps and plugins config method by [@wu-clan](https://github.com/wu-clan) in [#600](https://github.com/fastapi-practices/fastapi_best_architecture/pull/600)
* Add plugin info config and interfaces by [@wu-clan](https://github.com/wu-clan) in [#601](https://github.com/fastapi-practices/fastapi_best_architecture/pull/601)
* Fix the fastapi cli startup event loop by [@wu-clan](https://github.com/wu-clan) in [#602](https://github.com/fastapi-practices/fastapi_best_architecture/pull/602)
* Optimize the zip plugin install logic by [@wu-clan](https://github.com/wu-clan) in [#603](https://github.com/fastapi-practices/fastapi_best_architecture/pull/603)
* Update the casbin RBAC module path by [@wu-clan](https://github.com/wu-clan) in [#604](https://github.com/fastapi-practices/fastapi_best_architecture/pull/604)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
[Changes][v1.3.0]
<a id="v1.2.0"></a>
# [v1.2.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.2.0) - 2025-05-01
## What's Changed
* Update changelog for v1.1.2 by [@wu-clan](https://github.com/wu-clan) in [#589](https://github.com/fastapi-practices/fastapi_best_architecture/pull/589)
* Update code generator table columns by [@wu-clan](https://github.com/wu-clan) in [#590](https://github.com/fastapi-practices/fastapi_best_architecture/pull/590)
* Update the default RBAC solution by [@wu-clan](https://github.com/wu-clan) in [#593](https://github.com/fastapi-practices/fastapi_best_architecture/pull/593)
* Optimize the server information retrieval by [@wu-clan](https://github.com/wu-clan) in [#595](https://github.com/fastapi-practices/fastapi_best_architecture/pull/595)
* Refactor the data rule to scope rule by [@wu-clan](https://github.com/wu-clan) in [#596](https://github.com/fastapi-practices/fastapi_best_architecture/pull/596)
* Update the SQL script for creat tables by [@wu-clan](https://github.com/wu-clan) in [#597](https://github.com/fastapi-practices/fastapi_best_architecture/pull/597)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
[Changes][v1.2.0]
<a id="v1.1.2"></a>
# [v1.1.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.2) - 2025-04-23
## What's Changed
* Update the changelog for v1.1.1 by [@wu-clan](https://github.com/wu-clan) in [#583](https://github.com/fastapi-practices/fastapi_best_architecture/pull/583)
* Fix the condition to query menu by title by [@wu-clan](https://github.com/wu-clan) in [#584](https://github.com/fastapi-practices/fastapi_best_architecture/pull/584)
* Fix cache cleanup when updating role menu by [@wu-clan](https://github.com/wu-clan) in [#585](https://github.com/fastapi-practices/fastapi_best_architecture/pull/585)
* Optimize the userinfo cache cleaning logic by [@wu-clan](https://github.com/wu-clan) in [#586](https://github.com/fastapi-practices/fastapi_best_architecture/pull/586)
* Bump fastapi pagination from 0.12.34 to 0.13.0 by [@wu-clan](https://github.com/wu-clan) in [#587](https://github.com/fastapi-practices/fastapi_best_architecture/pull/587)
* Update the routing style of the task app by [@wu-clan](https://github.com/wu-clan) in [#588](https://github.com/fastapi-practices/fastapi_best_architecture/pull/588)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
[Changes][v1.1.2]
<a id="v1.1.1"></a>
# [v1.1.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.1) - 2025-04-18
## What's Changed
* Update changelog for v1.1.0 by [@wu-clan](https://github.com/wu-clan) in [#580](https://github.com/fastapi-practices/fastapi_best_architecture/pull/580)
* Fix the plugin system route injection by [@wu-clan](https://github.com/wu-clan) in [#581](https://github.com/fastapi-practices/fastapi_best_architecture/pull/581)
* Fix list query in the dict plugin by [@wu-clan](https://github.com/wu-clan) in [#582](https://github.com/fastapi-practices/fastapi_best_architecture/pull/582)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
[Changes][v1.1.1]
<a id="v1.1.0"></a>
# [v1.1.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.0) - 2025-04-17
## What's Changed
* Update the changelog for v1.0.5 by [@wu-clan](https://github.com/wu-clan) in [#572](https://github.com/fastapi-practices/fastapi_best_architecture/pull/572)
* Update the default value for some functions by [@wu-clan](https://github.com/wu-clan) in [#573](https://github.com/fastapi-practices/fastapi_best_architecture/pull/573)
* Optimize the file structure of code generator by [@wu-clan](https://github.com/wu-clan) in [#574](https://github.com/fastapi-practices/fastapi_best_architecture/pull/574)
* Update casbin RBAC verify to dynamic import by [@wu-clan](https://github.com/wu-clan) in [#576](https://github.com/fastapi-practices/fastapi_best_architecture/pull/576)
* Update unique columns in dict models by [@wu-clan](https://github.com/wu-clan) in [#577](https://github.com/fastapi-practices/fastapi_best_architecture/pull/577)
* Update the code generator to plugin by [@wu-clan](https://github.com/wu-clan) in [#578](https://github.com/fastapi-practices/fastapi_best_architecture/pull/578)
* Fix avatar url type of update avatar by [@huyuwei1996](https://github.com/huyuwei1996) in [#575](https://github.com/fastapi-practices/fastapi_best_architecture/pull/575)
* Update code generator file and table naming by [@wu-clan](https://github.com/wu-clan) in [#579](https://github.com/fastapi-practices/fastapi_best_architecture/pull/579)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[Changes][v1.1.0]
<a id="v1.0.5"></a>
# [v1.0.5](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.5) - 2025-04-09
## What's Changed
* Update the changelog for v1.0.4 by [@wu-clan](https://github.com/wu-clan) in [#558](https://github.com/fastapi-practices/fastapi_best_architecture/pull/558)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#559](https://github.com/fastapi-practices/fastapi_best_architecture/pull/559)
* Add python 3.13 to GitHub ci by [@wu-clan](https://github.com/wu-clan) in [#560](https://github.com/fastapi-practices/fastapi_best_architecture/pull/560)
* Update the system config to plugin by [@wu-clan](https://github.com/wu-clan) in [#561](https://github.com/fastapi-practices/fastapi_best_architecture/pull/561)
* Update dict data and type to plugin by [@wu-clan](https://github.com/wu-clan) in [#562](https://github.com/fastapi-practices/fastapi_best_architecture/pull/562)
* Update menu and add vben5 compatibility by [@wu-clan](https://github.com/wu-clan) in [#563](https://github.com/fastapi-practices/fastapi_best_architecture/pull/563)
* Update the vben5 tree data structure by [@wu-clan](https://github.com/wu-clan) in [#564](https://github.com/fastapi-practices/fastapi_best_architecture/pull/564)
* Update custom validation error messages by [@wu-clan](https://github.com/wu-clan) in [#566](https://github.com/fastapi-practices/fastapi_best_architecture/pull/566)
* Update the number of pagination le by [@wu-clan](https://github.com/wu-clan) in [#565](https://github.com/fastapi-practices/fastapi_best_architecture/pull/565)
* Fix the login password verification by [@wu-clan](https://github.com/wu-clan) in [#568](https://github.com/fastapi-practices/fastapi_best_architecture/pull/568)
* Fix the failure hook of celery task by [@wu-clan](https://github.com/wu-clan) in [#569](https://github.com/fastapi-practices/fastapi_best_architecture/pull/569)
* Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 by [@wu-clan](https://github.com/wu-clan) in [#570](https://github.com/fastapi-practices/fastapi_best_architecture/pull/570)
* Fix the log rule in gitignore by [@wu-clan](https://github.com/wu-clan) in [#571](https://github.com/fastapi-practices/fastapi_best_architecture/pull/571)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[Changes][v1.0.5]
<a id="v1.0.4"></a>
# [v1.0.4](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.4) - 2025-03-28
## What's Changed
* Update the changelog for v1.0.3 by [@wu-clan](https://github.com/wu-clan) in [#543](https://github.com/fastapi-practices/fastapi_best_architecture/pull/543)
* Updated the backend contribution guide by [@wu-clan](https://github.com/wu-clan) in [#544](https://github.com/fastapi-practices/fastapi_best_architecture/pull/544)
* Optimize the return of relationship interfaces by [@wu-clan](https://github.com/wu-clan) in [#545](https://github.com/fastapi-practices/fastapi_best_architecture/pull/545)
* Optimize the dynamic import of data models by [@wu-clan](https://github.com/wu-clan) in [#546](https://github.com/fastapi-practices/fastapi_best_architecture/pull/546)
* Update git and docker ignore files by [@wu-clan](https://github.com/wu-clan) in [#547](https://github.com/fastapi-practices/fastapi_best_architecture/pull/547)
* Optimize dependencies to reduce package size by [@wu-clan](https://github.com/wu-clan) in [#548](https://github.com/fastapi-practices/fastapi_best_architecture/pull/548)
* Fix async install plugin dependencies for windows by [@wu-clan](https://github.com/wu-clan) in [#549](https://github.com/fastapi-practices/fastapi_best_architecture/pull/549)
* Fix return schema of the config api by [@wu-clan](https://github.com/wu-clan) in [#551](https://github.com/fastapi-practices/fastapi_best_architecture/pull/551)
* Optimize schemas with model relationships by [@wu-clan](https://github.com/wu-clan) in [#552](https://github.com/fastapi-practices/fastapi_best_architecture/pull/552)
* Fix filters for opera log query list by [@ThankCat](https://github.com/ThankCat) in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
* Fix the celery env in docker compose by [@wu-clan](https://github.com/wu-clan) in [#555](https://github.com/fastapi-practices/fastapi_best_architecture/pull/555)
* Update volumes of redis in docker compose by [@wu-clan](https://github.com/wu-clan) in [#556](https://github.com/fastapi-practices/fastapi_best_architecture/pull/556)
* Fix the query for the sub department by [@PoetryL](https://github.com/PoetryL) in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
* Optimize codes and comments with cursor by [@wu-clan](https://github.com/wu-clan) in [#550](https://github.com/fastapi-practices/fastapi_best_architecture/pull/550)
## New Contributors
* [@ThankCat](https://github.com/ThankCat) made their first contribution in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
* [@PoetryL](https://github.com/PoetryL) made their first contribution in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[Changes][v1.0.4]
<a id="v1.0.3"></a>
# [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11
## What's Changed
* Update the changelog for v1.0.2 by [@wu-clan](https://github.com/wu-clan) in [#536](https://github.com/fastapi-practices/fastapi_best_architecture/pull/536)
* Update docker scripts in backend README by [@wu-clan](https://github.com/wu-clan) in [#537](https://github.com/fastapi-practices/fastapi_best_architecture/pull/537)
* Refactor toml and dependencies file dir by [@wu-clan](https://github.com/wu-clan) in [#538](https://github.com/fastapi-practices/fastapi_best_architecture/pull/538)
* Fix typos in Dockerfile comments by [@huyuwei1996](https://github.com/huyuwei1996) in [#539](https://github.com/fastapi-practices/fastapi_best_architecture/pull/539)
* Fix Dockerfile mounts for dependency installation by [@huyuwei1996](https://github.com/huyuwei1996) in [#540](https://github.com/fastapi-practices/fastapi_best_architecture/pull/540)
* Add Aliyun mirror to PyPI index in pyproject.toml by [@huyuwei1996](https://github.com/huyuwei1996) in [#541](https://github.com/fastapi-practices/fastapi_best_architecture/pull/541)
* Update docker scripts and nginx conf by [@wu-clan](https://github.com/wu-clan) in [#542](https://github.com/fastapi-practices/fastapi_best_architecture/pull/542)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[Changes][v1.0.3]
<a id="v1.0.2"></a>
# [v1.0.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.2) - 2025-03-01
## What's Changed
* Update the changelog for v1.0.1 by [@wu-clan](https://github.com/wu-clan) in [#532](https://github.com/fastapi-practices/fastapi_best_architecture/pull/532)
* Fix celery async task worker pool by [@wu-clan](https://github.com/wu-clan) in [#533](https://github.com/fastapi-practices/fastapi_best_architecture/pull/533)
* Add log module root and output levels by [@wu-clan](https://github.com/wu-clan) in [#534](https://github.com/fastapi-practices/fastapi_best_architecture/pull/534)
* Add plugin related interfaces by [@wu-clan](https://github.com/wu-clan) in [#535](https://github.com/fastapi-practices/fastapi_best_architecture/pull/535)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
[Changes][v1.0.2]
<a id="v1.0.1"></a>
# [v1.0.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.1) - 2025-02-26
@@ -342,7 +714,25 @@
[Changes][v1.0.0]
[v1.7.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.6.0...v1.7.0
[v1.6.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.2...v1.6.0
[v1.5.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.1...v1.5.2
[v1.5.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.5.0...v1.5.1
[v1.5.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.0
[v1.4.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
[v1.4.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
[v1.4.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
[v1.4.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
[v1.3.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
[v1.2.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
[v1.1.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
[v1.1.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
[v1.1.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[v1.0.5]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[v1.0.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
[v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
[v1.0.0]: https://github.com/fastapi-practices/fastapi_best_architecture/tree/v1.0.0
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.8.1 -->
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.0 -->
+13 -22
View File
@@ -2,8 +2,7 @@
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/
FROM ghcr.io/astral-sh/uv:python3.10-bookworm-slim AS builder
# Used for build Python packages
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
@@ -11,58 +10,50 @@ RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debi
&& apt-get install -y --no-install-recommends gcc python3-dev \
&& rm -rf /var/lib/apt/lists/*
COPY . /fba
WORKDIR /fba
# Configure uv environment
ENV UV_COMPILE_BYTECODE=1 \
UV_NO_CACHE=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/usr/local \
UV_INDEX=https://mirrors.aliyun.com/pypi/simple
UV_PROJECT_ENVIRONMENT=/usr/local
# Install dependencies with cache
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=backend/uv.lock,target=uv.lock \
--mount=type=bind,source=backend/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 /fba /fba
COPY --from=builder /usr/local /usr/local
# Install plugin dependencies
WORKDIR /fba
ENV PYTHONPATH=/fba
RUN python3 backend/scripts/init_plugin.py
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
# === FastPAI server iamge ===
WORKDIR /fba/backend
# === 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"]
CMD ["/usr/local/bin/granian", "main:app", "--interface", "asgi", "--host", "0.0.0.0", "--port","8000"]
# === Celery server iamge ===
# === 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
+5 -45
View File
@@ -4,14 +4,10 @@
# FastAPI Best Architecture
Enterprise-level backend architecture solution
English | [简体中文](./README.zh-CN.md)
A backend and frontend separation solution based on the FastAPI framework, following
the [pseudo 3-tier architecture](#pseudo-3-tier-architecture) design, supporting **Python 3.10** and above
versions
**🔥Continuously updated and maintained🔥**
[![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
![MySQL](https://img.shields.io/badge/MySQL-8.0%2B-%2300758f)
@@ -23,12 +19,10 @@ versions
![Docker](https://img.shields.io/badge/Docker-%232496ED?logo=docker&logoColor=white)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/yNN3wTbVAC)
![Discord](https://img.shields.io/discord/1185035164577972344)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
</div>
> [!NOTE]
> This repository as a template library open to any person or enterprise can be used for free!
## Pseudo 3-tier architecture
The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating
@@ -47,41 +41,7 @@ pattern, use templates to transform it to your heart's content!
| data access | dao / mapper | crud |
| model | model / entity | model |
## Features
- [x] Design with FastAPI PEP 593 Annotated Parameters
- [x] Global asynchronous design with async/await + asgiref
- [x] Follows Restful API specification
- [x] Global SQLAlchemy 2.0 syntax
- [x] Pydantic v1 and v2 (different branches)
- [x] Casbin RBAC access control model
- [x] Role menu RBAC access control model
- [x] Celery asynchronous tasks
- [x] JWT middleware whitelist authentication
- [x] Global customizable time zone time
- [x] Docker / Docker-compose deployment
- [x] Pytest Unit Testing
## Built-in features
- [x] User management: System User Role Management, Permission Allocation
- [x] Department management: Configure system organization (company, department, team...)
- [x] Menu management: Configure system menu, user menu, button permission tags
- [x] Role management: role menu permission allocation, role route permission allocation
- [x] Dictionary management: Maintain commonly used fixed data or parameters within the system
- [x] Token management: System user online status detection, supports kicking users offline
- [x] Login authentication: backend-based graphical captcha background authentication login
- [x] Multipoint login: One-click modification of multipoint login through user information
- [x] OAuth 2.0: Built-in self-developed OAuth 2.0 login integration
- [x] Code generation: automatic backend code generation, supports preview, writing, and download
- [x] Scheduled task: Automated task, asynchronous task, supports function calls
- [x] Plugin system: Say goodbye to high coupling integration through hot-pluggable plugin mode
- [x] Operation log: Record and query of system normal and abnormal operations
- [x] Login log: Record and query of normal and abnormal user login
- [x] Service monitoring: Server hardware device information and status
- [x] API documentation: Automatically generate online interactive API documentation
## Development and deployment
## Help
For more details, please check
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
@@ -103,7 +63,7 @@ the [official documentation](https://fastapi-practices.github.io/fastapi_best_ar
## Interactivity
[TG / Discord](https://wu-clan.github.io/homepage/)
[Discord](https://wu-clan.github.io/homepage/)
## Sponsor us
+5 -43
View File
@@ -4,12 +4,10 @@
# FastAPI Best Architecture
企业级后端架构解决方案
简体中文 | [English](./README.md)
基于 FastAPI 框架的前后端分离中后台解决方案,遵循[伪三层架构](#伪三层架构)设计, 支持 **python3.10** 及以上版本
**🔥持续更新维护中🔥**
[![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
![MySQL](https://img.shields.io/badge/MySQL-8.0%2B-%2300758f)
@@ -21,12 +19,10 @@
![Docker](https://img.shields.io/badge/Docker-%232496ED?logo=docker&logoColor=white)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/yNN3wTbVAC)
![Discord](https://img.shields.io/discord/1185035164577972344)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
</div>
> [!NOTE]
> 此仓库作为模板库公开,任何人或企业均可免费使用!
## 伪三层架构
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
@@ -43,41 +39,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
| 数据访问 | dao / mapper | crud |
| 模型 | model / entity | model |
## 特征
- [x] 全局 FastAPI PEP 593 Annotated 参数风格
- [x] async/await + asgiref 的全局异步设计
- [x] 遵循 Restful API 规范
- [x] 全局 SQLAlchemy 2.0 语法
- [x] Pydantic v1 和 v2 (不同分支)
- [x] Casbin RBAC 访问控制模型
- [x] 角色菜单 RBAC 访问控制模型
- [x] Celery 异步任务
- [x] JWT 中间件白名单认证
- [x] 全局自定义时区时间
- [x] Docker / Docker-compose 部署
- [x] Pytest 单元测试
## 内置功能
- [x] 用户管理:系统用户角色管理,权限分配
- [x] 部门管理:配置系统组织机构(公司、部门、小组...)
- [x] 菜单管理:配置系统菜单,用户菜单,按钮权限标识
- [x] 角色管理:角色菜单权限分配,角色路由权限分配
- [x] 字典管理:维护系统内部常用固定数据或参数
- [x] 令牌管理:系统用户在线状态检测,支持踢人下线
- [x] 登录认证:基于后端的图形验证码后台认证登录
- [x] 多点登录:通过用户信息一键修改多点登录支持
- [x] OAuth20:内置自研 OAuth 2.0 登录集成
- [x] 代码生成:后端代码自动生成,支持预览,写入及下载
- [x] 定时任务:自动化任务,异步任务,支持函数调用
- [x] 插件系统:通过热插拔插件模式告别高耦合集成
- [x] 操作日志:系统正常和异常操作的日志记录与查询
- [x] 登录日志:用户正常和异常登录的日志记录与查询
- [x] 服务监控:服务器硬件设备信息与状态
- [x] 接口文档:自动生成在线交互式 API 文档
## 开发部署
## 帮助
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
@@ -98,7 +60,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
+12 -11
View File
@@ -15,18 +15,19 @@ REDIS_DATABASE=0
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
# Opera Log
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
# Admin
# OAuth2
# [ App ] task
# Celery
CELERY_BROKER_REDIS_DATABASE=1
# Rabbitmq
CELERY_RABBITMQ_HOST='127.0.0.1'
CELERY_RABBITMQ_PORT=5672
CELERY_RABBITMQ_USERNAME='guest'
CELERY_RABBITMQ_PASSWORD='guest'
# [ Plugin ] oauth2
OAUTH2_GITHUB_CLIENT_ID='test'
OAUTH2_GITHUB_CLIENT_SECRET='test'
OAUTH2_LINUX_DO_CLIENT_ID='test'
OAUTH2_LINUX_DO_CLIENT_SECRET='test'
# Task
# Celery
CELERY_BROKER_REDIS_DATABASE=1
CELERY_BACKEND_REDIS_DATABASE=2
# Rabbitmq
RABBITMQ_HOST='127.0.0.1'
RABBITMQ_PORT=5672
RABBITMQ_USERNAME='guest'
RABBITMQ_PASSWORD='guest'
# [ Plugin ] email
EMAIL_USERNAME=''
EMAIL_PASSWORD=''
+1 -7
View File
@@ -1,12 +1,6 @@
__pycache__/
.env
venv/
.venv/
.mypy_cache/
log/
alembic/versions/
static/media/
.ruff_cache/
.pytest_cache/
.python-version
*.log
celerybeat-schedule.*
+28 -24
View File
@@ -1,64 +1,68 @@
# FBA Project - Backend
# FastAPI Best Architecture - Backend
## 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
2. Run the following Docker command to build container:
```shell
docker build -f backend/backend.dockerfile -t fba_backend_independent .
docker build -f Dockerfile -t fba_backend_independent .
```
3. Start container
Native boot needs to change `127.0.0.1` in `.env` to `host.docker.internal`
```shell
docker run -d fba_backend_independent -p 8000:8000 --name fba_app
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
```
## Contributing
1. Prerequisites
You'll need the following prerequisites:
- Any python version between Python >= 3.10
- Python >= 3.10
- Git
- [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
Clone your fork and cd into the repo directory
Clone your forked repository:
```shell
git clone https://github.com/<your username>/fastapi_best_architecture.git
cd fastapi_best_architecture/backend
uv venv
uv pip install -r requirements.txt
git clone https://github.com/<your account>/fastapi_best_architecture.git
```
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
# Checkout a new branch and make your changes
git checkout -b your-new-feature-branch
```
4. Run linting
4. Format and Lint
Auto-formatting and lint via `pre-commit`
```shell
# Run automated code formatting and linting
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
@@ -68,7 +72,7 @@
>
> It is recommended to execute under the backend directory, and chmod authorization may be required
- `pre_start.sh`: Perform automatic database migration and create database tables
- `pre_start.sh`: Perform automatic database migration
- `celery-start.sh`: For celery docker script, implementation is not recommended
+12
View File
@@ -1,2 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.i18n import i18n
from backend.utils.console import console
__version__ = '1.8.0'
def get_version() -> str | None:
console.print(f'[cyan]{__version__}[/]')
# 初始化 i18n
i18n.load_locales()
+4 -9
View File
@@ -13,21 +13,16 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
sys.path.append('../')
from backend.app import get_app_models
from backend.common.model import MappedBase
from backend.core import path_conf
from backend.database.db import SQLALCHEMY_DATABASE_URL
from backend.plugin.tools import get_plugin_models
# import your new model here
from backend.app.admin.model import * # noqa: F401
from backend.app.generator.model import * # noqa: F401
# import plugin model
for cls in get_plugin_models():
# import models
for cls in get_app_models() + get_plugin_models():
class_name = cls.__name__
if class_name in globals():
print(f'\nWarning: Class "{class_name}" already exists in global namespace.')
else:
if class_name not in globals():
globals()[class_name] = cls
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
+43
View File
@@ -1,2 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import inspect
import os.path
from backend.common.log import log
from backend.core.path_conf import BASE_PATH
from backend.utils.import_parse import import_module_cached
def get_app_models():
"""获取 app 所有模型类"""
app_path = os.path.join(BASE_PATH, 'app')
list_dirs = os.listdir(app_path)
apps = []
for d in list_dirs:
if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__':
apps.append(d)
classes = []
for app in apps:
try:
module_path = f'backend.app.{app}.model'
module = import_module_cached(module_path)
except ModuleNotFoundError as e:
log.warning(f'应用 {app} 中不包含 model 相关配置: {e}')
continue
except Exception as e:
raise e
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj):
classes.append(obj)
return classes
# import all app models for auto create db tables
for cls in get_app_models():
class_name = cls.__name__
if class_name not in globals():
globals()[class_name] = cls
-2
View File
@@ -5,14 +5,12 @@ from fastapi import APIRouter
from backend.app.admin.api.v1.auth import router as auth_router
from backend.app.admin.api.v1.log import router as log_router
from backend.app.admin.api.v1.monitor import router as monitor_router
from backend.app.admin.api.v1.oauth2 import router as oauth2_router
from backend.app.admin.api.v1.sys import router as sys_router
from backend.core.conf import settings
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
v1.include_router(auth_router)
v1.include_router(oauth2_router)
v1.include_router(sys_router)
v1.include_router(log_router)
v1.include_router(monitor_router)
+1 -1
View File
@@ -8,4 +8,4 @@ from backend.app.admin.api.v1.auth.captcha import router as captcha_router
router = APIRouter(prefix='/auth')
router.include_router(auth_router, tags=['授权'])
router.include_router(captcha_router, prefix='/captcha', tags=['验证码'])
router.include_router(captcha_router, tags=['验证码'])
+14 -8
View File
@@ -17,9 +17,9 @@ router = APIRouter()
@router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证')
async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken:
async def login_swagger(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken:
token, user = await auth_service.swagger_login(obj=obj)
return GetSwaggerToken(access_token=token, user=user) # type: ignore
return GetSwaggerToken(access_token=token, user=user)
@router.post(
@@ -28,20 +28,26 @@ async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetS
description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman',
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
)
async def user_login(
async def login(
request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
) -> ResponseSchemaModel[GetLoginToken]:
data = await auth_service.login(request=request, response=response, obj=obj, background_tasks=background_tasks)
return response_base.success(data=data)
@router.post('/token/new', summary='创建新 token')
async def create_new_token(request: Request, response: Response) -> ResponseSchemaModel[GetNewToken]:
data = await auth_service.new_token(request=request)
@router.get('/codes', summary='获取所有授权码', description='适配 vben admin v5', dependencies=[DependsJwtAuth])
async def get_codes(request: Request) -> ResponseSchemaModel[list[str]]:
codes = await auth_service.get_codes(request=request)
return response_base.success(data=codes)
@router.post('/tokens', summary='刷新 token')
async def refresh_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
data = await auth_service.refresh_token(request=request)
return response_base.success(data=data)
@router.post('/logout', summary='用户登出', dependencies=[DependsJwtAuth])
async def user_logout(request: Request, response: Response) -> ResponseModel:
@router.post('/logout', summary='用户登出')
async def logout(request: Request, response: Response) -> ResponseModel:
await auth_service.logout(request=request, response=response)
return response_base.success()
+4 -4
View File
@@ -5,16 +5,16 @@ from fastapi import APIRouter, Depends, Request
from fastapi_limiter.depends import RateLimiter
from starlette.concurrency import run_in_threadpool
from backend.app.admin.conf import admin_settings
from backend.app.admin.schema.captcha import GetCaptchaDetail
from backend.common.response.response_schema import ResponseSchemaModel, response_base
from backend.core.conf import settings
from backend.database.redis import redis_client
router = APIRouter()
@router.get(
'',
'/captcha',
summary='获取登录验证码',
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
)
@@ -26,9 +26,9 @@ async def get_captcha(request: Request) -> ResponseSchemaModel[GetCaptchaDetail]
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
ip = request.state.ip
await redis_client.set(
f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}',
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}',
code,
ex=admin_settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS,
)
data = GetCaptchaDetail(image_type=img_type, image=img)
return response_base.success(data=data)
+12 -14
View File
@@ -4,7 +4,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Query
from backend.app.admin.schema.login_log import GetLoginLogDetail
from backend.app.admin.schema.login_log import DeleteLoginLogParam, GetLoginLogDetail
from backend.app.admin.service.login_log_service import login_log_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -18,17 +18,17 @@ router = APIRouter()
@router.get(
'',
summary='(模糊条件)分页获取登录日志',
summary='分页获取登录日志',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_login_logs(
async def get_login_logs_paged(
db: CurrentSession,
username: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
ip: Annotated[str | None, Query()] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
ip: Annotated[str | None, Query(description='IP 地址')] = None,
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
log_select = await login_log_service.get_select(username=username, status=status, ip=ip)
page_data = await paging_data(db, log_select)
@@ -37,14 +37,14 @@ async def get_pagination_login_logs(
@router.delete(
'',
summary='批量删除登录日志',
summary='批量删除登录日志',
dependencies=[
Depends(RequestPermission('log:login:del')),
DependsRBAC,
],
)
async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
count = await login_log_service.delete(pk=pk)
async def delete_login_logs(obj: DeleteLoginLogParam) -> ResponseModel:
count = await login_log_service.delete(obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -54,12 +54,10 @@ async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> ResponseMode
'/all',
summary='清空登录日志',
dependencies=[
Depends(RequestPermission('log:login:empty')),
Depends(RequestPermission('log:login:clear')),
DependsRBAC,
],
)
async def delete_all_login_logs() -> ResponseModel:
count = await login_log_service.delete_all()
if count > 0:
return response_base.success()
return response_base.fail()
await login_log_service.delete_all()
return response_base.success()
+12 -14
View File
@@ -4,7 +4,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Query
from backend.app.admin.schema.opera_log import GetOperaLogDetail
from backend.app.admin.schema.opera_log import DeleteOperaLogParam, GetOperaLogDetail
from backend.app.admin.service.opera_log_service import opera_log_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -18,17 +18,17 @@ router = APIRouter()
@router.get(
'',
summary='(模糊条件)分页获取操作日志',
summary='分页获取操作日志',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_opera_logs(
async def get_opera_logs_paged(
db: CurrentSession,
username: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
ip: Annotated[str | None, Query()] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
ip: Annotated[str | None, Query(description='IP 地址')] = None,
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
log_select = await opera_log_service.get_select(username=username, status=status, ip=ip)
page_data = await paging_data(db, log_select)
@@ -37,14 +37,14 @@ async def get_pagination_opera_logs(
@router.delete(
'',
summary='批量删除操作日志',
summary='批量删除操作日志',
dependencies=[
Depends(RequestPermission('log:opera:del')),
DependsRBAC,
],
)
async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
count = await opera_log_service.delete(pk=pk)
async def delete_opera_logs(obj: DeleteOperaLogParam) -> ResponseModel:
count = await opera_log_service.delete(obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -54,12 +54,10 @@ async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> ResponseMode
'/all',
summary='清空操作日志',
dependencies=[
Depends(RequestPermission('log:opera:empty')),
Depends(RequestPermission('log:opera:clear')),
DependsRBAC,
],
)
async def delete_all_opera_logs() -> ResponseModel:
count = await opera_log_service.delete_all()
if count > 0:
return response_base.success()
return response_base.fail()
await opera_log_service.delete_all()
return response_base.success()
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.monitor.online import router as token_router
from backend.app.admin.api.v1.monitor.redis import router as redis_router
from backend.app.admin.api.v1.monitor.server import router as server_router
@@ -9,3 +10,4 @@ router = APIRouter(prefix='/monitors')
router.include_router(redis_router, prefix='/redis', tags=['redis监控'])
router.include_router(server_router, prefix='/server', tags=['服务器监控'])
router.include_router(token_router, prefix='/sessions', tags=['会话监控'])
@@ -6,10 +6,10 @@ from typing import Annotated
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
from backend.common.enums import StatusType
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, superuser_verify
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.core.conf import settings
@@ -18,17 +18,36 @@ from backend.database.redis import redis_client
router = APIRouter()
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
async def get_tokens(username: Annotated[str | None, Query()] = None) -> ResponseSchemaModel[list[GetTokenDetail]]:
@router.get('', summary='获取在线用户', dependencies=[DependsJwtAuth])
async def get_sessions(
username: Annotated[str | None, Query(description='用户名')] = None,
) -> ResponseSchemaModel[list[GetTokenDetail]]:
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
token_online = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
data = []
online_clients = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
data: list[GetTokenDetail] = []
def append_token_detail() -> None:
data.append(
token_detail.model_copy(
update={
'username': extra_info.get('username', '未知'),
'nickname': extra_info.get('nickname', '未知'),
'ip': extra_info.get('ip', '未知'),
'os': extra_info.get('os', '未知'),
'browser': extra_info.get('browser', '未知'),
'device': extra_info.get('device', '未知'),
'last_login_time': extra_info.get('last_login_time', '未知'),
}
)
)
for key in token_keys:
token = await redis_client.get(key)
token_payload = jwt_decode(token)
user_id = token_payload.id
session_uuid = token_payload.session_uuid
token_detail = GetTokenDetail(
id=token_payload.id,
id=user_id,
session_uuid=session_uuid,
username='未知',
nickname='未知',
@@ -36,31 +55,16 @@ async def get_tokens(username: Annotated[str | None, Query()] = None) -> Respons
os='未知',
browser='未知',
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='未知',
expire_time=token_payload.expire_time,
)
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
if extra_info:
def append_token_detail():
data.append(
token_detail.model_copy(
update={
'username': extra_info.get('username'),
'nickname': extra_info.get('nickname'),
'ip': extra_info.get('ip'),
'os': extra_info.get('os'),
'browser': extra_info.get('browser'),
'device': extra_info.get('device'),
'last_login_time': extra_info.get('last_login_time'),
}
)
)
extra_info = json.loads(extra_info)
if extra_info.get('login_type') != 'swagger':
if username:
# 排除 swagger 登录生成的 token
if extra_info.get('swagger') is None:
if username is not None:
if username == extra_info.get('username'):
append_token_detail()
else:
@@ -72,13 +76,17 @@ async def get_tokens(username: Annotated[str | None, Query()] = None) -> Respons
@router.delete(
'/{pk}',
summary='下线',
summary='强制下线',
dependencies=[
Depends(RequestPermission('sys:token:kick')),
Depends(RequestPermission('sys:session:delete')),
DependsRBAC,
],
)
async def kick_out(request: Request, pk: Annotated[int, Path(...)], session_uuid: KickOutToken) -> ResponseModel:
async def delete_session(
request: Request,
pk: Annotated[int, Path(description='用户 ID')],
session_uuid: Annotated[str, Query(description='会话 UUID')],
) -> ResponseModel:
superuser_verify(request)
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{pk}:{session_uuid}')
await revoke_token(pk, session_uuid)
return response_base.success()
+6 -11
View File
@@ -1,23 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends
from fastapi import APIRouter
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.utils.redis_info import redis_info
router = APIRouter()
@router.get(
'',
summary='redis 监控',
dependencies=[
Depends(RequestPermission('sys:monitor:redis')),
DependsJwtAuth,
],
)
@router.get('', summary='redis 监控', dependencies=[DependsJwtAuth])
async def get_redis_info() -> ResponseModel:
data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()}
data = {
'info': await redis_info.get_info(),
'stats': await redis_info.get_stats(),
}
return response_base.success(data=data)
+2 -10
View File
@@ -1,24 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends
from fastapi import APIRouter
from starlette.concurrency import run_in_threadpool
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.utils.server_info import server_info
router = APIRouter()
@router.get(
'',
summary='server 监控',
dependencies=[
Depends(RequestPermission('sys:monitor:server')),
DependsJwtAuth,
],
)
@router.get('', summary='server 监控', dependencies=[DependsJwtAuth])
async def get_server_info() -> ResponseModel:
data = {
# 扔到线程池,避免阻塞
@@ -1,11 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.oauth2.github import router as github_router
from backend.app.admin.api.v1.oauth2.linux_do import router as linux_do_router
router = APIRouter(prefix='/oauth2')
router.include_router(github_router, prefix='/github', tags=['GitHub OAuth2'])
router.include_router(linux_do_router, prefix='/linux-do', tags=['LinuxDo OAuth2'])
+7 -13
View File
@@ -1,29 +1,23 @@
#!/usr/bin/env python3
# !/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.sys.config import router as config_router
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
from backend.app.admin.api.v1.sys.data_scope import router as data_scope_router
from backend.app.admin.api.v1.sys.dept import router as dept_router
from backend.app.admin.api.v1.sys.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.files import router as file_router
from backend.app.admin.api.v1.sys.menu import router as menu_router
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
from backend.app.admin.api.v1.sys.role import router as role_router
from backend.app.admin.api.v1.sys.token import router as token_router
from backend.app.admin.api.v1.sys.upload import router as upload_router
from backend.app.admin.api.v1.sys.user import router as user_router
router = APIRouter(prefix='/sys')
router.include_router(config_router, prefix='/configs', tags=['系统配置'])
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
router.include_router(menu_router, prefix='/menus', tags=['系统菜单'])
router.include_router(role_router, prefix='/roles', tags=['系统角色'])
router.include_router(user_router, prefix='/users', tags=['系统用户'])
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则'])
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
router.include_router(file_router, prefix='/files', tags=['系统文件'])
router.include_router(plugin_router, prefix='/plugins', tags=['系统插件'])
-145
View File
@@ -1,145 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.config import (
CreateConfigParam,
GetConfigDetail,
SaveBuiltInConfigParam,
UpdateConfigParam,
)
from backend.app.admin.service.config_service import config_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
router = APIRouter()
@router.get('/website', summary='获取网站配置信息', dependencies=[DependsJwtAuth])
async def get_website_config() -> ResponseSchemaModel[GetConfigDetail]:
config = await config_service.get_built_in_config('website')
return response_base.success(data=config)
@router.post(
'/website',
summary='保存网站配置信息',
dependencies=[
Depends(RequestPermission('sys:config:website:add')),
DependsRBAC,
],
)
async def save_website_config(objs: list[SaveBuiltInConfigParam]) -> ResponseModel:
await config_service.save_built_in_config(objs, 'website')
return response_base.success()
@router.get('/protocol', summary='获取用户协议', dependencies=[DependsJwtAuth])
async def get_protocol_config() -> ResponseSchemaModel[GetConfigDetail]:
config = await config_service.get_built_in_config('protocol')
return response_base.success(data=config)
@router.post(
'/protocol',
summary='保存用户协议',
dependencies=[
Depends(RequestPermission('sys:config:protocol:add')),
DependsRBAC,
],
)
async def save_protocol_config(objs: list[SaveBuiltInConfigParam]) -> ResponseModel:
await config_service.save_built_in_config(objs, 'protocol')
return response_base.success()
@router.get('/policy', summary='获取用户政策', dependencies=[DependsJwtAuth])
async def get_policy_config() -> ResponseSchemaModel[GetConfigDetail]:
config = await config_service.get_built_in_config('policy')
return response_base.success(data=config)
@router.post(
'/policy',
summary='保存用户政策',
dependencies=[
Depends(RequestPermission('sys:config:policy:add')),
DependsRBAC,
],
)
async def save_policy_config(objs: list[SaveBuiltInConfigParam]) -> ResponseModel:
await config_service.save_built_in_config(objs, 'policy')
return response_base.success()
@router.get('/{pk}', summary='获取系统参数配置详情', dependencies=[DependsJwtAuth])
async def get_config(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetConfigDetail]:
config = await config_service.get(pk)
return response_base.success(data=config)
@router.get(
'',
summary='(模糊条件)分页获取所有系统参数配置',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_config(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
type: Annotated[str | None, Query()] = None,
) -> ResponseSchemaModel[PageData[GetConfigDetail]]:
config_select = await config_service.get_select(name=name, type=type)
page_data = await paging_data(db, config_select)
return response_base.success(data=page_data)
@router.post(
'',
summary='创建系统参数配置',
dependencies=[
Depends(RequestPermission('sys:config:add')),
DependsRBAC,
],
)
async def create_config(obj: CreateConfigParam) -> ResponseModel:
await config_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新系统参数配置',
dependencies=[
Depends(RequestPermission('sys:config:edit')),
DependsRBAC,
],
)
async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -> ResponseModel:
count = await config_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'',
summary='(批量)删除系统参数配置',
dependencies=[
Depends(RequestPermission('sys:config:del')),
DependsRBAC,
],
)
async def delete_config(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
count = await config_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+31 -22
View File
@@ -2,9 +2,15 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.data_rule import CreateDataRuleParam, GetDataRuleDetail, UpdateDataRuleParam
from backend.app.admin.schema.data_rule import (
CreateDataRuleParam,
DeleteDataRuleParam,
GetDataRuleColumnDetail,
GetDataRuleDetail,
UpdateDataRuleParam,
)
from backend.app.admin.service.data_rule_service import data_rule_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -12,47 +18,48 @@ from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()
@router.get('/models', summary='获取支持过滤的数据库模型', dependencies=[DependsJwtAuth])
@router.get('/models', summary='获取数据规则可用模型', dependencies=[DependsJwtAuth])
async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
models = await data_rule_service.get_models()
return response_base.success(data=models)
@router.get('/model/{model}/columns', summary='获取支持过滤的数据库模型列', dependencies=[DependsJwtAuth])
async def get_data_rule_model_columns(model: Annotated[str, Path()]) -> ResponseSchemaModel[list[str]]:
@router.get('/models/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
async def get_data_rule_model_columns(
model: Annotated[str, Path(description='模型名称')],
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
models = await data_rule_service.get_columns(model=model)
return response_base.success(data=models)
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
async def get_all_data_rule() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
data_rules = await data_rule_service.get_all()
data = select_list_serialize(data_rules)
async def get_all_data_rules() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
data = await data_rule_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth])
async def get_data_rule(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDataRuleDetail]:
data_rule = await data_rule_service.get(pk=pk)
data = GetDataRuleDetail(**select_as_dict(data_rule))
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
async def get_data_rule(
pk: Annotated[int, Path(description='数据规则 ID')],
) -> ResponseSchemaModel[GetDataRuleDetail]:
data = await data_rule_service.get(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='(模糊条件)分页获取所有数据权限规则',
summary='分页获取所有数据规则',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_data_rule(
db: CurrentSession, name: Annotated[str | None, Query()] = None
async def get_data_rules_paged(
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
data_rule_select = await data_rule_service.get_select(name=name)
page_data = await paging_data(db, data_rule_select)
@@ -61,7 +68,7 @@ async def get_pagination_data_rule(
@router.post(
'',
summary='创建数据权限规则',
summary='创建数据规则',
dependencies=[
Depends(RequestPermission('data:rule:add')),
DependsRBAC,
@@ -74,13 +81,15 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
@router.put(
'/{pk}',
summary='更新数据权限规则',
summary='更新数据规则',
dependencies=[
Depends(RequestPermission('data:rule:edit')),
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)
if count > 0:
return response_base.success()
@@ -89,14 +98,14 @@ async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRulePar
@router.delete(
'',
summary='批量删除数据权限规则',
summary='批量删除数据规则',
dependencies=[
Depends(RequestPermission('data:rule:del')),
DependsRBAC,
],
)
async def delete_data_rule(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
count = await data_rule_service.delete(request=request, pk=pk)
async def delete_data_rules(obj: DeleteDataRuleParam) -> ResponseModel:
count = await data_rule_service.delete(obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.data_scope import (
CreateDataScopeParam,
DeleteDataScopeParam,
GetDataScopeDetail,
GetDataScopeWithRelationDetail,
UpdateDataScopeParam,
UpdateDataScopeRuleParam,
)
from backend.app.admin.service.data_scope_service import data_scope_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
router = APIRouter()
@router.get('/all', summary='获取所有数据范围', dependencies=[DependsJwtAuth])
async def get_all_data_scope() -> ResponseSchemaModel[list[GetDataScopeDetail]]:
data = await data_scope_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
async def get_data_scope(
pk: Annotated[int, Path(description='数据范围 ID')],
) -> ResponseSchemaModel[GetDataScopeDetail]:
data = await data_scope_service.get(pk=pk)
return response_base.success(data=data)
@router.get('/{pk}/rules', summary='获取数据范围所有规则', dependencies=[DependsJwtAuth])
async def get_data_scope_rules(
pk: Annotated[int, Path(description='数据范围 ID')],
) -> ResponseSchemaModel[GetDataScopeWithRelationDetail]:
data = await data_scope_service.get_rules(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='分页获取所有数据范围',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_data_scopes_paged(
db: CurrentSession,
name: Annotated[str | None, Query(description='范围名称')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetDataScopeDetail]]:
data_scope_select = await data_scope_service.get_select(name=name, status=status)
page_data = await paging_data(db, data_scope_select)
return response_base.success(data=page_data)
@router.post(
'',
summary='创建数据范围',
dependencies=[
Depends(RequestPermission('data:scope:add')),
DependsRBAC,
],
)
async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
await data_scope_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新数据范围',
dependencies=[
Depends(RequestPermission('data:scope:edit')),
DependsRBAC,
],
)
async def update_data_scope(
pk: Annotated[int, Path(description='数据范围 ID')], obj: UpdateDataScopeParam
) -> ResponseModel:
count = await data_scope_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put(
'/{pk}/rules',
summary='更新数据范围规则',
dependencies=[
Depends(RequestPermission('data:scope:rule:edit')),
DependsRBAC,
],
)
async def update_data_scope_rules(
pk: Annotated[int, Path(description='数据范围 ID')], rule_ids: UpdateDataScopeRuleParam
):
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'',
summary='批量删除数据范围',
dependencies=[
Depends(RequestPermission('data:scope:del')),
DependsRBAC,
],
)
async def delete_data_scopes(obj: DeleteDataScopeParam) -> ResponseModel:
count = await data_scope_service.delete(obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
+13 -14
View File
@@ -10,26 +10,25 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
async def get_dept(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDeptDetail]:
dept = await dept_service.get(pk=pk)
data = GetDeptDetail(**select_as_dict(dept))
async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseSchemaModel[GetDeptDetail]:
data = await dept_service.get(pk=pk)
return response_base.success(data=data)
@router.get('', summary='获取所有部门展示', dependencies=[DependsJwtAuth])
async def get_all_depts_tree(
name: Annotated[str | None, Query()] = None,
leader: Annotated[str | None, Query()] = None,
phone: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
@router.get('', summary='获取部门', dependencies=[DependsJwtAuth])
async def get_dept_tree(
request: Request,
name: Annotated[str | None, Query(description='部门名称')] = None,
leader: Annotated[str | None, Query(description='部门负责人')] = None,
phone: Annotated[str | None, Query(description='联系电话')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[list[dict[str, Any]]]:
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status)
dept = await dept_service.get_tree(request=request, name=name, leader=leader, phone=phone, status=status)
return response_base.success(data=dept)
@@ -54,7 +53,7 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel:
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)
if count > 0:
return response_base.success()
@@ -69,8 +68,8 @@ async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> Re
DependsRBAC,
],
)
async def delete_dept(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await dept_service.delete(request=request, pk=pk)
async def delete_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.dict_data import CreateDictDataParam, GetDictDataDetail, UpdateDictDataParam
from backend.app.admin.service.dict_data_service import dict_data_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth])
async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDictDataDetail]:
dict_data = await dict_data_service.get(pk=pk)
data = GetDictDataDetail(**select_as_dict(dict_data))
return response_base.success(data=data)
@router.get(
'',
summary='(模糊条件)分页获取所有字典',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_dict_datas(
db: CurrentSession,
label: Annotated[str | None, Query()] = None,
value: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
) -> ResponseSchemaModel[PageData[GetDictDataDetail]]:
dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status)
page_data = await paging_data(db, dict_data_select)
return response_base.success(data=page_data)
@router.post(
'',
summary='创建字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:add')),
DependsRBAC,
],
)
async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel:
await dict_data_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:edit')),
DependsRBAC,
],
)
async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataParam) -> ResponseModel:
count = await dict_data_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'',
summary='(批量)删除字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:del')),
DependsRBAC,
],
)
async def delete_dict_data(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
count = await dict_data_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, File, UploadFile
from backend.common.dataclasses import UploadUrl
from backend.common.response.response_schema import ResponseSchemaModel, response_base
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.file_ops import upload_file, upload_file_verify
router = APIRouter()
@router.post(
'/upload',
summary='文件上传',
dependencies=[
Depends(RequestPermission('sys:file:upload')),
DependsRBAC,
],
)
async def upload_files(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
upload_file_verify(file)
filename = await upload_file(file)
return response_base.success(data={'url': f'/static/upload/{filename}'})
+13 -14
View File
@@ -10,29 +10,28 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/sidebar', summary='获取用户菜单展示树', dependencies=[DependsJwtAuth])
async def get_user_sidebar_tree(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_user_menu_tree(request=request)
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='已适配 vben admin v5', dependencies=[DependsJwtAuth])
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
menu = await menu_service.get_sidebar(request=request)
return response_base.success(data=menu)
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
async def get_menu(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetMenuDetail]:
menu = await menu_service.get(pk=pk)
data = GetMenuDetail(**select_as_dict(menu))
async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseSchemaModel[GetMenuDetail]:
data = await menu_service.get(pk=pk)
return response_base.success(data=data)
@router.get('', summary='获取所有菜单展示', dependencies=[DependsJwtAuth])
async def get_all_menus(
title: Annotated[str | None, Query()] = None, status: Annotated[int | None, Query()] = None
@router.get('', summary='获取菜单', dependencies=[DependsJwtAuth])
async def get_menu_tree(
title: Annotated[str | None, Query(description='菜单标题')] = None,
status: Annotated[int | None, Query(description='状体')] = None,
) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_menu_tree(title=title, status=status)
menu = await menu_service.get_tree(title=title, status=status)
return response_base.success(data=menu)
@@ -57,7 +56,7 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel:
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)
if count > 0:
return response_base.success()
@@ -72,8 +71,8 @@ async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> Re
DependsRBAC,
],
)
async def delete_menu(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await menu_service.delete(request=request, pk=pk)
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
count = await menu_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+60 -67
View File
@@ -1,95 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import os.path
import zipfile
from typing import Annotated, Any
from typing import Annotated
from fastapi import APIRouter, Depends, File, UploadFile
from fastapi import APIRouter, Depends, File, Path, 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.app.admin.service.plugin_service import plugin_service
from backend.common.enums import PluginType
from backend.common.response.response_code import CustomResponse
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.core.path_conf import PLUGIN_DIR
from backend.plugin.tools import install_requirements_async
router = APIRouter()
@router.get('', summary='获取所有插件', dependencies=[DependsJwtAuth])
async def get_all_plugins() -> ResponseSchemaModel[list[dict[str, Any]]]:
plugins = await plugin_service.get_all()
return response_base.success(data=plugins)
@router.get('/changed', summary='是否存在插件变更', dependencies=[DependsJwtAuth])
async def plugin_changed() -> ResponseSchemaModel[bool]:
plugins = await plugin_service.changed()
return response_base.success(data=bool(plugins))
@router.post(
'/install',
'',
summary='安装插件',
description='使用插件 zip 压缩包进行安装',
description='使用插件 zip 压缩包或 git 仓库地址进行安装',
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='此插件已安装')
os.makedirs(full_plugin_path)
# 解压安装
members = []
for member in zf.infolist():
if member.filename.startswith(plugin_dir_in_zip):
member.filename = member.filename.replace(plugin_dir_in_zip, '')
if not member.filename:
continue
members.append(member)
zf.extractall(PLUGIN_DIR, members)
if os.path.exists(os.path.join(full_plugin_path, 'requirements.txt')):
await install_requirements_async(False)
return response_base.success()
async def install_plugin(
type: Annotated[PluginType, Query(description='插件类型')],
file: Annotated[UploadFile | None, File()] = None,
repo_url: Annotated[str | None, Query(description='插件 git 仓库地址')] = None,
) -> ResponseModel:
plugin_name = await plugin_service.install(type=type, file=file, repo_url=repo_url)
return response_base.success(
res=CustomResponse(
code=200, msg=f'插件 {plugin_name} 安装成功,请根据插件说明(README.md)进行相关配置并重启服务'
)
)
@router.post(
'/zip',
summary='打包插件',
@router.delete(
'/{plugin}',
summary='卸载插件',
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
dependencies=[
Depends(RequestPermission('sys:plugin:zip')),
Depends(RequestPermission('sys:plugin:uninstall')),
DependsRBAC,
],
)
async def build_plugin_zip(plugin: Annotated[str, Query()]):
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)
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
await plugin_service.uninstall(plugin=plugin)
return response_base.success(
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务')
)
@router.put(
'/{plugin}/status',
summary='更新插件状态',
dependencies=[
Depends(RequestPermission('sys:plugin:edit')),
DependsRBAC,
],
)
async def update_plugin_status(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
await plugin_service.update_status(plugin=plugin)
return response_base.success()
@router.get('/{plugin}', summary='下载插件', dependencies=[DependsJwtAuth])
async def download_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
bio = await plugin_service.build(plugin=plugin)
return StreamingResponse(
bio,
media_type='application/x-zip-compressed',
+32 -40
View File
@@ -2,17 +2,17 @@
# -*- coding: utf-8 -*-
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.role import (
CreateRoleParam,
DeleteRoleParam,
GetRoleDetail,
GetRoleWithRelationDetail,
UpdateRoleMenuParam,
UpdateRoleParam,
UpdateRoleRuleParam,
UpdateRoleScopeParam,
)
from backend.app.admin.service.data_rule_service import data_rule_service
from backend.app.admin.service.menu_service import menu_service
from backend.app.admin.service.role_service import role_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -20,56 +20,48 @@ from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
roles = await role_service.get_all()
data = select_list_serialize(roles)
data = await role_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
async def get_user_all_roles(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetRoleDetail]]:
roles = await role_service.get_by_user(pk=pk)
data = select_list_serialize(roles)
return response_base.success(data=data)
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
async def get_role_all_menus(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_role_menu_tree(pk=pk)
@router.get('/{pk}/menus', summary='获取角色菜单树', dependencies=[DependsJwtAuth])
async def get_role_menu_tree(
pk: Annotated[int, Path(description='角色 ID')],
) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
menu = await role_service.get_menu_tree(pk=pk)
return response_base.success(data=menu)
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth])
async def get_role_all_rules(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[int]]:
rule = await data_rule_service.get_role_rules(pk=pk)
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
async def get_role_scopes(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
rule = await role_service.get_scopes(pk=pk)
return response_base.success(data=rule)
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
async def get_role(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetRoleDetail]:
role = await role_service.get(pk=pk)
data = GetRoleDetail(**select_as_dict(role))
async def get_role(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
data = await role_service.get(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='(模糊条件)分页获取所有角色',
summary='分页获取所有角色',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_roles(
async def get_roles_paged(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
name: Annotated[str | None, Query(description='角色名称')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
role_select = await role_service.get_select(name=name, status=status)
page_data = await paging_data(db, role_select)
@@ -97,7 +89,7 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel:
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)
if count > 0:
return response_base.success()
@@ -105,7 +97,7 @@ async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> Re
@router.put(
'/{pk}/menu',
'/{pk}/menus',
summary='更新角色菜单',
dependencies=[
Depends(RequestPermission('sys:role:menu:edit')),
@@ -113,26 +105,26 @@ async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> Re
],
)
async def update_role_menus(
request: Request, pk: Annotated[int, Path(...)], menu_ids: UpdateRoleMenuParam
pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
) -> ResponseModel:
count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids)
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put(
'/{pk}/rule',
summary='更新角色数据权限规则',
'/{pk}/scopes',
summary='更新角色数据范围',
dependencies=[
Depends(RequestPermission('sys:role:rule:edit')),
Depends(RequestPermission('sys:role:scope:edit')),
DependsRBAC,
],
)
async def update_role_rules(
request: Request, pk: Annotated[int, Path(...)], rule_ids: UpdateRoleRuleParam
async def update_role_scopes(
pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
) -> ResponseModel:
count = await role_service.update_role_rule(request=request, pk=pk, rule_ids=rule_ids)
count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -140,14 +132,14 @@ async def update_role_rules(
@router.delete(
'',
summary='批量删除角色',
summary='批量删除角色',
dependencies=[
Depends(RequestPermission('sys:role:del')),
DependsRBAC,
],
)
async def delete_role(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
count = await role_service.delete(request=request, pk=pk)
async def delete_roles(obj: DeleteRoleParam) -> ResponseModel:
count = await role_service.delete(obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, File, UploadFile
from backend.common.dataclasses import UploadUrl
from backend.common.enums import FileType
from backend.common.response.response_schema import ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.utils.file_ops import file_verify, upload_file
router = APIRouter()
@router.post('/image', summary='上传图片', dependencies=[DependsJwtAuth])
async def upload_image(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
file_verify(file, FileType.image)
filename = await upload_file(file)
return response_base.success(data={'url': f'/static/upload/{filename}'})
@router.post('/video', summary='上传视频', dependencies=[DependsJwtAuth])
async def upload_video(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
file_verify(file, FileType.video)
filename = await upload_file(file)
return response_base.success(data={'url': f'/static/upload/{filename}'})
+88 -88
View File
@@ -2,159 +2,159 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi import APIRouter, Body, Depends, Path, Query, Request
from backend.app.admin.schema.role import GetRoleDetail
from backend.app.admin.schema.user import (
AddUserParam,
AvatarParam,
GetCurrentUserInfoDetail,
GetUserInfoDetail,
RegisterUserParam,
GetCurrentUserInfoWithRelationDetail,
GetUserInfoWithRelationDetail,
ResetPasswordParam,
UpdateUserParam,
UpdateUserRoleParam,
)
from backend.app.admin.service.user_service import user_service
from backend.common.enums import UserPermissionType
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.post('/register', summary='注册用户')
async def register_user(obj: RegisterUserParam) -> ResponseModel:
await user_service.register(obj=obj)
return response_base.success()
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoDetail]:
await user_service.add(request=request, obj=obj)
current_user = await user_service.get_userinfo(username=obj.username)
data = GetUserInfoDetail(**select_as_dict(current_user))
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
data = request.user.model_dump()
return response_base.success(data=data)
@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth])
async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseModel:
count = await user_service.pwd_reset(request=request, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'})
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoDetail]:
data = GetCurrentUserInfoDetail(**request.user.model_dump())
@router.get('/{pk}', summary='获取用户信息', dependencies=[DependsJwtAuth])
async def get_userinfo(
pk: Annotated[int, Path(description='用户 ID')],
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
data = await user_service.get_userinfo(pk=pk)
return response_base.success(data=data)
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
async def get_user(username: Annotated[str, Path(...)]) -> ResponseSchemaModel[GetUserInfoDetail]:
current_user = await user_service.get_userinfo(username=username)
data = GetUserInfoDetail(**select_as_dict(current_user))
@router.get('/{pk}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
async def get_user_roles(pk: Annotated[int, Path(description='用户 ID')]) -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await user_service.get_roles(pk=pk)
return response_base.success(data=data)
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel:
count = await user_service.update(request=request, username=username, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put(
'/{username}/role',
summary='更新用户角色',
dependencies=[
Depends(RequestPermission('sys:user:role:edit')),
DependsRBAC,
],
)
async def update_user_role(
request: Request, username: Annotated[str, Path(...)], obj: UpdateUserRoleParam
) -> ResponseModel:
await user_service.update_roles(request=request, username=username, obj=obj)
return response_base.success()
@router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtAuth])
async def update_avatar(request: Request, username: Annotated[str, Path(...)], avatar: AvatarParam) -> ResponseModel:
count = await user_service.update_avatar(request=request, username=username, avatar=avatar)
if count > 0:
return response_base.success()
return response_base.fail()
@router.get(
'',
summary='(模糊条件)分页获取所有用户',
summary='分页获取所有用户',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_users(
async def get_users_paged(
db: CurrentSession,
dept: Annotated[int | None, Query()] = None,
username: Annotated[str | None, Query()] = None,
phone: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
) -> ResponseSchemaModel[PageData[GetUserInfoDetail]]:
dept: Annotated[int | None, Query(description='部门 ID')] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
phone: Annotated[str | None, Query(description='手机号')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status)
page_data = await paging_data(db, user_select)
return response_base.success(data=page_data)
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
async def super_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await user_service.update_permission(request=request, pk=pk)
@router.post('', summary='创建用户', dependencies=[DependsRBAC])
async def create_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
await user_service.create(request=request, obj=obj)
data = await user_service.get_userinfo(username=obj.username)
return response_base.success(data=data)
@router.put('/{pk}', summary='更新用户信息', dependencies=[DependsRBAC])
async def update_user(
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: UpdateUserParam
) -> ResponseModel:
count = await user_service.update(request=request, pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/{pk}/staff', summary='修改用户后台登录权限', dependencies=[DependsRBAC])
async def staff_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await user_service.update_staff(request=request, pk=pk)
@router.put('/{pk}/permissions', summary='更新用户权限', dependencies=[DependsRBAC])
async def update_user_permission(
request: Request,
pk: Annotated[int, Path(description='用户 ID')],
type: Annotated[UserPermissionType, Query(description='权限类型')],
) -> ResponseModel:
count = await user_service.update_permission(request=request, pk=pk, type=type)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsRBAC])
async def status_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await user_service.update_status(request=request, pk=pk)
@router.put('/me/password', summary='更新当前用户密码', dependencies=[DependsJwtAuth])
async def update_user_password(request: Request, obj: ResetPasswordParam) -> ResponseModel:
count = await user_service.update_password(request=request, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/{pk}/multi', summary='修改用户多点登录状态', dependencies=[DependsRBAC])
async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await user_service.update_multi_login(request=request, pk=pk)
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsRBAC])
async def reset_user_password(
request: Request,
pk: Annotated[int, Path(description='用户 ID')],
password: Annotated[str, Body(embed=True, description='新密码')],
) -> ResponseModel:
count = await user_service.reset_password(request=request, pk=pk, password=password)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/me/nickname', summary='更新当前用户昵称', dependencies=[DependsJwtAuth])
async def update_user_nickname(
request: Request, nickname: Annotated[str, Body(embed=True, description='用户昵称')]
) -> ResponseModel:
count = await user_service.update_nickname(request=request, nickname=nickname)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/me/avatar', summary='更新当前用户头像', dependencies=[DependsJwtAuth])
async def update_user_avatar(
request: Request, avatar: Annotated[str, Body(embed=True, description='用户头像地址')]
) -> ResponseModel:
count = await user_service.update_avatar(request=request, avatar=avatar)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/me/email', summary='更新当前用户邮箱', dependencies=[DependsJwtAuth])
async def update_user_email(
request: Request,
captcha: Annotated[str, Body(embed=True, description='邮箱验证码')],
email: Annotated[str, Body(embed=True, description='用户邮箱')],
) -> ResponseModel:
count = await user_service.update_email(request=request, captcha=captcha, email=email)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
path='/{username}',
summary='用户注销',
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
path='/{pk}',
summary='删除用户',
dependencies=[
Depends(RequestPermission('sys:user:del')),
DependsRBAC,
],
)
async def delete_user(username: Annotated[str, Path(...)]) -> ResponseModel:
count = await user_service.delete(username=username)
async def delete_user(pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
from backend.core.path_conf import BasePath
class AdminSettings(BaseSettings):
"""Admin Settings"""
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
# OAuth2https://github.com/fastapi-practices/fastapi_oauth20
# GitHub
OAUTH2_GITHUB_CLIENT_ID: str
OAUTH2_GITHUB_CLIENT_SECRET: str
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
# Linux Do
OAUTH2_LINUX_DO_CLIENT_ID: str
OAUTH2_LINUX_DO_CLIENT_SECRET: str
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
# Front-end redirect address
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
# Captcha
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒
# Config
CONFIG_BUILT_IN_TYPES: list = ['website', 'protocol', 'policy']
@lru_cache
def get_admin_settings() -> AdminSettings:
"""获取 admin 配置"""
return AdminSettings()
admin_settings = get_admin_settings()
-109
View File
@@ -1,109 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.conf import admin_settings
from backend.app.admin.model import Config
from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam
class CRUDConfig(CRUDPlus[Config]):
async def get(self, db: AsyncSession, pk: int) -> Config | None:
"""
获取系统参数配置
:param db:
:param pk:
:return:
"""
return await self.select_model_by_column(db, id=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES)
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
"""
通过 type 获取内置系统配置
:param db:
:param type:
:return:
"""
return await self.select_models(db, type=type)
async def get_by_key_and_type(self, db: AsyncSession, key: str, type: str) -> Config | None:
"""
通过 name 和 type 获取内置系统配置
:param db:
:param key:
:param type:
:return:
"""
return await self.select_model_by_column(db, key=key, type=type)
async def get_by_key(self, db: AsyncSession, key: str, built_in: bool = False) -> Config | None:
"""
通过 key 获取系统配置参数
:param db:
:param key:
:param built_in:
:return:
"""
filters = {'key': key}
if not built_in:
filters.update({'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES})
return await self.select_model_by_column(db, **filters)
async def get_list(self, name: str = None, type: str = None) -> Select:
"""
获取系统参数配置列表
:param name:
:param type:
:return:
"""
filters = {'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES}
if name is not None:
filters.update(name__like=f'%{name}%')
if type is not None:
filters.update(type__like=f'%{type}%')
return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj_in: CreateConfigParam) -> None:
"""
创建 Config
:param db:
:param obj_in:
:return:
"""
await self.create_model(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateConfigParam) -> int:
"""
更新 Config
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_model(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除 Config
:param db:
:param pk:
:return:
"""
return await self.delete_model_by_column(
db, allow_multiple=True, id__in=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES
)
config_dao: CRUDConfig = CRUDConfig(Config)
+36 -35
View File
@@ -2,9 +2,8 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select, desc, select
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule
@@ -12,79 +11,81 @@ from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRu
class CRUDDataRule(CRUDPlus[DataRule]):
"""数据规则数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
"""
获取数据权限规则
获取规则详情
:param db:
:param pk:
:param db: 数据库会话
:param pk: 规则 ID
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, name: str = None) -> Select:
async def get_list(self, name: str | None) -> Select:
"""
获取数据权限规则列表
获取规则列表
:param name: 规则名称
:return:
"""
stmt = select(self.model).options(noload(self.model.roles)).order_by(desc(self.model.created_time))
where_list = []
filters = {}
if name is not None:
where_list.append(self.model.name.like(f'%{name}%'))
if where_list:
stmt = stmt.where(*where_list)
return stmt
filters['name__like'] = f'%{name}%'
async def get_by_name(self, db: AsyncSession, name: str):
return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
"""
通过 name 获取数据权限规则
通过名称获取规则
:param db:
:param name:
:param db: 数据库会话
:param name: 规则名称
:return:
"""
return await self.select_model_by_column(db, name=name)
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
"""
获取所有数据权限规则
获取所有规则
:param db:
:param db: 数据库会话
:return:
"""
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 obj_in:
:param db: 数据库会话
:param obj: 创建规则参数
: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 pk:
:param obj_in:
:param db: 数据库会话
:param pk: 规则 ID
:param obj: 更新规则参数
:return:
"""
return await self.update_model(db, pk, obj_in)
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
"""
删除数据权限规则
批量删除规则
:param db:
:param pk:
:param db: 数据库会话
:param pks: 规则 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
data_rule_dao: CRUDDataRule = CRUDDataRule(DataRule)
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule, DataScope
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
class CRUDDataScope(CRUDPlus[DataScope]):
"""数据范围数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DataScope | None:
"""
获取数据范围详情
:param db: 数据库会话
:param pk: 范围 ID
:return:
"""
return await self.select_model(db, pk)
async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None:
"""
通过名称获取数据范围
:param db: 数据库会话
:param name: 范围名称
:return:
"""
return await self.select_model_by_column(db, name=name)
async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope:
"""
获取数据范围关联数据
:param db: 数据库会话
:param pk: 范围 ID
:return:
"""
return await self.select_model(db, pk, load_strategies=['rules'])
async def get_all(self, db: AsyncSession) -> Sequence[DataScope]:
"""
获取所有数据范围
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def get_list(self, name: str | None, status: int | None) -> Select:
"""
获取数据范围列表
:param name: 范围名称
:param status: 范围状态
:return:
"""
filters = {}
if name is not None:
filters['name__like'] = f'%{name}%'
if status is not None:
filters['status'] = status
return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters)
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
"""
创建数据范围
:param db: 数据库会话
:param obj: 创建数据范围参数
:return:
"""
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int:
"""
更新数据范围
:param db: 数据库会话
:param pk: 范围 ID
:param obj: 更新数据范围参数
:return:
"""
return await self.update_model(db, pk, obj)
async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
"""
更新数据范围规则
:param db: 数据库会话
:param pk: 范围 ID
:param rule_ids: 数据规则 ID 列表
:return:
"""
current_data_scope = await self.get_with_relation(db, pk)
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
rules = await db.execute(stmt)
current_data_scope.rules = rules.scalars().all()
return len(current_data_scope.rules)
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
"""
批量删除数据范围
:param db: 数据库会话
:param pks: 范围 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
data_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
+58 -52
View File
@@ -2,116 +2,122 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import select
from fastapi import Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
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.common.security.permission import filter_data_permission
class CRUDDept(CRUDPlus[Dept]):
"""部门数据库操作类"""
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
"""
获取部门
获取部门详情
:param db:
:param dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
return await self.select_model_by_column(db, id=dept_id, del_flag=0)
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
"""
通过 name 获取 API
通过名称获取部门
:param db:
:param name:
:param db: 数据库会话
:param name: 部门名称
:return:
"""
return await self.select_model_by_column(db, name=name, del_flag=0)
async def get_all(
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: int = None
self,
request: Request,
db: AsyncSession,
name: str | None,
leader: str | None,
phone: str | None,
status: int | None,
) -> Sequence[Dept]:
"""
获取所有部门
:param db:
:param name:
:param leader:
:param phone:
:param status:
:param request: FastAPI 请求对象
:param db: 数据库会话
:param name: 部门名称
:param leader: 负责人
:param phone: 联系电话
:param status: 部门状态
:return:
"""
filters = {'del_flag__eq': 0}
if name is not None:
filters.update(name__like=f'%{name}%')
if leader is not None:
filters.update(leader__like=f'%{leader}%')
if phone is not None:
filters.update(phone__startswith=phone)
if status is not None:
filters.update(status=status)
return await self.select_models_order(db, sort_columns='sort', **filters)
filters = {'del_flag': 0}
async def create(self, db: AsyncSession, obj_in: CreateDeptParam) -> None:
if name is not None:
filters['name__like'] = f'%{name}%'
if leader is not None:
filters['leader__like'] = f'%{leader}%'
if phone is not None:
filters['phone__startswith'] = phone
if status is not None:
filters['status'] = status
data_filtered = await filter_data_permission(db, request)
return await self.select_models_order(db, 'sort', 'desc', data_filtered, **filters)
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
"""
创建部门
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建部门参数
: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 dept_id:
:param obj_in:
:param db: 数据库会话
:param dept_id: 部门 ID
:param obj: 更新部门参数
: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:
"""
删除部门
:param db:
:param dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> list[User]:
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
"""
获取关联
获取部门及关联数据
:param db:
:param dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
result = await db.execute(stmt)
user_relation = result.scalars().first()
return user_relation.users
return await self.select_model(db, dept_id, load_strategies=['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 dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == dept_id)
result = await db.execute(stmt)
dept = result.scalars().first()
return dept.children
return await self.select_models(db, parent_id=dept_id, del_flag=0)
dept_dao: CRUDDept = CRUDDept(Dept)
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select, and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DictData
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
class CRUDDictData(CRUDPlus[DictData]):
async def get(self, db: AsyncSession, pk: int) -> DictData | None:
"""
获取字典数据
:param db:
:param pk:
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, label: str = None, value: str = None, status: int = None) -> Select:
"""
获取所有字典数据
:param label:
:param value:
:param status:
:return:
"""
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
where_list = []
if label is not None:
where_list.append(self.model.label.like(f'%{label}%'))
if value is not None:
where_list.append(self.model.value.like(f'%{value}%'))
if status is not None:
where_list.append(self.model.status == status)
if where_list:
stmt = stmt.where(and_(*where_list))
return stmt
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
"""
通过 label 获取字典数据
:param db:
:param label:
:return:
"""
return await self.select_model_by_column(db, label=label)
async def create(self, db: AsyncSession, obj_in: CreateDictDataParam) -> None:
"""
创建数据字典
:param db:
:param obj_in:
:return:
"""
await self.create_model(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictDataParam) -> int:
"""
更新数据字典
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_model(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除字典数据
:param db:
:param pk:
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
"""
获取字典数据和类型
:param db:
:param pk:
:return:
"""
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk)
dict_data = await db.execute(stmt)
return dict_data.scalars().first()
dict_data_dao: CRUDDictData = CRUDDictData(DictData)
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DictType
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
class CRUDDictType(CRUDPlus[DictType]):
async def get(self, db: AsyncSession, pk: int) -> DictType | None:
"""
获取字典类型
:param db:
:param pk:
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, *, name: str = None, code: str = None, status: int = None) -> Select:
"""
获取所有字典类型
:param name:
:param code:
:param status:
:return:
"""
filters = {}
if name is not None:
filters.update(name__like=f'%{name}%')
if code is not None:
filters.update(code__like=f'%{code}%')
if status is not None:
filters.update(status=status)
return await self.select_order('created_time', 'desc', **filters)
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
"""
通过 code 获取字典类型
:param db:
:param code:
:return:
"""
return await self.select_model_by_column(db, code=code)
async def create(self, db: AsyncSession, obj_in: CreateDictTypeParam) -> None:
"""
创建字典类型
:param db:
:param obj_in:
:return:
"""
await self.create_model(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictTypeParam) -> int:
"""
更新字典类型
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_model(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除字典类型
:param db:
:param pk:
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
dict_type_dao: CRUDDictType = CRUDDictType(DictType)
+26 -20
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy import delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
@@ -9,52 +10,57 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
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, status: int | None, ip: str | None) -> Select:
"""
获取登录日志列表
:param username:
:param status:
:param ip:
:param username: 用户名
:param status: 登录状态
:param ip: IP 地址
:return:
"""
filters = {}
if username is not None:
filters.update(username__like=f'%{username}%')
filters['username__like'] = f'%{username}%'
if status is not None:
filters.update(status=status)
filters['status'] = status
if ip is not None:
filters.update(ip__like=f'%{ip}%')
filters['ip__like'] = f'%{ip}%'
return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam) -> None:
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
"""
创建登录日志
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建登录日志参数
:return:
"""
await self.create_model(db, obj_in, commit=True)
await self.create_model(db, obj, commit=True)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
"""
删除登录日志
批量删除登录日志
:param db:
:param pk:
:param db: 数据库会话
:param pks: 登录日志 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
async def delete_all(self, db: AsyncSession) -> int:
@staticmethod
async def delete_all(db: AsyncSession) -> None:
"""
删除所有登录日志
删除所有日志
:param db:
:param db: 数据库会话
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True)
await db.execute(sa_delete(LoginLog))
login_log_dao: CRUDLoginLog = CRUDLoginLog(LoginLog)
+49 -50
View File
@@ -2,8 +2,7 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import and_, asc, select
from sqlalchemy.orm import selectinload
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Menu
@@ -11,101 +10,101 @@ from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
class CRUDMenu(CRUDPlus[Menu]):
async def get(self, db, menu_id: int) -> Menu | None:
"""
获取菜单
"""菜单数据库操作类"""
:param db:
:param menu_id:
async def get(self, db: AsyncSession, menu_id: int) -> Menu | None:
"""
获取菜单详情
:param db: 数据库会话
:param menu_id: 菜单 ID
:return:
"""
return await self.select_model(db, menu_id)
async def get_by_title(self, db, title: str) -> Menu | None:
async def get_by_title(self, db: AsyncSession, title: str) -> Menu | None:
"""
通过 title 获取菜单
通过标题获取菜单
:param db:
:param title:
:param db: 数据库会话
:param title: 菜单标题
:return:
"""
return await self.select_model_by_column(db, title=title, menu_type__ne=2)
return await self.select_model_by_column(db, title=title, type__ne=2)
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
"""
获取所有菜单
获取菜单列表
:param db:
:param title:
:param status:
:param db: 数据库会话
:param title: 菜单标题
:param status: 菜单状态
:return:
"""
filters = {}
if title is not None:
filters.update(title=f'%{title}%')
filters['title__like'] = f'%{title}%'
if status is not None:
filters.update(status=status)
filters['status'] = status
return await self.select_models_order(db, 'sort', **filters)
async def get_role_menus(self, db, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
async def get_sidebar(self, db: AsyncSession, menu_ids: list[int] | None) -> Sequence[Menu]:
"""
获取角色菜单
获取用户的菜单侧边栏
:param db:
:param superuser:
:param menu_ids:
:param db: 数据库会话
:param menu_ids: 菜单 ID 列表
:return:
"""
stmt = select(self.model).order_by(asc(self.model.sort))
where_list = [self.model.menu_type.in_([0, 1])]
if not superuser:
where_list.append(self.model.id.in_(menu_ids))
stmt = stmt.where(and_(*where_list))
menu = await db.execute(stmt)
return menu.scalars().all()
filters = {'type__in': [0, 1, 3, 4]}
async def create(self, db, obj_in: CreateMenuParam) -> None:
if menu_ids:
filters['id__in'] = menu_ids
return await self.select_models_order(db, 'sort', 'asc', **filters)
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
"""
创建菜单
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建菜单参数
: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 menu_id:
:param obj_in:
:param db: 数据库会话
:param menu_id: 菜单 ID
:param obj: 更新菜单参数
: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 menu_id:
:param db: 数据库会话
:param menu_id: 菜单 ID
:return:
"""
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 menu_id:
:param db: 数据库会话
:param menu_id: 菜单 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
result = await db.execute(stmt)
menu = result.scalars().first()
menu = await self.select_model(db, menu_id, load_strategies=['children'])
return menu.children
+36 -20
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy import delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
@@ -9,52 +10,67 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
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, status: int | None, ip: str | None) -> Select:
"""
获取操作日志列表
:param username:
:param status:
:param ip:
:param username: 用户名
:param status: 操作状态
:param ip: IP 地址
:return:
"""
filters = {}
if username is not None:
filters.update(username=f'%{username}%')
filters['username__like'] = f'%{username}%'
if status is not None:
filters.update(status=status)
filters['status__eq'] = status
if ip is not None:
filters.update(ip=f'%{ip}%')
filters['ip__like'] = f'%{ip}%'
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 obj_in:
:param db: 数据库会话
:param obj: 操作日志创建参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
async def bulk_create(self, db: AsyncSession, objs: list[CreateOperaLogParam]) -> None:
"""
删除操作日志
批量创建操作日志
:param db:
:param pk:
:param db: 数据库会话
:param objs: 操作日志创建参数列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
await self.create_models(db, objs)
async def delete_all(self, db: AsyncSession) -> int:
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
"""
删除所有操作日志
批量删除操作日志
:param db:
:param db: 数据库会话
:param pks: 操作日志 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True)
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
@staticmethod
async def delete_all(db: AsyncSession) -> None:
"""
删除所有日志
:param db: 数据库会话
:return:
"""
await db.execute(sa_delete(OperaLog))
opera_log_dao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog)
+68 -82
View File
@@ -2,161 +2,147 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select, desc, select
from sqlalchemy.orm import noload, selectinload
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule, Menu, Role, User
from backend.app.admin.model import DataScope, Menu, Role
from backend.app.admin.schema.role import (
CreateRoleParam,
UpdateRoleMenuParam,
UpdateRoleParam,
UpdateRoleRuleParam,
UpdateRoleScopeParam,
)
class CRUDRole(CRUDPlus[Role]):
async def get(self, db, role_id: int) -> Role | None:
"""
获取角色
"""角色数据库操作类"""
:param db:
:param role_id:
async def get(self, db: AsyncSession, role_id: int) -> Role | None:
"""
获取角色详情
:param db: 数据库会话
:param role_id: 角色 ID
:return:
"""
return await self.select_model(db, role_id)
async def get_with_relation(self, db, role_id: int) -> Role | None:
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
"""
获取角色和菜单
获取角色及关联数据
:param db:
:param role_id:
:param db: 数据库会话
:param role_id: 角色 ID
:return:
"""
stmt = (
select(self.model)
.options(selectinload(self.model.menus), selectinload(self.model.rules))
.where(self.model.id == role_id)
)
role = await db.execute(stmt)
return role.scalars().first()
return await self.select_model(db, role_id, load_strategies=['menus', 'scopes'])
async def get_all(self, db) -> Sequence[Role]:
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
"""
获取所有角色
:param db:
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def get_by_user(self, db, user_id: int) -> Sequence[Role]:
"""
获取用户所有角色
:param db:
:param user_id:
:return:
"""
stmt = select(self.model).join(self.model.users).where(User.id == user_id)
roles = await db.execute(stmt)
return roles.scalars().all()
async def get_list(self, name: str = None, status: int = None) -> Select:
async def get_list(self, name: str | None, status: int | None) -> Select:
"""
获取角色列表
:param name:
:param status:
:param name: 角色名称
:param status: 角色状态
:return:
"""
stmt = (
select(self.model)
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.rules))
.order_by(desc(self.model.created_time))
)
where_list = []
if name:
where_list.append(self.model.name.like(f'%{name}%'))
filters = {}
if name is not None:
filters['name__like'] = f'%{name}%'
if status is not None:
where_list.append(self.model.status == status)
if where_list:
stmt = stmt.where(*where_list)
return stmt
filters['status'] = status
async def get_by_name(self, db, name: str) -> Role | None:
return await self.select_order(
'id',
load_strategies={
'users': 'noload',
'menus': 'noload',
'scopes': 'noload',
},
**filters,
)
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
"""
通过 name 获取角色
通过名称获取角色
:param db:
:param name:
:param db: 数据库会话
:param name: 角色名称
:return:
"""
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 obj_in:
:param db: 数据库会话
:param obj: 创建角色参数
: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 role_id:
:param obj_in:
:param db: 数据库会话
:param role_id: 角色 ID
:param obj: 更新角色参数
: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 role_id:
:param menu_ids:
:param db: 数据库会话
:param role_id: 角色 ID
:param menu_ids: 菜单 ID 列表
:return:
"""
current_role = await self.get_with_relation(db, role_id)
# 更新菜单
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
menus = await db.execute(stmt)
current_role.menus = menus.scalars().all()
return len(current_role.menus)
async def update_rules(self, db, role_id: int, rule_ids: UpdateRoleRuleParam) -> int:
async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
"""
更新角色数据权限
更新角色数据范围
:param db:
:param role_id:
:param rule_ids:
:param db: 数据库会话
:param role_id: 角色 ID
:param scope_ids: 权限范围 ID 列表
:return:
"""
current_role = await self.get_with_relation(db, role_id)
# 更新数据权限
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
rules = await db.execute(stmt)
current_role.rules = rules.scalars().all()
return len(current_role.rules)
stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
scopes = await db.execute(stmt)
current_role.scopes = scopes.scalars().all()
return len(current_role.scopes)
async def delete(self, db, role_id: list[int]) -> int:
async def delete(self, db: AsyncSession, role_ids: list[int]) -> int:
"""
删除角色
批量删除角色
:param db:
:param role_id:
:param db: 数据库会话
:param role_ids: 角色 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_id)
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_ids)
role_dao: CRUDRole = CRUDRole(Role)
+147 -180
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
import bcrypt
from sqlalchemy import and_, desc, select
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload
from sqlalchemy.sql import Select
@@ -10,83 +10,63 @@ from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Dept, Role, User
from backend.app.admin.schema.user import (
AddOAuth2UserParam,
AddUserParam,
AvatarParam,
RegisterUserParam,
UpdateUserParam,
UpdateUserRoleParam,
)
from backend.common.security.jwt import get_hash_password
from backend.utils.timezone import timezone
class CRUDUser(CRUDPlus[User]):
"""用户数据库操作类"""
async def get(self, db: AsyncSession, user_id: int) -> User | None:
"""
获取用户
获取用户详情
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
return await self.select_model(db, user_id)
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
"""
通过 username 获取用户
通过用户名获取用户
:param db:
:param username:
:param db: 数据库会话
:param username: 用户名
:return:
"""
return await self.select_model_by_column(db, username=username)
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
"""
通过 nickname 获取用户
通过昵称获取用户
:param db:
:param nickname:
:param db: 数据库会话
:param nickname: 用户昵称
:return:
"""
return await self.select_model_by_column(db, nickname=nickname)
async def update_login_time(self, db: AsyncSession, username: str) -> int:
"""
更新用户登录时间
更新用户最后登录时间
:param db:
:param username:
:param db: 数据库会话
:param username: 用户名
:return:
"""
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool = False) -> None:
"""
创建用户
:param db:
:param obj:
:param social: 社交用户适配 oauth 2.0
:return:
"""
if not social:
salt = bcrypt.gensalt()
obj.password = get_hash_password(obj.password, salt)
dict_obj = obj.model_dump()
dict_obj.update({'is_staff': True, 'salt': salt})
else:
dict_obj = obj.model_dump()
dict_obj.update({'is_staff': True, 'salt': None})
new_user = self.model(**dict_obj)
db.add(new_user)
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
"""
后台添加用户
添加用户
:param db:
:param obj:
:param db: 数据库会话
:param obj: 添加用户参数
:return:
"""
salt = bcrypt.gensalt()
@@ -94,227 +74,214 @@ class CRUDUser(CRUDPlus[User]):
dict_obj = obj.model_dump(exclude={'roles'})
dict_obj.update({'salt': salt})
new_user = self.model(**dict_obj)
role_list = []
for role_id in obj.roles:
role_list.append(await db.get(Role, role_id))
new_user.roles.extend(role_list)
stmt = select(Role).where(Role.id.in_(obj.roles))
roles = await db.execute(stmt)
new_user.roles = roles.scalars().all()
db.add(new_user)
async def update_userinfo(self, db: AsyncSession, input_user: int, obj: UpdateUserParam) -> int:
async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None:
"""
通过 OAuth2 添加用户
:param db: 数据库会话
:param obj: 注册用户参数
:return:
"""
dict_obj = obj.model_dump()
dict_obj.update({'is_staff': True, 'salt': None})
new_user = self.model(**dict_obj)
stmt = select(Role)
role = await db.execute(stmt)
new_user.roles = [role.scalars().first()] # 默认绑定第一个角色
db.add(new_user)
async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int:
"""
更新用户信息
:param db:
:param input_user:
:param obj:
:param db: 数据库会话
:param input_user: 用户 ID
:param obj: 更新用户参数
:return:
"""
return await self.update_model(db, input_user, obj)
role_ids = obj.roles
del obj.roles
count = await self.update_model(db, input_user.id, obj)
@staticmethod
async def update_role(db: AsyncSession, input_user: User, obj: UpdateUserRoleParam) -> None:
stmt = select(Role).where(Role.id.in_(role_ids))
roles = await db.execute(stmt)
input_user.roles = roles.scalars().all()
return count
async def update_nickname(self, db: AsyncSession, user_id: int, nickname: str) -> int:
"""
更新用户角色
更新用户昵称
:param db:
:param input_user:
:param obj:
:param db: 数据库会话
:param user_id: 用户 ID
:param nickname: 用户昵称
:return:
"""
# 删除用户所有角色
for i in list(input_user.roles):
input_user.roles.remove(i)
# 添加用户角色
role_list = []
for role_id in obj.roles:
role_list.append(await db.get(Role, role_id))
input_user.roles.extend(role_list)
return await self.update_model(db, user_id, {'nickname': nickname})
async def update_avatar(self, db: AsyncSession, input_user: int, avatar: AvatarParam) -> int:
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
"""
更新用户头像
:param db:
:param input_user:
:param avatar:
:param db: 数据库会话
:param user_id: 用户 ID
:param avatar: 头像地址
:return:
"""
return await self.update_model(db, input_user, {'avatar': avatar.url})
return await self.update_model(db, user_id, {'avatar': avatar})
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
"""
更新用户邮箱
:param db: 数据库会话
:param user_id: 用户 ID
:param email: 邮箱
:return:
"""
return await self.update_model(db, user_id, {'email': email})
async def delete(self, db: AsyncSession, user_id: int) -> int:
"""
删除用户
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
return await self.delete_model(db, user_id)
async def check_email(self, db: AsyncSession, email: str) -> User | None:
"""
检查邮箱是否存在
检查邮箱是否已被绑定
:param db:
:param email:
:param db: 数据库会话
:param email: 电子邮箱
:return:
"""
return await self.select_model_by_column(db, email=email)
async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int:
async def reset_password(self, db: AsyncSession, pk: int, password: str) -> int:
"""
重置用户密码
:param db:
:param pk:
:param new_pwd:
:param db: 数据库会话
:param pk: 用户 ID
:param password: 新密码
:return:
"""
return await self.update_model(db, pk, {'password': new_pwd})
salt = bcrypt.gensalt()
new_pwd = get_hash_password(password, salt)
return await self.update_model(db, pk, {'password': new_pwd, 'salt': salt})
async def get_list(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select:
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
"""
获取用户列表
:param dept:
:param username:
:param phone:
:param status:
:param dept: 部门 ID
:param username: 用户名
:param phone: 电话号码
:param status: 用户状态
:return:
"""
stmt = (
select(self.model)
.options(
selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)),
noload(self.model.socials),
selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.rules)),
)
.order_by(desc(self.model.join_time))
)
where_list = []
filters = {}
if dept:
where_list.append(self.model.dept_id == dept)
filters['dept_id'] = dept
if username:
where_list.append(self.model.username.like(f'%{username}%'))
filters['username__like'] = f'%{username}%'
if phone:
where_list.append(self.model.phone.like(f'%{phone}%'))
filters['phone_like'] = f'%{phone}%'
if status is not None:
where_list.append(self.model.status == status)
if where_list:
stmt = stmt.where(and_(*where_list))
return stmt
filters['status'] = status
async def get_super(self, db: AsyncSession, user_id: int) -> bool:
return await self.select_order(
'id',
'desc',
load_options=[
selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)),
selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)),
],
**filters,
)
async def set_super(self, db: AsyncSession, user_id: int, is_super: bool) -> int:
"""
获取用户超级管理员状态
设置用户超级管理员状态
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:param is_super: 是否超级管理员
:return:
"""
user = await self.get(db, user_id)
return user.is_superuser
return await self.update_model(db, user_id, {'is_superuser': is_super})
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
async def set_staff(self, db: AsyncSession, user_id: int, is_staff: bool) -> int:
"""
获取用户后台登录状态
设置用户后台登录状态
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:param is_staff: 是否可登录后台
:return:
"""
user = await self.get(db, user_id)
return user.is_staff
return await self.update_model(db, user_id, {'is_staff': is_staff})
async def get_status(self, db: AsyncSession, user_id: int) -> int:
"""
获取用户状态
:param db:
:param user_id:
:return:
"""
user = await self.get(db, user_id)
return user.status
async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户多点登录状态
:param db:
:param user_id:
:return:
"""
user = await self.get(db, user_id)
return user.is_multi_login
async def set_super(self, db: AsyncSession, user_id: int, _super: bool) -> int:
"""
设置用户超级管理员
:param db:
:param user_id:
:param _super:
:return:
"""
return await self.update_model(db, user_id, {'is_superuser': _super})
async def set_staff(self, db: AsyncSession, user_id: int, staff: bool) -> int:
"""
设置用户后台登录
:param db:
:param user_id:
:param staff:
:return:
"""
return await self.update_model(db, user_id, {'is_staff': staff})
async def set_status(self, db: AsyncSession, user_id: int, status: bool) -> int:
async def set_status(self, db: AsyncSession, user_id: int, status: int) -> int:
"""
设置用户状态
:param db:
:param user_id:
:param status:
:param db: 数据库会话
:param user_id: 用户 ID
:param status: 状态
:return:
"""
return await self.update_model(db, user_id, {'status': status})
async def set_multi_login(self, db: AsyncSession, user_id: int, multi_login: bool) -> int:
"""
设置用户多登录
设置用户多登录状态
:param db:
:param user_id:
:param multi_login:
:param db: 数据库会话
:param user_id: 用户 ID
:param multi_login: 是否允许多端登录
:return:
"""
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
async def get_with_relation(self, db: AsyncSession, *, user_id: int = None, 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 user_id:
:param username:
:param db: 数据库会话
:param user_id: 用户 ID
:param username: 用户名
:return:
"""
stmt = select(self.model).options(
selectinload(self.model.dept),
selectinload(self.model.roles).options(
selectinload(Role.menus),
selectinload(Role.rules),
),
)
filters = []
filters = {}
if user_id:
filters.append(self.model.id == user_id)
filters['id'] = user_id
if username:
filters.append(self.model.username == username)
user = await db.execute(stmt.where(*filters))
return user.scalars().first()
filters['username'] = username
return await self.select_model_by_column(
db,
load_options=[selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes))],
load_strategies=['dept'],
**filters,
)
user_dao: CRUDUser = CRUDUser(User)
@@ -1,44 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import UserSocial
from backend.app.admin.schema.user_social import CreateUserSocialParam
from backend.common.enums import UserSocialType
class CRUDOUserSocial(CRUDPlus[UserSocial]):
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
"""
获取用户社交账号绑定
:param db:
:param pk:
:param source:
:return:
"""
return await self.select_model_by_column(db, user_id=pk, source=source)
async def create(self, db: AsyncSession, obj_in: CreateUserSocialParam) -> None:
"""
创建用户社交账号绑定
:param db:
:param obj_in:
:return:
"""
await self.create_model(db, obj_in)
async def delete(self, db: AsyncSession, social_id: int) -> int:
"""
删除用户社交账号绑定
:param db:
:param social_id:
:return:
"""
return await self.delete_model(db, social_id)
user_social_dao: CRUDOUserSocial = CRUDOUserSocial(UserSocial)
+1 -4
View File
@@ -1,13 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.admin.model.config import Config
from backend.app.admin.model.data_rule import DataRule
from backend.app.admin.model.data_scope import DataScope
from backend.app.admin.model.dept import Dept
from backend.app.admin.model.dict_data import DictData
from backend.app.admin.model.dict_type import DictType
from backend.app.admin.model.login_log import LoginLog
from backend.app.admin.model.menu import Menu
from backend.app.admin.model.opera_log import OperaLog
from backend.app.admin.model.role import Role
from backend.app.admin.model.user import User
from backend.app.admin.model.user_social import UserSocial
+14 -7
View File
@@ -1,26 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_rule
from backend.app.admin.model.m2m import sys_data_scope_rule
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DataScope
class DataRule(Base):
"""数据权限规则表"""
"""数据规则表"""
__tablename__ = 'sys_data_rule'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(255), unique=True, comment='规则名称')
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型')
column: Mapped[str] = mapped_column(String(20), comment='数据库字段')
name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
column: Mapped[str] = mapped_column(String(20), comment='模型字段')
operator: Mapped[int] = mapped_column(comment='运算符(0and、1or')
expression: Mapped[int] = mapped_column(
comment='表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in'
)
value: Mapped[str] = mapped_column(String(255), comment='规则值')
# 角色规则多对多
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules') # noqa: F821
# 数据范围规则多对多
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules')
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_data_scope_rule, sys_role_data_scope
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DataRule, Role
class DataScope(Base):
"""数据范围表"""
__tablename__ = 'sys_data_scope'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
# 数据范围规则多对多
rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='scopes')
# 角色数据范围多对多
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes')
+11 -6
View File
@@ -1,13 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from __future__ import annotations
from sqlalchemy import Boolean, ForeignKey, String
from typing import TYPE_CHECKING, Optional
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
from sqlalchemy.dialects.postgresql import INTEGER
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import User
class Dept(Base):
"""部门表"""
@@ -27,10 +32,10 @@ class Dept(Base):
# 父级部门一对多
parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
BigInteger, ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
)
parent: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list['Dept'] | None] = relationship(init=False, back_populates='parent')
parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
# 部门用户一对多
users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
+35 -15
View File
@@ -1,35 +1,55 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
from sqlalchemy import BigInteger, Column, ForeignKey, Table
from backend.common.model import MappedBase
sys_user_role = Table(
'sys_user_role',
MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('user_id', BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
)
sys_role_menu = Table(
'sys_role_menu',
MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('menu_id', BigInteger, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
)
sys_role_data_rule = Table(
'sys_role_data_rule',
sys_role_data_scope = Table(
'sys_role_data_scope',
MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
Column(
'data_rule_id',
Integer,
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
'data_scope_id',
BigInteger,
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True,
comment='数据权限规则ID',
comment='数据范围 ID',
),
)
sys_data_scope_rule = Table(
'sys_data_scope_rule',
MappedBase.metadata,
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column(
'data_scope_id',
BigInteger,
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True,
comment='数据范围 ID',
),
Column(
'data_rule_id',
BigInteger,
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
primary_key=True,
comment='数据规则 ID',
),
)
+16 -8
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from __future__ import annotations
from sqlalchemy import ForeignKey, String
from typing import TYPE_CHECKING, Optional
from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -10,6 +12,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_menu
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import Role
class Menu(Base):
"""菜单表"""
@@ -19,25 +24,28 @@ class Menu(Base):
id: Mapped[id_key] = mapped_column(init=False)
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
path: Mapped[str | None] = mapped_column(String(200), comment='路由地址')
sort: Mapped[int] = mapped_column(default=0, comment='排序')
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址')
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
link: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='外链地址'
)
remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
# 父级菜单一对多
parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
BigInteger, ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
)
parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent')
parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
# 菜单角色多对多
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_menu, back_populates='menus') # 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 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 backend.common.model import DataClassBase, id_key
+17 -7
View File
@@ -1,13 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import String
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.dialects.postgresql import INTEGER, TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_rule, sys_role_menu, sys_user_role
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DataScope, Menu, User
class Role(Base):
"""角色表"""
@@ -17,15 +24,18 @@ class Role(Base):
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
is_filter_scopes: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='过滤数据权限(0否 1是)'
)
remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
# 角色用户多对多
users: Mapped[list['User']] = relationship(init=False, secondary=sys_user_role, back_populates='roles') # noqa: F821
users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles')
# 角色菜单多对多
menus: Mapped[list['Menu']] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') # noqa: F821
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
# 角色数据权限规则多对多
rules: Mapped[list['DataRule']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles') # noqa: F821
# 角色数据范围多对多
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='roles')
+13 -11
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from datetime import datetime
from typing import Union
from typing import TYPE_CHECKING
from sqlalchemy import VARBINARY, Boolean, DateTime, ForeignKey, String
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.utils.timezone import timezone
if TYPE_CHECKING:
from backend.app.admin.model import Dept, Role
class User(Base):
"""用户表"""
@@ -21,22 +26,22 @@ class User(Base):
id: Mapped[id_key] = mapped_column(init=False)
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称')
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱')
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
is_superuser: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
)
is_staff: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
)
status: Mapped[int] = mapped_column(default=1, comment='用户账号状态(0停用 1正常)')
is_multi_login: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
)
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
join_time: Mapped[datetime] = mapped_column(
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='注册时间'
)
@@ -48,10 +53,7 @@ class User(Base):
dept_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
)
dept: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='users') # noqa: F821
# 用户社交信息一对多
socials: Mapped[list['UserSocial']] = relationship(init=False, back_populates='user') # noqa: F821
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
# 用户角色多对多
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')
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
class UserSocial(Base):
"""用户社交表(OAuth2"""
__tablename__ = 'sys_user_social'
id: Mapped[id_key] = mapped_column(init=False)
source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
open_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 open id')
uid: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 ID')
union_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 union id')
scope: Mapped[str | None] = mapped_column(String(120), default=None, comment='第三方用户授予的权限')
code: Mapped[str | None] = mapped_column(String(50), default=None, comment='用户的授权 code')
# 用户社交信息一对多
user_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
)
user: Mapped[Union['User', None]] = relationship(init=False, back_populates='socials') # noqa: F821
+2
View File
@@ -6,5 +6,7 @@ from backend.common.schema import SchemaBase
class GetCaptchaDetail(SchemaBase):
"""验证码详情"""
image_type: str = Field(description='图片类型')
image: str = Field(description='图片内容')
-38
View File
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict
from backend.common.schema import SchemaBase
class SaveBuiltInConfigParam(SchemaBase):
name: str
key: str
value: str
class ConfigSchemaBase(SchemaBase):
name: str
type: str | None
key: str
value: str
is_frontend: bool
remark: str | None
class CreateConfigParam(ConfigSchemaBase):
pass
class UpdateConfigParam(ConfigSchemaBase):
pass
class GetConfigDetail(ConfigSchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
+27 -13
View File
@@ -9,28 +9,42 @@ from backend.common.schema import SchemaBase
class DataRuleSchemaBase(SchemaBase):
name: str
model: str
column: str
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR)
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq)
value: str
"""数据规则基础模型"""
name: str = Field(description='规则名称')
model: str = Field(description='模型名称')
column: str = Field(description='字段名称')
operator: RoleDataRuleOperatorType = Field(description='操作符(AND/OR')
expression: RoleDataRuleExpressionType = Field(description='表达式类型')
value: str = Field(description='规则值')
class CreateDataRuleParam(DataRuleSchemaBase):
pass
"""创建数据规则参数"""
class UpdateDataRuleParam(DataRuleSchemaBase):
pass
"""更新数据规则参数"""
class DeleteDataRuleParam(SchemaBase):
"""删除数据规则参数"""
pks: list[int] = Field(description='规则 ID 列表')
class GetDataRuleDetail(DataRuleSchemaBase):
"""数据规则详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='规则 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
def __hash__(self):
return hash(self.name)
class GetDataRuleColumnDetail(SchemaBase):
"""数据规则可用模型字段详情"""
key: str = Field(description='字段名')
comment: str = Field(description='字段评论')
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.admin.schema.data_rule import GetDataRuleDetail
from backend.common.enums import StatusType
from backend.common.schema import SchemaBase
class DataScopeBase(SchemaBase):
"""数据范围基础模型"""
name: str = Field(description='名称')
status: StatusType = Field(description='状态')
class CreateDataScopeParam(DataScopeBase):
"""创建数据范围参数"""
class UpdateDataScopeParam(DataScopeBase):
"""更新数据范围参数"""
class UpdateDataScopeRuleParam(SchemaBase):
"""更新数据范围规则参数"""
rules: list[int] = Field(description='数据规则 ID 列表')
class DeleteDataScopeParam(SchemaBase):
"""删除数据范围参数"""
pks: list[int] = Field(description='数据范围 ID 列表')
class GetDataScopeDetail(DataScopeBase):
"""数据范围详情"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='数据范围 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
class GetDataScopeWithRelationDetail(GetDataScopeDetail):
"""数据范围关联详情"""
rules: list[GetDataRuleDetail] = Field([], description='数据规则列表')
+17 -13
View File
@@ -9,27 +9,31 @@ from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
class DeptSchemaBase(SchemaBase):
name: str
parent_id: int | None = Field(default=None, description='部门父级ID')
sort: int = Field(default=0, ge=0, description='排序')
leader: str | None = None
phone: CustomPhoneNumber | None = None
email: CustomEmailStr | None = None
status: StatusType = Field(default=StatusType.enable)
"""部门基础模型"""
name: str = Field(description='部门名称')
parent_id: int | None = Field(None, description='部门父级 ID')
sort: int = Field(0, ge=0, description='排序')
leader: str | None = Field(None, description='负责人')
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
email: CustomEmailStr | None = Field(None, description='邮箱')
status: StatusType = Field(description='状态')
class CreateDeptParam(DeptSchemaBase):
pass
"""创建部门参数"""
class UpdateDeptParam(DeptSchemaBase):
pass
"""更新部门参数"""
class GetDeptDetail(DeptSchemaBase):
"""部门详情"""
model_config = ConfigDict(from_attributes=True)
id: int
del_flag: bool
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='部门 ID')
del_flag: bool = Field(description='是否删除')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.admin.schema.dict_type import GetDictTypeDetail
from backend.common.enums import StatusType
from backend.common.schema import SchemaBase
class DictDataSchemaBase(SchemaBase):
type_id: int
label: str
value: str
sort: int
status: StatusType = Field(default=StatusType.enable)
remark: str | None = None
class CreateDictDataParam(DictDataSchemaBase):
pass
class UpdateDictDataParam(DictDataSchemaBase):
pass
class GetDictDataDetail(DictDataSchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int
type: GetDictTypeDetail | None = None
created_time: datetime
updated_time: datetime | None = None
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
from backend.common.enums import StatusType
from backend.common.schema import SchemaBase
class DictTypeSchemaBase(SchemaBase):
name: str
code: str
status: StatusType = Field(default=StatusType.enable)
remark: str | None = None
class CreateDictTypeParam(DictTypeSchemaBase):
pass
class UpdateDictTypeParam(DictTypeSchemaBase):
pass
class GetDictTypeDetail(DictTypeSchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
+28 -18
View File
@@ -2,37 +2,47 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict
from pydantic import ConfigDict, Field
from backend.common.schema import SchemaBase
class LoginLogSchemaBase(SchemaBase):
user_uuid: str
username: str
status: int
ip: str
country: str | None
region: str | None
city: str | None
user_agent: str
browser: str | None
os: str | None
device: str | None
msg: str
login_time: datetime
"""登录日志基础模型"""
user_uuid: str = Field(description='用户 UUID')
username: str = Field(description='用户名')
status: int = Field(description='登录状态')
ip: str = Field(description='IP 地址')
country: str | None = Field(None, description='国家')
region: str | None = Field(None, description='地区')
city: str | None = Field(None, description='城市')
user_agent: str = Field(description='用户代理')
browser: str | None = Field(None, description='浏览器')
os: str | None = Field(None, description='操作系统')
device: str | None = Field(None, description='设备')
msg: str = Field(description='消息')
login_time: datetime = Field(description='登录时间')
class CreateLoginLogParam(LoginLogSchemaBase):
pass
"""创建登录日志参数"""
class UpdateLoginLogParam(LoginLogSchemaBase):
pass
"""更新登录日志参数"""
class DeleteLoginLogParam(SchemaBase):
"""删除登录日志参数"""
pks: list[int] = Field(description='登录日志 ID 列表')
class GetLoginLogDetail(LoginLogSchemaBase):
"""登录日志详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
id: int = Field(description='日志 ID')
created_time: datetime = Field(description='创建时间')
+23 -18
View File
@@ -9,32 +9,37 @@ from backend.common.schema import SchemaBase
class MenuSchemaBase(SchemaBase):
title: str
name: str
parent_id: int | None = Field(default=None, description='菜单父级ID')
sort: int = Field(default=0, ge=0, description='排序')
icon: str | None = None
path: str | None = None
menu_type: MenuType = Field(default=MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = None
perms: str | None = None
status: StatusType = Field(default=StatusType.enable)
display: StatusType = Field(default=StatusType.enable)
cache: StatusType = Field(default=StatusType.enable)
remark: str | None = None
"""菜单基础模型"""
title: str = Field(description='菜单标题')
name: str = Field(description='菜单名称')
path: str | None = Field(None, description='路由地址')
parent_id: int | None = Field(None, description='菜单父级 ID')
sort: int = Field(0, ge=0, description='排序')
icon: str | None = Field(None, description='图标')
type: MenuType = Field(description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
component: str | None = Field(None, description='组件路径')
perms: str | None = Field(None, description='权限标识')
status: StatusType = Field(description='状态')
display: StatusType = Field(description='是否显示')
cache: StatusType = Field(description='是否缓存')
link: str | None = Field(None, description='外链地址')
remark: str | None = Field(None, description='备注')
class CreateMenuParam(MenuSchemaBase):
pass
"""创建菜单参数"""
class UpdateMenuParam(MenuSchemaBase):
pass
"""更新菜单参数"""
class GetMenuDetail(MenuSchemaBase):
"""菜单详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='菜单 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
+34 -23
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Any
from pydantic import ConfigDict, Field
@@ -9,37 +10,47 @@ from backend.common.schema import SchemaBase
class OperaLogSchemaBase(SchemaBase):
trace_id: str
username: str | None = None
method: str
title: str
path: str
ip: str
country: str | None = None
region: str | None = None
city: str | None = None
user_agent: str
os: str | None = None
browser: str | None = None
device: str | None = None
args: dict | None = None
status: StatusType = Field(default=StatusType.enable)
code: str
msg: str | None = None
cost_time: float
opera_time: datetime
"""操作日志基础模型"""
trace_id: str = Field(description='追踪 ID')
username: str | None = Field(None, description='用户名')
method: str = Field(description='请求方法')
title: str = Field(description='操作标题')
path: str = Field(description='请求路径')
ip: str = Field(description='IP 地址')
country: str | None = Field(None, description='国家')
region: str | None = Field(None, description='地区')
city: str | None = Field(None, description='城市')
user_agent: str = Field(description='用户代理')
os: str | None = Field(None, description='操作系统')
browser: str | None = Field(None, description='浏览器')
device: str | None = Field(None, description='设备')
args: dict[str, Any] | None = Field(None, description='请求参数')
status: StatusType = Field(description='状态')
code: str = Field(description='状态码')
msg: str | None = Field(None, description='消息')
cost_time: float = Field(description='耗时')
opera_time: datetime = Field(description='操作时间')
class CreateOperaLogParam(OperaLogSchemaBase):
pass
"""创建操作日志参数"""
class UpdateOperaLogParam(OperaLogSchemaBase):
pass
"""更新操作日志参数"""
class DeleteOperaLogParam(SchemaBase):
"""删除操作日志参数"""
pks: list[int] = Field(description='操作日志 ID 列表')
class GetOperaLogDetail(OperaLogSchemaBase):
"""操作日志详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
id: int = Field(description='日志 ID')
created_time: datetime = Field(description='创建时间')
+34 -14
View File
@@ -4,39 +4,59 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.admin.schema.data_rule import GetDataRuleDetail
from backend.app.admin.schema.data_scope import GetDataScopeDetail
from backend.app.admin.schema.menu import GetMenuDetail
from backend.common.enums import StatusType
from backend.common.schema import SchemaBase
class RoleSchemaBase(SchemaBase):
name: str
status: StatusType = Field(default=StatusType.enable)
remark: str | None = None
"""角色基础模型"""
name: str = Field(description='角色名称')
status: StatusType = Field(description='状态')
is_filter_scopes: bool = Field(True, description='过滤数据权限')
remark: str | None = Field(None, description='备注')
class CreateRoleParam(RoleSchemaBase):
pass
"""创建角色参数"""
class UpdateRoleParam(RoleSchemaBase):
pass
"""更新角色参数"""
class DeleteRoleParam(SchemaBase):
"""删除角色参数"""
pks: list[int] = Field(description='角色 ID 列表')
class UpdateRoleMenuParam(SchemaBase):
menus: list[int]
"""更新角色菜单参数"""
menus: list[int] = Field(description='菜单 ID 列表')
class UpdateRoleRuleParam(SchemaBase):
rules: list[int]
class UpdateRoleScopeParam(SchemaBase):
"""更新角色数据范围参数"""
scopes: list[int] = Field(description='数据范围 ID 列表')
class GetRoleDetail(RoleSchemaBase):
"""角色详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
menus: list[GetMenuDetail | None] = []
rules: list[GetDataRuleDetail | None] = []
id: int = Field(description='角色 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
class GetRoleWithRelationDetail(GetRoleDetail):
"""角色关联详情"""
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
scopes: list[GetDataScopeDetail | None] = Field([], description='数据范围列表')
+29 -23
View File
@@ -2,44 +2,50 @@
# -*- coding: utf-8 -*-
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.schema import SchemaBase
class GetSwaggerToken(SchemaBase):
access_token: str
token_type: str = 'Bearer'
user: GetUserInfoNoRelationDetail
"""Swagger 认证令牌"""
access_token: str = Field(description='访问令牌')
token_type: str = Field('Bearer', description='令牌类型')
user: GetUserInfoDetail = Field(description='用户信息')
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):
pass
"""获取新令牌"""
class GetLoginToken(AccessTokenBase):
user: GetUserInfoNoRelationDetail
"""获取登录令牌"""
class KickOutToken(SchemaBase):
session_uuid: str
user: GetUserInfoDetail = Field(description='用户信息')
class GetTokenDetail(SchemaBase):
id: int
session_uuid: str
username: str
nickname: str
ip: str
os: str
browser: str
device: str
status: StatusType
last_login_time: str
expire_time: datetime
"""令牌详情"""
id: int = Field(description='用户 ID')
session_uuid: str = Field(description='会话 UUID')
username: str = Field(description='用户名')
nickname: str = Field(description='昵称')
ip: str = Field(description='IP 地址')
os: str = Field(description='操作系统')
browser: str = Field(description='浏览器')
device: str = Field(description='设备')
status: StatusType = Field(description='状态')
last_login_time: str = Field(description='最后登录时间')
expire_time: datetime = Field(description='过期时间')
+66 -55
View File
@@ -3,88 +3,109 @@
from datetime import datetime
from typing import Any
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
from pydantic import ConfigDict, Field, HttpUrl, model_validator
from typing_extensions import Self
from 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.schema import CustomPhoneNumber, SchemaBase
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
class AuthSchemaBase(SchemaBase):
username: str
password: str | None
"""用户认证基础模型"""
username: str = Field(description='用户名')
password: str = Field(description='密码')
class AuthLoginParam(AuthSchemaBase):
captcha: str
"""用户登录参数"""
class RegisterUserParam(AuthSchemaBase):
nickname: str | None = None
email: EmailStr = Field(examples=['user@example.com'])
captcha: str = Field(description='验证码')
class AddUserParam(AuthSchemaBase):
dept_id: int
roles: list[int]
nickname: str | None = None
email: EmailStr = Field(examples=['user@example.com'])
"""添加用户参数"""
nickname: str | None = Field(None, description='昵称')
email: CustomEmailStr | None = Field(None, description='邮箱')
phone: CustomPhoneNumber | None = Field(None, description='手机号码')
dept_id: int = Field(description='部门 ID')
roles: list[int] = Field(description='角色 ID 列表')
class AddOAuth2UserParam(AuthSchemaBase):
"""添加 OAuth2 用户参数"""
password: str | None = Field(None, description='密码')
nickname: str | None = Field(None, description='昵称')
email: CustomEmailStr | None = Field(None, description='邮箱')
avatar: HttpUrl | None = Field(None, description='头像地址')
class ResetPasswordParam(SchemaBase):
"""重置密码参数"""
old_password: str = Field(description='旧密码')
new_password: str = Field(description='新密码')
confirm_password: str = Field(description='确认密码')
class UserInfoSchemaBase(SchemaBase):
dept_id: int | None = None
username: str
nickname: str
email: EmailStr = Field(examples=['user@example.com'])
phone: CustomPhoneNumber | None = None
"""用户信息基础模型"""
dept_id: int | None = Field(None, description='部门 ID')
username: str = Field(description='用户名')
nickname: str = Field(description='昵称')
avatar: HttpUrl | None = Field(None, description='头像地址')
email: CustomEmailStr | None = Field(None, description='邮箱')
phone: CustomPhoneNumber | None = Field(None, description='手机号')
class UpdateUserParam(UserInfoSchemaBase):
pass
"""更新用户参数"""
roles: list[int] = Field(description='角色 ID 列表')
class UpdateUserRoleParam(SchemaBase):
roles: list[int]
class GetUserInfoDetail(UserInfoSchemaBase):
"""用户信息详情"""
class AvatarParam(SchemaBase):
url: HttpUrl = Field(description='头像 http 地址')
class GetUserInfoNoRelationDetail(UserInfoSchemaBase):
model_config = ConfigDict(from_attributes=True)
dept_id: int | None = None
id: int
uuid: str
avatar: str | None = None
status: StatusType = Field(default=StatusType.enable)
is_superuser: bool
is_staff: bool
is_multi_login: bool
join_time: datetime = None
last_login_time: datetime | None = None
dept_id: int | None = Field(None, description='部门 ID')
id: int = Field(description='用户 ID')
uuid: str = Field(description='用户 UUID')
status: StatusType = Field(description='状态')
is_superuser: bool = Field(description='是否超级管理员')
is_staff: bool = Field(description='是否管理员')
is_multi_login: bool = Field(description='是否允许多端登录')
join_time: datetime = Field(description='加入时间')
last_login_time: datetime | None = Field(None, description='最后登录时间')
class GetUserInfoDetail(GetUserInfoNoRelationDetail):
class GetUserInfoWithRelationDetail(GetUserInfoDetail):
"""用户信息关联详情"""
model_config = ConfigDict(from_attributes=True)
dept: GetDeptDetail | None = None
roles: list[GetRoleDetail]
dept: GetDeptDetail | None = Field(None, description='部门信息')
roles: list[GetRoleWithRelationDetail] = Field(description='角色列表')
class GetCurrentUserInfoDetail(GetUserInfoDetail):
class GetCurrentUserInfoWithRelationDetail(GetUserInfoWithRelationDetail):
"""当前用户信息关联详情"""
model_config = ConfigDict(from_attributes=True)
dept: str | None = None
roles: list[str]
dept: str | None = Field(None, description='部门名称')
roles: list[str] = Field(description='角色名称列表')
@model_validator(mode='before')
@classmethod
def handel(cls, data: Any) -> Self:
"""处理部门和角色"""
"""处理部门和角色数据"""
dept = data['dept']
if dept:
data['dept'] = dept['name']
@@ -92,13 +113,3 @@ class GetCurrentUserInfoDetail(GetUserInfoDetail):
if roles:
data['roles'] = [role['name'] for role in roles]
return data
class CurrentUserIns(GetUserInfoDetail):
model_config = ConfigDict(from_attributes=True)
class ResetPasswordParam(SchemaBase):
old_password: str
new_password: str
confirm_password: str
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import UserSocialType
from backend.common.schema import SchemaBase
class UserSocialSchemaBase(SchemaBase):
source: UserSocialType
open_id: str | None = None
uid: str | None = None
union_id: str | None = None
scope: str | None = None
code: str | None = None
class CreateUserSocialParam(UserSocialSchemaBase):
user_id: int
class UpdateUserSocialParam(SchemaBase):
pass
+120 -50
View File
@@ -5,7 +5,7 @@ from fastapi.security import HTTPBasicCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.background import BackgroundTask, BackgroundTasks
from backend.app.admin.conf import admin_settings
from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_user import user_dao
from backend.app.admin.model import User
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
@@ -13,6 +13,7 @@ from backend.app.admin.schema.user import AuthLoginParam
from backend.app.admin.service.login_log_service import login_log_service
from backend.common.enums import LoginLogStatusType
from backend.common.exception import errors
from backend.common.i18n import t
from backend.common.log import log
from backend.common.response.response_code import CustomErrorCode
from backend.common.security.jwt import (
@@ -30,68 +31,99 @@ from backend.utils.timezone import timezone
class AuthService:
"""认证服务类"""
@staticmethod
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
"""
验证用户名和密码
:param db: 数据库会话
:param username: 用户名
:param password: 密码
:return:
"""
user = await user_dao.get_by_username(db, username)
if not user:
raise errors.NotFoundError(msg='用户名或密码有误')
elif not password_verify(password, user.password):
if user.password is None:
raise errors.AuthorizationError(msg='用户名或密码有误')
elif not user.status:
else:
if not password_verify(password, user.password):
raise errors.AuthorizationError(msg='用户名或密码有误')
if not user.status:
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
return user
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
"""
Swagger 文档登录
:param obj: 登录凭证
:return:
"""
async with async_db_session.begin() as db:
user = await self.user_verify(db, obj.username, obj.password)
await user_dao.update_login_time(db, obj.username)
a_token = await create_access_token(
str(user.id),
access_token = await create_access_token(
user.id,
user.is_multi_login,
# extra info
login_type='swagger',
swagger=True,
)
return a_token.access_token, user
return access_token.access_token, user
async def login(
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
) -> GetLoginToken:
"""
用户登录
:param request: 请求对象
:param response: 响应对象
:param obj: 登录参数
:param background_tasks: 后台任务
:return:
"""
async with async_db_session.begin() as db:
user = None
try:
user = await self.user_verify(db, obj.username, obj.password)
captcha_code = await redis_client.get(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
if not captcha_code:
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
raise errors.RequestError(msg=t('error.captcha.expired'))
if captcha_code.lower() != obj.captcha.lower():
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
await user_dao.update_login_time(db, obj.username)
await db.refresh(user)
a_token = await create_access_token(
str(user.id),
access_token = await create_access_token(
user.id,
user.is_multi_login,
# extra info
username=user.username,
nickname=user.nickname,
last_login_time=timezone.t_str(user.last_login_time),
last_login_time=timezone.to_str(user.last_login_time),
ip=request.state.ip,
os=request.state.os,
browser=request.state.browser,
device=request.state.device,
)
r_token = await create_refresh_token(str(user.id), user.is_multi_login)
refresh_token = await create_refresh_token(access_token.session_uuid, user.id, user.is_multi_login)
response.set_cookie(
key=settings.COOKIE_REFRESH_TOKEN_KEY,
value=r_token.refresh_token,
value=refresh_token.refresh_token,
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
expires=timezone.f_utc(r_token.refresh_token_expire_time),
expires=timezone.to_utc(refresh_token.refresh_token_expire_time),
httponly=True,
)
except errors.NotFoundError as e:
log.error('登陆错误: 用户名不存在')
raise errors.NotFoundError(msg=e.msg)
except (errors.AuthorizationError, errors.CustomError) as e:
except (errors.RequestError, errors.CustomError) as e:
if not user:
log.error('登陆错误: 用户密码有误')
task = BackgroundTask(
@@ -106,7 +138,7 @@ class AuthService:
msg=e.msg,
),
)
raise errors.AuthorizationError(msg=e.msg, background=task)
raise errors.RequestError(code=e.code, msg=e.msg, background=task)
except Exception as e:
log.error(f'登陆错误: {e}')
raise e
@@ -120,40 +152,72 @@ class AuthService:
username=obj.username,
login_time=timezone.now(),
status=LoginLogStatusType.success.value,
msg='登录成功',
msg=t('success.login.success'),
),
)
data = GetLoginToken(
access_token=a_token.access_token,
access_token_expire_time=a_token.access_token_expire_time,
session_uuid=a_token.session_uuid,
access_token=access_token.access_token,
access_token_expire_time=access_token.access_token_expire_time,
session_uuid=access_token.session_uuid,
user=user, # type: ignore
)
return data
@staticmethod
async def new_token(*, request: Request) -> GetNewToken:
async def get_codes(*, request: Request) -> list[str]:
"""
获取用户权限码
:param request: FastAPI 请求对象
:return:
"""
codes = set()
if request.user.is_superuser:
async with async_db_session.begin() as db:
menus = await menu_dao.get_all(db, None, None)
for menu in menus:
if menu.perms:
codes.add(*menu.perms.split(','))
else:
roles = request.user.roles
if roles:
for role in roles:
for menu in role.menus:
if menu.perms:
codes.add(*menu.perms.split(','))
return list(codes)
@staticmethod
async def refresh_token(*, request: Request) -> GetNewToken:
"""
刷新令牌
:param request: FastAPI 请求对象
:return:
"""
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if not refresh_token:
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
try:
user_id = jwt_decode(refresh_token).id
except Exception:
raise errors.TokenError(msg='Refresh Token 无效')
raise errors.RequestError(msg='Refresh Token 已过期,请重新登录')
token_payload = jwt_decode(refresh_token)
async with async_db_session() as db:
user = await user_dao.get(db, user_id)
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户名或密码有误')
raise errors.NotFoundError(msg='用户不存在')
elif not user.status:
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
if not user.is_multi_login:
if await redis_client.keys(match=f'{settings.TOKEN_REDIS_PREFIX}:{user.id}:*'):
raise errors.ForbiddenError(msg='此用户已在异地登录,请重新登录并及时修改密码')
new_token = await create_new_token(
user_id=str(user.id),
refresh_token=refresh_token,
multi_login=user.is_multi_login,
refresh_token,
token_payload.session_uuid,
user.id,
user.is_multi_login,
# extra info
username=user.username,
nickname=user.nickname,
last_login_time=timezone.t_str(user.last_login_time),
last_login_time=timezone.to_str(user.last_login_time),
ip=request.state.ip,
os=request.state.os,
browser=request.state.browser,
@@ -168,22 +232,28 @@ class AuthService:
@staticmethod
async def logout(*, request: Request, response: Response) -> None:
token = get_token(request)
token_payload = jwt_decode(token)
user_id = token_payload.id
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
if request.user.is_multi_login:
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
if refresh_token:
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
else:
key_prefix = [
f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:',
]
for prefix in key_prefix:
await redis_client.delete_prefix(prefix)
"""
用户登出
:param request: FastAPI 请求对象
:param response: FastAPI 响应对象
:return:
"""
try:
token = get_token(request)
token_payload = jwt_decode(token)
user_id = token_payload.id
session_uuid = token_payload.session_uuid
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
except errors.TokenError:
return
finally:
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
await redis_client.delete(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}')
if refresh_token:
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
auth_service: AuthService = AuthService()
@@ -1,75 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select
from backend.app.admin.conf import admin_settings
from backend.app.admin.crud.crud_config import config_dao
from backend.app.admin.model import Config
from backend.app.admin.schema.config import (
CreateConfigParam,
SaveBuiltInConfigParam,
UpdateConfigParam,
)
from backend.common.exception import errors
from backend.database.db import async_db_session
class ConfigService:
@staticmethod
async def get_built_in_config(type: str) -> Sequence[Config]:
async with async_db_session() as db:
return await config_dao.get_by_type(db, type)
@staticmethod
async def save_built_in_config(objs: list[SaveBuiltInConfigParam], type: str) -> None:
async with async_db_session.begin() as db:
for obj in objs:
config = await config_dao.get_by_key_and_type(db, obj.key, type)
if config is None:
if await config_dao.get_by_key(db, obj.key, built_in=True):
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
await config_dao.create_model(db, obj, flush=True, type=type)
else:
await config_dao.update_model(db, config.id, obj, type=type)
@staticmethod
async def get(pk) -> Config | dict:
async with async_db_session() as db:
config = await config_dao.get(db, pk)
if not config:
raise errors.NotFoundError(msg='参数配置不存在')
return config
@staticmethod
async def get_select(*, name: str = None, type: str = None) -> Select:
return await config_dao.get_list(name=name, type=type)
@staticmethod
async def create(*, obj: CreateConfigParam) -> None:
async with async_db_session.begin() as db:
if obj.type in admin_settings.CONFIG_BUILT_IN_TYPES:
raise errors.ForbiddenError(msg='非法类型参数')
config = await config_dao.get_by_key(db, obj.key)
if config:
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
await config_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateConfigParam) -> int:
async with async_db_session.begin() as db:
config = await config_dao.get(db, pk)
if not config:
raise errors.NotFoundError(msg='参数配置不存在')
count = await config_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
async with async_db_session.begin() as db:
count = await config_dao.delete(db, pk)
return count
config_service: ConfigService = ConfigService()
+62 -26
View File
@@ -2,87 +2,123 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
from backend.app.admin.crud.crud_data_rule import data_rule_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import DataRule
from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRuleParam
from backend.app.admin.schema.data_rule import (
CreateDataRuleParam,
DeleteDataRuleParam,
GetDataRuleColumnDetail,
UpdateDataRuleParam,
)
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.import_parse import dynamic_import_data_model
class DataRuleService:
"""数据规则服务类"""
@staticmethod
async def get(*, pk: int) -> DataRule:
"""
获取数据规则详情
:param pk: 规则 ID
:return:
"""
async with async_db_session() as db:
data_rule = await data_rule_dao.get(db, pk)
if not data_rule:
raise errors.NotFoundError(msg='数据规则不存在')
return data_rule
@staticmethod
async def get_role_rules(*, pk: int) -> list[int]:
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
rule_ids = [rule.id for rule in role.rules]
return rule_ids
@staticmethod
async def get_models() -> list[str]:
"""获取所有数据规则可用模型"""
return list(settings.DATA_PERMISSION_MODELS.keys())
@staticmethod
async def get_columns(model: str) -> list[str]:
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
"""
获取数据规则可用模型的字段列表
:param model: 模型名称
:return:
"""
if model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据模型不存在')
try:
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
except (ImportError, AttributeError):
raise errors.ServerError(msg=f'数据模型 {model} 动态导入失败,请联系系统超级管理员')
raise errors.NotFoundError(msg='数据规则可用模型不存在')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
GetDataRuleColumnDetail(key=column.key, comment=column.comment)
for column in model_ins.__table__.columns
if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
]
return model_columns
@staticmethod
async def get_select(*, name: str = None) -> Select:
async def get_select(*, name: str | None) -> Select:
"""
获取数据规则列表查询条件
:param name: 规则名称
:return:
"""
return await data_rule_dao.get_list(name=name)
@staticmethod
async def get_all() -> Sequence[DataRule]:
"""获取所有数据规则"""
async with async_db_session() as db:
data_rules = await data_rule_dao.get_all(db)
return data_rules
@staticmethod
async def create(*, obj: CreateDataRuleParam) -> None:
"""
创建数据规则
:param obj: 规则创建参数
:return:
"""
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get_by_name(db, obj.name)
if data_rule:
raise errors.ForbiddenError(msg='数据权限规则已存在')
raise errors.ConflictError(msg='数据规则已存在')
await data_rule_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
"""
更新数据规则
:param pk: 规则 ID
:param obj: 规则更新参数
:return:
"""
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get(db, pk)
if not data_rule:
raise errors.NotFoundError(msg='数据权限规则不存在')
raise errors.NotFoundError(msg='数据规则不存在')
if data_rule.name != obj.name:
if await data_rule_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='数据规则已存在')
count = await data_rule_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, request: Request, pk: list[int]) -> int:
async def delete(*, obj: DeleteDataRuleParam) -> int:
"""
批量删除数据规则
:param obj: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_rule_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
count = await data_rule_dao.delete(db, obj.pks)
return count
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select
from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.app.admin.model import DataScope
from backend.app.admin.schema.data_scope import (
CreateDataScopeParam,
DeleteDataScopeParam,
UpdateDataScopeParam,
UpdateDataScopeRuleParam,
)
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
class DataScopeService:
"""数据范围服务类"""
@staticmethod
async def get(*, pk: int) -> DataScope:
"""
获取数据范围详情
:param pk: 范围 ID
:return:
"""
async with async_db_session() as db:
data_scope = await data_scope_dao.get(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
return data_scope
@staticmethod
async def get_all() -> Sequence[DataScope]:
"""获取所有数据范围"""
async with async_db_session() as db:
data_scopes = await data_scope_dao.get_all(db)
return data_scopes
@staticmethod
async def get_rules(*, pk: int) -> DataScope:
"""
获取数据范围规则
:param pk: 范围 ID
:return:
"""
async with async_db_session() as db:
data_scope = await data_scope_dao.get_with_relation(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
return data_scope
@staticmethod
async def get_select(*, name: str | None, status: int | None) -> Select:
"""
获取数据范围列表查询条件
:param name: 范围名称
:param status: 范围状态
:return:
"""
return await data_scope_dao.get_list(name, status)
@staticmethod
async def create(*, obj: CreateDataScopeParam) -> None:
"""
创建数据范围
:param obj: 数据范围参数
:return:
"""
async with async_db_session.begin() as db:
data_scope = await data_scope_dao.get_by_name(db, obj.name)
if data_scope:
raise errors.ConflictError(msg='数据范围已存在')
await data_scope_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDataScopeParam) -> int:
"""
更新数据范围
:param pk: 范围 ID
:param obj: 数据范围更新参数
:return:
"""
async with async_db_session.begin() as db:
data_scope = await data_scope_dao.get(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
if data_scope.name != obj.name:
if await data_scope_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='数据范围已存在')
count = await data_scope_dao.update(db, pk, obj)
for role in await data_scope.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_data_scope_rule(*, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
"""
更新数据范围规则
:param pk: 范围 ID
:param rule_ids: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_scope_dao.update_rules(db, pk, rule_ids)
return count
@staticmethod
async def delete(*, obj: DeleteDataScopeParam) -> int:
"""
批量删除数据范围
:param obj: 范围 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_scope_dao.delete(db, obj.pks)
for pk in obj.pks:
data_rule = await data_scope_dao.get(db, pk)
if data_rule:
for role in await data_rule.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
data_scope_service: DataScopeService = DataScopeService()
+49 -11
View File
@@ -15,8 +15,16 @@ from backend.utils.build_tree import get_tree_data
class DeptService:
"""部门服务类"""
@staticmethod
async def get(*, pk: int) -> Dept:
"""
获取部门详情
:param pk: 部门 ID
:return:
"""
async with async_db_session() as db:
dept = await dept_dao.get(db, pk)
if not dept:
@@ -24,20 +32,36 @@ class DeptService:
return dept
@staticmethod
async def get_dept_tree(
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None
async def get_tree(
*, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
) -> list[dict[str, Any]]:
"""
获取部门树形结构
:param request: FastAPI 请求对象
:param name: 部门名称
:param leader: 部门负责人
:param phone: 联系电话
:param status: 状态
:return:
"""
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(request, db, name, leader, phone, status)
tree_data = get_tree_data(dept_select)
return tree_data
@staticmethod
async def create(*, obj: CreateDeptParam) -> None:
"""
创建部门
:param obj: 部门创建参数
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get_by_name(db, obj.name)
if dept:
raise errors.ForbiddenError(msg='部门名称已存在')
raise errors.ConflictError(msg='部门名称已存在')
if obj.parent_id:
parent_dept = await dept_dao.get(db, obj.parent_id)
if not parent_dept:
@@ -46,13 +70,20 @@ class DeptService:
@staticmethod
async def update(*, pk: int, obj: UpdateDeptParam) -> int:
"""
更新部门
:param pk: 部门 ID
:param obj: 部门更新参数
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get(db, pk)
if not dept:
raise errors.NotFoundError(msg='部门不存在')
if dept.name != obj.name:
if await dept_dao.get_by_name(db, obj.name):
raise errors.ForbiddenError(msg='部门名称已存在')
raise errors.ConflictError(msg='部门名称已存在')
if obj.parent_id:
parent_dept = await dept_dao.get(db, obj.parent_id)
if not parent_dept:
@@ -63,16 +94,23 @@ class DeptService:
return count
@staticmethod
async def delete(*, request: Request, pk: int) -> int:
async def delete(*, pk: int) -> int:
"""
删除部门
:param pk: 部门 ID
:return:
"""
async with async_db_session.begin() as db:
dept_user = await dept_dao.get_with_relation(db, pk)
if dept_user:
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
dept = await dept_dao.get_with_relation(db, pk)
if dept.users:
raise errors.ConflictError(msg='部门下存在用户,无法删除')
children = await dept_dao.get_children(db, pk)
if children:
raise errors.ForbiddenError(msg='部门下存在子部门,无法删除')
raise errors.ConflictError(msg='部门下存在子部门,无法删除')
count = await dept_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
for user in dept.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from backend.app.admin.crud.crud_dict_data import dict_data_dao
from backend.app.admin.crud.crud_dict_type import dict_type_dao
from backend.app.admin.model import DictData
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
from backend.common.exception import errors
from backend.database.db import async_db_session
class DictDataService:
@staticmethod
async def get(*, pk: int) -> DictData:
async with async_db_session() as db:
dict_data = await dict_data_dao.get_with_relation(db, pk)
if not dict_data:
raise errors.NotFoundError(msg='字典数据不存在')
return dict_data
@staticmethod
async def get_select(*, label: str = None, value: str = None, status: int = None) -> Select:
return await dict_data_dao.get_list(label=label, value=value, status=status)
@staticmethod
async def create(*, obj: CreateDictDataParam) -> None:
async with async_db_session.begin() as db:
dict_data = await dict_data_dao.get_by_label(db, obj.label)
if dict_data:
raise errors.ForbiddenError(msg='字典数据已存在')
dict_type = await dict_type_dao.get(db, obj.type_id)
if not dict_type:
raise errors.NotFoundError(msg='字典类型不存在')
await dict_data_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDictDataParam) -> int:
async with async_db_session.begin() as db:
dict_data = await dict_data_dao.get(db, pk)
if not dict_data:
raise errors.NotFoundError(msg='字典数据不存在')
if dict_data.label != obj.label:
if await dict_data_dao.get_by_label(db, obj.label):
raise errors.ForbiddenError(msg='字典数据已存在')
dict_type = await dict_type_dao.get(db, obj.type_id)
if not dict_type:
raise errors.NotFoundError(msg='字典类型不存在')
count = await dict_data_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
async with async_db_session.begin() as db:
count = await dict_data_dao.delete(db, pk)
return count
dict_data_service: DictDataService = DictDataService()
@@ -1,43 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from backend.app.admin.crud.crud_dict_type import dict_type_dao
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
from backend.common.exception import errors
from backend.database.db import async_db_session
class DictTypeService:
@staticmethod
async def get_select(*, name: str = None, code: str = None, status: int = None) -> Select:
return await dict_type_dao.get_list(name=name, code=code, status=status)
@staticmethod
async def create(*, obj: CreateDictTypeParam) -> None:
async with async_db_session.begin() as db:
dict_type = await dict_type_dao.get_by_code(db, obj.code)
if dict_type:
raise errors.ForbiddenError(msg='字典类型已存在')
await dict_type_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDictTypeParam) -> int:
async with async_db_session.begin() as db:
dict_type = await dict_type_dao.get(db, pk)
if not dict_type:
raise errors.NotFoundError(msg='字典类型不存在')
if dict_type.code != obj.code:
if await dict_type_dao.get_by_code(db, obj.code):
raise errors.ForbiddenError(msg='字典类型已存在')
count = await dict_type_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
async with async_db_session.begin() as db:
count = await dict_type_dao.delete(db, pk)
return count
dict_type_service: DictTypeService = DictTypeService()
+37 -9
View File
@@ -7,14 +7,24 @@ from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_login_log import login_log_dao
from backend.app.admin.schema.login_log import CreateLoginLogParam
from backend.app.admin.schema.login_log import CreateLoginLogParam, DeleteLoginLogParam
from backend.common.log import log
from backend.database.db import async_db_session
class LoginLogService:
"""登录日志服务类"""
@staticmethod
async def get_select(*, username: str, status: int, ip: str) -> Select:
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取登录日志列表查询条件
:param username: 用户名
:param status: 状态
:param ip: IP 地址
:return:
"""
return await login_log_dao.get_list(username=username, status=status, ip=ip)
@staticmethod
@@ -28,8 +38,20 @@ class LoginLogService:
status: int,
msg: str,
) -> None:
"""
创建登录日志
:param db: 数据库会话
:param request: FastAPI 请求对象
:param user_uuid: 用户 UUID
:param username: 用户名
:param login_time: 登录时间
:param status: 状态
:param msg: 消息
:return:
"""
try:
obj_in = CreateLoginLogParam(
obj = CreateLoginLogParam(
user_uuid=user_uuid,
username=username,
status=status,
@@ -44,21 +66,27 @@ class LoginLogService:
msg=msg,
login_time=login_time,
)
await login_log_dao.create(db, obj_in)
await login_log_dao.create(db, obj)
except Exception as e:
log.error(f'登录日志创建失败: {e}')
@staticmethod
async def delete(*, pk: list[int]) -> int:
async def delete(*, obj: DeleteLoginLogParam) -> int:
"""
批量删除登录日志
:param obj: 日志 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await login_log_dao.delete(db, pk)
count = await login_log_dao.delete(db, obj.pks)
return count
@staticmethod
async def delete_all() -> int:
async def delete_all() -> None:
"""清空所有登录日志"""
async with async_db_session.begin() as db:
count = await login_log_dao.delete_all(db)
return count
await login_log_dao.delete_all(db)
login_log_service: LoginLogService = LoginLogService()
+67 -29
View File
@@ -5,19 +5,26 @@ from typing import Any
from fastapi import Request
from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import Menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data
from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
class MenuService:
"""菜单服务类"""
@staticmethod
async def get(*, pk: int) -> Menu:
"""
获取菜单详情
:param pk: 菜单 ID
:return:
"""
async with async_db_session() as db:
menu = await menu_dao.get(db, menu_id=pk)
if not menu:
@@ -25,42 +32,53 @@ class MenuService:
return menu
@staticmethod
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]:
async def get_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
"""
获取菜单树形结构
:param title: 菜单标题
:param status: 状态
:return:
"""
async with async_db_session() as db:
menu_select = await menu_dao.get_all(db, title=title, status=status)
menu_tree = get_tree_data(menu_select)
menu_data = await menu_dao.get_all(db, title=title, status=status)
menu_tree = get_tree_data(menu_data)
return menu_tree
@staticmethod
async def get_role_menu_tree(*, pk: int) -> list[dict[str, Any]]:
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
menu_ids = [menu.id for menu in role.menus]
menu_select = await menu_dao.get_role_menus(db, False, menu_ids)
menu_tree = get_tree_data(menu_select)
return menu_tree
async def get_sidebar(*, request: Request) -> list[dict[str, Any] | None]:
"""
获取用户的菜单侧边栏
@staticmethod
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
:param request: FastAPI 请求对象
:return:
"""
async with async_db_session() as db:
roles = request.user.roles
menu_ids = []
menu_tree = []
if roles:
for role in roles:
menu_ids.extend([menu.id for menu in role.menus])
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
menu_tree = get_tree_data(menu_select)
if request.user.is_superuser:
menu_data = await menu_dao.get_sidebar(db, None)
else:
roles = request.user.roles
menu_ids = set()
if roles:
for role in roles:
for menu in role.menus:
menu_ids.add(menu.id)
menu_data = await menu_dao.get_sidebar(db, list(menu_ids))
menu_tree = get_vben5_tree_data(menu_data)
return menu_tree
@staticmethod
async def create(*, obj: CreateMenuParam) -> None:
"""
创建菜单
:param obj: 菜单创建参数
:return:
"""
async with async_db_session.begin() as db:
title = await menu_dao.get_by_title(db, obj.title)
if title:
raise errors.ForbiddenError(msg='菜单标题已存在')
raise errors.ConflictError(msg='菜单标题已存在')
if obj.parent_id:
parent_menu = await menu_dao.get(db, obj.parent_id)
if not parent_menu:
@@ -69,13 +87,20 @@ class MenuService:
@staticmethod
async def update(*, pk: int, obj: UpdateMenuParam) -> int:
"""
更新菜单
:param pk: 菜单 ID
:param obj: 菜单更新参数
:return:
"""
async with async_db_session.begin() as db:
menu = await menu_dao.get(db, pk)
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
if menu.title != obj.title:
if await menu_dao.get_by_title(db, obj.title):
raise errors.ForbiddenError(msg='菜单标题已存在')
raise errors.ConflictError(msg='菜单标题已存在')
if obj.parent_id:
parent_menu = await menu_dao.get(db, obj.parent_id)
if not parent_menu:
@@ -83,16 +108,29 @@ class MenuService:
if obj.parent_id == menu.id:
raise errors.ForbiddenError(msg='禁止关联自身为父级')
count = await menu_dao.update(db, pk, obj)
for role in await menu.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def delete(*, request: Request, pk: int) -> int:
async def delete(*, pk: int) -> int:
"""
删除菜单
:param pk: 菜单 ID
:return:
"""
async with async_db_session.begin() as db:
children = await menu_dao.get_children(db, pk)
if children:
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
raise errors.ConflictError(msg='菜单下存在子菜单,无法删除')
menu = await menu_dao.get(db, pk)
count = await menu_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
if menu:
for role in await menu.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fast_captcha import text_captcha
from fastapi import BackgroundTasks, Request, Response
from backend.app.admin.conf import admin_settings
from backend.app.admin.crud.crud_user import user_dao
from backend.app.admin.crud.crud_user_social import user_social_dao
from backend.app.admin.schema.token import GetLoginToken
from backend.app.admin.schema.user import RegisterUserParam
from backend.app.admin.schema.user_social import CreateUserSocialParam
from backend.app.admin.service.login_log_service import login_log_service
from backend.common.enums import LoginLogStatusType, UserSocialType
from backend.common.exception.errors import AuthorizationError
from backend.common.security import jwt
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.timezone import timezone
class OAuth2Service:
@staticmethod
async def create_with_login(
*,
request: Request,
response: Response,
background_tasks: BackgroundTasks,
user: dict,
social: UserSocialType,
) -> GetLoginToken | None:
async with async_db_session.begin() as db:
# 获取 OAuth2 平台用户信息
social_id = user.get('id')
social_username = user.get('username')
if social == UserSocialType.github:
social_username = user.get('login')
social_nickname = user.get('name')
social_email = user.get('email')
if social == UserSocialType.linuxdo: # 不提供明文邮箱的平台
social_email = f'{social_username}@linux.do'
if not social_email:
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
# 创建系统用户
sys_user = await user_dao.check_email(db, social_email)
if not sys_user:
sys_user = await user_dao.get_by_username(db, social_username)
if sys_user:
social_username = f'{social_username}#{text_captcha(5)}'
sys_user = await user_dao.get_by_nickname(db, social_nickname)
if sys_user:
social_username = f'{social_nickname}#{text_captcha(5)}'
new_sys_user = RegisterUserParam(
username=social_username, password=None, nickname=social_username, email=social_email
)
await user_dao.create(db, new_sys_user, social=True)
await db.flush()
sys_user = await user_dao.check_email(db, social_email)
# 绑定社交用户
sys_user_id = sys_user.id
user_social = await user_social_dao.get(db, sys_user_id, social.value)
if not user_social:
new_user_social = CreateUserSocialParam(source=social.value, uid=str(social_id), user_id=sys_user_id)
await user_social_dao.create(db, new_user_social)
# 创建 token
access_token = await jwt.create_access_token(
str(sys_user_id),
sys_user.is_multi_login,
# extra info
username=sys_user.username,
nickname=sys_user.nickname,
last_login_time=timezone.t_str(timezone.now()),
ip=request.state.ip,
os=request.state.os,
browser=request.state.browser,
device=request.state.device,
)
refresh_token = await jwt.create_refresh_token(str(sys_user_id), multi_login=sys_user.is_multi_login)
await user_dao.update_login_time(db, sys_user.username)
await db.refresh(sys_user)
login_log = dict(
db=db,
request=request,
user_uuid=sys_user.uuid,
username=sys_user.username,
login_time=timezone.now(),
status=LoginLogStatusType.success.value,
msg='登录成功(OAuth2',
)
background_tasks.add_task(login_log_service.create, **login_log)
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
response.set_cookie(
key=settings.COOKIE_REFRESH_TOKEN_KEY,
value=refresh_token.refresh_token,
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
expires=timezone.f_utc(refresh_token.refresh_token_expire_time),
httponly=True,
)
data = GetLoginToken(
access_token=access_token.access_token,
access_token_expire_time=access_token.access_token_expire_time,
user=sys_user, # type: ignore
session_uuid=access_token.session_uuid,
)
return data
oauth2_service: OAuth2Service = OAuth2Service()
+42 -9
View File
@@ -3,31 +3,64 @@
from sqlalchemy import Select
from backend.app.admin.crud.crud_opera_log import opera_log_dao
from backend.app.admin.schema.opera_log import CreateOperaLogParam
from backend.app.admin.schema.opera_log import CreateOperaLogParam, DeleteOperaLogParam
from backend.database.db import async_db_session
class OperaLogService:
"""操作日志服务类"""
@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, status: int | None, ip: str | None) -> Select:
"""
获取操作日志列表查询条件
:param username: 用户名
:param status: 状态
:param ip: IP 地址
:return:
"""
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
@staticmethod
async def create(*, obj_in: CreateOperaLogParam):
async def create(*, obj: CreateOperaLogParam) -> None:
"""
创建操作日志
:param obj: 操作日志创建参数
:return:
"""
async with async_db_session.begin() as db:
await opera_log_dao.create(db, obj_in)
await opera_log_dao.create(db, obj)
@staticmethod
async def delete(*, pk: list[int]) -> int:
async def bulk_create(*, objs: list[CreateOperaLogParam]) -> None:
"""
批量创建操作日志
:param objs: 操作日志创建参数列表
:return:
"""
async with async_db_session.begin() as db:
count = await opera_log_dao.delete(db, pk)
await opera_log_dao.bulk_create(db, objs)
@staticmethod
async def delete(*, obj: DeleteOperaLogParam) -> int:
"""
批量删除操作日志
:param obj: 日志 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await opera_log_dao.delete(db, obj.pks)
return count
@staticmethod
async def delete_all() -> int:
async def delete_all() -> None:
"""清空所有操作日志"""
async with async_db_session.begin() as db:
count = await opera_log_dao.delete_all(db)
return count
await opera_log_dao.delete_all(db)
opera_log_service: OperaLogService = OperaLogService()
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import json
import os
import shutil
import zipfile
from typing import Any
from fastapi import UploadFile
from backend.common.enums import PluginType, StatusType
from backend.common.exception import errors
from backend.core.conf import settings
from backend.core.path_conf import PLUGIN_DIR
from backend.database.redis import redis_client
from backend.plugin.tools import uninstall_requirements_async
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
from backend.utils.timezone import timezone
class PluginService:
"""插件服务类"""
@staticmethod
async def get_all() -> list[dict[str, Any]]:
"""获取所有插件"""
keys = []
result = []
async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*'):
keys.append(key)
for info in await redis_client.mget(*keys):
result.append(json.loads(info))
return result
@staticmethod
async def changed() -> str | None:
"""检查插件是否发生变更"""
return await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
@staticmethod
async def install(*, type: PluginType, file: UploadFile | None = None, repo_url: str | None = None) -> str:
"""
安装插件
:param type: 插件类型
:param file: 插件 zip 压缩包
:param repo_url: git 仓库地址
:return:
"""
if type == PluginType.zip:
if not file:
raise errors.RequestError(msg='ZIP 压缩包不能为空')
return await install_zip_plugin(file)
if not repo_url:
raise errors.RequestError(msg='Git 仓库地址不能为空')
return await install_git_plugin(repo_url)
@staticmethod
async def uninstall(*, plugin: str):
"""
卸载插件
:param plugin: 插件名称
:return:
"""
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
if not os.path.exists(plugin_dir):
raise errors.NotFoundError(msg='插件不存在')
await uninstall_requirements_async(plugin)
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
shutil.move(plugin_dir, bacup_dir)
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
@staticmethod
async def update_status(*, plugin: str):
"""
更新插件状态
:param plugin: 插件名称
:return:
"""
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
if not plugin_info:
raise errors.NotFoundError(msg='插件不存在')
plugin_info = json.loads(plugin_info)
# 更新持久缓存状态
new_status = (
str(StatusType.enable.value)
if plugin_info['plugin']['enable'] == str(StatusType.disable.value)
else str(StatusType.disable.value)
)
plugin_info['plugin']['enable'] = new_status
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(plugin_info, ensure_ascii=False))
@staticmethod
async def build(*, plugin: str) -> io.BytesIO:
"""
打包插件为 zip 压缩包
:param plugin: 插件名称
:return:
"""
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
if not os.path.exists(plugin_dir):
raise errors.NotFoundError(msg='插件不存在')
bio = io.BytesIO()
with zipfile.ZipFile(bio, 'w') as zf:
for root, dirs, files in os.walk(plugin_dir):
dirs[:] = [d for d in dirs if d != '__pycache__']
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, start=plugin_dir)
zf.write(file_path, os.path.join(plugin, arcname))
bio.seek(0)
return bio
plugin_service: PluginService = PluginService()
+107 -28
View File
@@ -1,29 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from typing import Any, Sequence
from fastapi import Request
from sqlalchemy import Select
from backend.app.admin.crud.crud_data_rule import data_rule_dao
from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import Role
from backend.app.admin.schema.role import (
CreateRoleParam,
DeleteRoleParam,
UpdateRoleMenuParam,
UpdateRoleParam,
UpdateRoleRuleParam,
UpdateRoleScopeParam,
)
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data
class RoleService:
"""角色服务类"""
@staticmethod
async def get(*, pk: int) -> Role:
"""
获取角色详情
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
@@ -32,43 +41,96 @@ class RoleService:
@staticmethod
async def get_all() -> Sequence[Role]:
"""获取所有角色"""
async with async_db_session() as db:
roles = await role_dao.get_all(db)
return roles
@staticmethod
async def get_by_user(*, pk: int) -> Sequence[Role]:
async with async_db_session() as db:
roles = await role_dao.get_by_user(db, user_id=pk)
return roles
async def get_select(*, name: str | None, status: int | None) -> Select:
"""
获取角色列表查询条件
@staticmethod
async def get_select(*, name: str = None, status: int = None) -> Select:
:param name: 角色名称
:param status: 状态
:return:
"""
return await role_dao.get_list(name=name, status=status)
@staticmethod
async def get_menu_tree(*, pk: int) -> list[dict[str, Any] | None]:
"""
获取角色的菜单树形结构
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
menu_tree = get_tree_data(role.menus) if role.menus else []
return menu_tree
@staticmethod
async def get_scopes(*, pk: int) -> list[int]:
"""
获取角色数据范围列表
:param pk:
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
scope_ids = [scope.id for scope in role.scopes]
return scope_ids
@staticmethod
async def create(*, obj: CreateRoleParam) -> None:
"""
创建角色
:param obj: 角色创建参数
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get_by_name(db, obj.name)
if role:
raise errors.ForbiddenError(msg='角色已存在')
raise errors.ConflictError(msg='角色已存在')
await role_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateRoleParam) -> int:
"""
更新角色
:param pk: 角色 ID
:param obj: 角色更新参数
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
if role.name != obj.name:
role = await role_dao.get_by_name(db, obj.name)
if role:
raise errors.ForbiddenError(msg='角色已存在')
if await role_dao.get_by_name(db, obj.name):
raise errors.ConflictError(msg='角色已存在')
count = await role_dao.update(db, pk, obj)
for user in await role.awaitable_attrs.users:
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_role_menu(*, request: Request, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
async def update_role_menu(*, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
"""
更新角色菜单
:param pk: 角色 ID
:param menu_ids: 菜单 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
@@ -78,30 +140,47 @@ class RoleService:
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
count = await role_dao.update_menus(db, pk, menu_ids)
if pk in [role.id for role in request.user.roles]:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
for user in await role.awaitable_attrs.users:
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_role_rule(*, request: Request, pk: int, rule_ids: UpdateRoleRuleParam) -> int:
async def update_role_scope(*, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
"""
更新角色数据范围
:param pk: 角色 ID
:param scope_ids: 权限规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
for rule_id in rule_ids.rules:
rule = await data_rule_dao.get(db, rule_id)
if not rule:
raise errors.NotFoundError(msg='数据权限不存在')
count = await role_dao.update_rules(db, pk, rule_ids)
if pk in [role.id for role in request.user.roles]:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
for scope_id in scope_ids.scopes:
scope = await data_scope_dao.get(db, scope_id)
if not scope:
raise errors.NotFoundError(msg='数据范围不存在')
count = await role_dao.update_scopes(db, pk, scope_ids)
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def delete(*, request: Request, pk: list[int]) -> int:
async def delete(*, obj: DeleteRoleParam) -> int:
"""
批量删除角色
:param obj: 角色 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await role_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
count = await role_dao.delete(db, obj.pks)
for pk in obj.pks:
role = await role_dao.get(db, pk)
if role:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
+270 -199
View File
@@ -2,246 +2,317 @@
# -*- coding: utf-8 -*-
import random
from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.crud.crud_user import user_dao
from backend.app.admin.model import User
from backend.app.admin.model import Role, User
from backend.app.admin.schema.user import (
AddUserParam,
AvatarParam,
RegisterUserParam,
ResetPasswordParam,
UpdateUserParam,
UpdateUserRoleParam,
)
from backend.common.enums import UserPermissionType
from backend.common.exception import errors
from backend.common.security.jwt import get_hash_password, get_token, jwt_decode, password_verify, superuser_verify
from backend.common.response.response_code import CustomErrorCode
from backend.common.security.jwt import get_token, jwt_decode, password_verify, superuser_verify
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
class UserService:
@staticmethod
async def register(*, obj: RegisterUserParam) -> None:
async with async_db_session.begin() as db:
if not obj.password:
raise errors.ForbiddenError(msg='密码为空')
username = await user_dao.get_by_username(db, obj.username)
if username:
raise errors.ForbiddenError(msg='用户已注册')
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(10000, 88888)}'
nickname = await user_dao.get_by_nickname(db, obj.nickname)
if nickname:
raise errors.ForbiddenError(msg='昵称已注册')
email = await user_dao.check_email(db, obj.email)
if email:
raise errors.ForbiddenError(msg='邮箱已注册')
await user_dao.create(db, obj)
"""用户服务类"""
@staticmethod
async def add(*, request: Request, obj: AddUserParam) -> None:
async def get_userinfo(*, pk: int | None = None, username: str | None = None) -> User:
"""
获取用户信息
:param pk: 用户 ID
:param username: 用户名
:return:
"""
async with async_db_session() as db:
user = await user_dao.get_with_relation(db, user_id=pk, username=username)
if not user:
raise errors.NotFoundError(msg='用户不存在')
return user
@staticmethod
async def get_roles(*, pk: int) -> Sequence[Role]:
"""
获取用户所有角色
:param pk: 用户 ID
:return:
"""
async with async_db_session() as db:
user = await user_dao.get_with_relation(db, user_id=pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
return user.roles
@staticmethod
async def get_select(*, dept: int, username: str, phone: str, status: int) -> 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)
@staticmethod
async def create(*, request: Request, obj: AddUserParam) -> None:
"""
创建用户
:param request: FastAPI 请求对象
:param obj: 用户添加参数
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
username = await user_dao.get_by_username(db, obj.username)
if username:
raise errors.ForbiddenError(msg='用户已注册')
if await user_dao.get_by_username(db, obj.username):
raise errors.ConflictError(msg='用户名已注册')
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(88888, 99999)}'
nickname = await user_dao.get_by_nickname(db, obj.nickname)
if nickname:
raise errors.ForbiddenError(msg='昵称已注册')
if not obj.password:
raise errors.ForbiddenError(msg='密码为空')
email = await user_dao.check_email(db, obj.email)
if email:
raise errors.ForbiddenError(msg='邮箱已注册')
dept = await dept_dao.get(db, obj.dept_id)
if not dept:
raise errors.RequestError(msg='密码不允许为空')
if not await dept_dao.get(db, obj.dept_id):
raise errors.NotFoundError(msg='部门不存在')
for role_id in obj.roles:
role = await role_dao.get(db, role_id)
if not role:
if not await role_dao.get(db, role_id):
raise errors.NotFoundError(msg='角色不存在')
await user_dao.add(db, obj)
@staticmethod
async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int:
async def update(*, request: Request, pk: int, obj: UpdateUserParam) -> int:
"""
更新用户信息
:param request: FastAPI 请求对象
:param pk: 用户 ID
:param obj: 用户更新参数
:return:
"""
async with async_db_session.begin() as db:
user = await user_dao.get(db, request.user.id)
if not password_verify(obj.old_password, user.password):
raise errors.ForbiddenError(msg='原密码错误')
np1 = obj.new_password
np2 = obj.confirm_password
if np1 != np2:
raise errors.ForbiddenError(msg='密码输入不一致')
new_pwd = get_hash_password(obj.new_password, user.salt)
count = await user_dao.reset_password(db, request.user.id, new_pwd)
superuser_verify(request)
user = await user_dao.get_with_relation(db, user_id=pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if obj.username != user.username:
if await user_dao.get_by_username(db, obj.username):
raise errors.ConflictError(msg='用户名已注册')
for role_id in obj.roles:
if not await role_dao.get(db, role_id):
raise errors.NotFoundError(msg='角色不存在')
count = await user_dao.update(db, user, obj)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_permission(*, request: Request, pk: int, type: UserPermissionType) -> int:
"""
更新用户权限
:param request: FastAPI 请求对象
:param pk: 用户 ID
:param type: 权限类型
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
match type:
case UserPermissionType.superuser:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if pk == request.user.id:
raise errors.ForbiddenError(msg='禁止修改自身权限')
count = await user_dao.set_super(db, pk, not user.status)
case UserPermissionType.staff:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if pk == request.user.id:
raise errors.ForbiddenError(msg='禁止修改自身权限')
count = await user_dao.set_staff(db, pk, not user.is_staff)
case UserPermissionType.status:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if pk == request.user.id:
raise errors.ForbiddenError(msg='禁止修改自身权限')
count = await user_dao.set_status(db, pk, 0 if user.status == 1 else 1)
case UserPermissionType.multi_login:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
multi_login = user.is_multi_login if pk != user.id else request.user.is_multi_login
new_multi_login = not multi_login
count = await user_dao.set_multi_login(db, pk, new_multi_login)
token = get_token(request)
token_payload = jwt_decode(token)
if pk == user.id:
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
if not new_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
await redis_client.delete_prefix(
key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}'
)
else:
# 系统管理员修改他人时,他人 token 全部失效
if not new_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
await redis_client.delete_prefix(key_prefix)
case _:
raise errors.RequestError(msg='权限类型不存在')
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def reset_password(*, request: Request, pk: int, password: str) -> int:
"""
重置用户密码
:param request: FastAPI 请求对象
:param pk: 用户 ID
:param password: 新密码
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.reset_password(db, user.id, password)
key_prefix = [
f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}',
f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}',
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
]
for prefix in key_prefix:
await redis_client.delete(prefix)
return count
@staticmethod
async def update_nickname(*, request: Request, nickname: str) -> int:
"""
更新当前用户昵称
:param request: FastAPI 请求对象
:param nickname: 用户昵称
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.update_nickname(db, token_payload.id, nickname)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_avatar(*, request: Request, avatar: str) -> int:
"""
更新当前用户头像
:param request: FastAPI 请求对象
:param avatar: 头像地址
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.update_avatar(db, token_payload.id, avatar)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_email(*, request: Request, captcha: str, email: str) -> int:
"""
更新当前用户邮箱
:param request: FastAPI 请求对象
:param captcha: 邮箱验证码
:param email: 邮箱
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
if not captcha_code:
raise errors.RequestError(msg='验证码已失效,请重新获取')
if captcha != captcha_code:
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
count = await user_dao.update_email(db, token_payload.id, email)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_password(*, request: Request, obj: ResetPasswordParam) -> int:
"""
更新当前用户密码
:param request: FastAPI 请求对象
:param obj: 密码重置参数
:return:
"""
async with async_db_session.begin() as db:
token = get_token(request)
token_payload = jwt_decode(token)
user = await user_dao.get(db, token_payload.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if not password_verify(obj.old_password, user.password):
raise errors.RequestError(msg='原密码错误')
if obj.new_password != obj.confirm_password:
raise errors.RequestError(msg='密码输入不一致')
count = await user_dao.reset_password(db, user.id, obj.new_password)
key_prefix = [
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
]
for prefix in key_prefix:
await redis_client.delete_prefix(prefix)
return count
@staticmethod
async def get_userinfo(*, username: str) -> User:
async with async_db_session() as db:
user = await user_dao.get_with_relation(db, username=username)
async def delete(*, pk: int) -> int:
"""
删除用户
:param pk: 用户 ID
:return:
"""
async with async_db_session.begin() as db:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
return user
@staticmethod
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
async with async_db_session.begin() as db:
if not request.user.is_superuser:
if request.user.username != username:
raise errors.ForbiddenError(msg='你只能修改自己的信息')
input_user = await user_dao.get_with_relation(db, username=username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
if input_user.username != obj.username:
_username = await user_dao.get_by_username(db, obj.username)
if _username:
raise errors.ForbiddenError(msg='用户名已注册')
if input_user.nickname != obj.nickname:
nickname = await user_dao.get_by_nickname(db, obj.nickname)
if nickname:
raise errors.ForbiddenError(msg='昵称已注册')
if input_user.email != obj.email:
email = await user_dao.check_email(db, obj.email)
if email:
raise errors.ForbiddenError(msg='邮箱已注册')
count = await user_dao.update_userinfo(db, input_user.id, obj)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
return count
@staticmethod
async def update_roles(*, request: Request, username: str, obj: UpdateUserRoleParam) -> None:
async with async_db_session.begin() as db:
if not request.user.is_superuser:
if request.user.username != username:
raise errors.AuthorizationError
input_user = await user_dao.get_with_relation(db, username=username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
for role_id in obj.roles:
role = await role_dao.get(db, role_id)
if not role:
raise errors.NotFoundError(msg='角色不存在')
await user_dao.update_role(db, input_user, obj)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{input_user.id}')
@staticmethod
async def update_avatar(*, request: Request, username: str, avatar: AvatarParam) -> int:
async with async_db_session.begin() as db:
if not request.user.is_superuser:
if request.user.username != username:
raise errors.AuthorizationError
input_user = await user_dao.get_by_username(db, username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.update_avatar(db, input_user.id, avatar)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
return count
@staticmethod
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select:
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
@staticmethod
async def update_permission(*, request: Request, pk: int) -> int:
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
raise errors.NotFoundError(msg='用户不存在')
else:
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
super_status = await user_dao.get_super(db, pk)
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}')
return count
@staticmethod
async def update_staff(*, request: Request, pk: int) -> int:
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
raise errors.NotFoundError(msg='用户不存在')
else:
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
staff_status = await user_dao.get_staff(db, pk)
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}')
return count
@staticmethod
async def update_status(*, request: Request, pk: int) -> int:
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
raise errors.NotFoundError(msg='用户不存在')
else:
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
status = await user_dao.get_status(db, pk)
count = await user_dao.set_status(db, pk, False if status else True)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
@staticmethod
async def update_multi_login(*, request: Request, pk: int) -> int:
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
raise errors.NotFoundError(msg='用户不存在')
else:
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, False if multi_login else True)
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:
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
async def delete(*, username: str) -> int:
async with async_db_session.begin() as db:
input_user = await user_dao.get_by_username(db, username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.delete(db, input_user.id)
count = await user_dao.delete(db, user.id)
key_prefix = [
f'{settings.TOKEN_REDIS_PREFIX}:{input_user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{input_user.id}',
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
]
for key in key_prefix:
await redis_client.delete_prefix(key)
+4 -1
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio.session import AsyncSession
from backend.database.db import create_async_engine_and_session, create_database_url
@@ -8,7 +11,7 @@ TEST_SQLALCHEMY_DATABASE_URL = create_database_url(unittest=True)
_, async_test_db_session = create_async_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL)
async def override_get_db():
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
"""session 生成器"""
async with async_test_db_session() as session:
yield session
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.generator.api.v1.gen import router as gen_router
from backend.app.generator.api.v1.gen_business import router as gen_business_router
from backend.app.generator.api.v1.gen_model import router as gen_model_router
from backend.core.conf import settings
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(gen_business_router, prefix='/businesses')
v1.include_router(gen_model_router, prefix='/models')
@@ -1,86 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.app.generator.schema.gen_model import GetGenModelDetail
from backend.app.generator.service.gen_business_service import gen_business_service
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
businesses = await gen_business_service.get_all()
data = select_list_serialize(businesses)
return response_base.success(data=data)
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
async def get_business(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetGenBusinessDetail]:
business = await gen_business_service.get(pk=pk)
data = GetGenBusinessDetail(**select_as_dict(business))
return response_base.success(data=data)
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
async def get_business_all_models(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetGenModelDetail]]:
models = await gen_model_service.get_by_business(business_id=pk)
data = select_list_serialize(models)
return response_base.success(data=data)
@router.post(
'',
summary='创建代码生成业务',
deprecated=True,
dependencies=[
Depends(RequestPermission('gen:code:business:add')),
DependsRBAC,
],
)
async def create_business(obj: CreateGenBusinessParam) -> ResponseModel:
await gen_business_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新代码生成业务',
dependencies=[
Depends(RequestPermission('gen:code:business:edit')),
DependsRBAC,
],
)
async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessParam) -> ResponseModel:
count = await gen_business_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'/{pk}',
summary='删除代码生成业务',
dependencies=[
Depends(RequestPermission('gen:code:business:del')),
DependsRBAC,
],
)
async def delete_business(pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await gen_business_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_model import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/types', summary='获取代码生成模型列类型', dependencies=[DependsJwtAuth])
async def get_model_types() -> ResponseSchemaModel[list[str]]:
model_types = await gen_model_service.get_types()
return response_base.success(data=model_types)
@router.get('/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth])
async def get_model(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetGenModelDetail]:
model = await gen_model_service.get(pk=pk)
data = GetGenModelDetail(**select_as_dict(model))
return response_base.success(data=data)
@router.post(
'',
summary='创建代码生成模型',
dependencies=[
Depends(RequestPermission('gen:code:model:add')),
DependsRBAC,
],
)
async def create_model(obj: CreateGenModelParam) -> ResponseModel:
await gen_model_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新代码生成模型',
dependencies=[
Depends(RequestPermission('gen:code:model:edit')),
DependsRBAC,
],
)
async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam) -> ResponseModel:
count = await gen_model_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'/{pk}',
summary='删除代码生成模型',
dependencies=[
Depends(RequestPermission('gen:code:model:del')),
DependsRBAC,
],
)
async def delete_model(pk: Annotated[int, Path(...)]) -> ResponseModel:
count = await gen_model_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings):
"""Admin Settings"""
# 模版目录
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
# 代码下载
DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
@lru_cache
def get_generator_settings() -> GeneratorSettings:
"""获取 generator 配置"""
return GeneratorSettings()
generator_settings = get_generator_settings()
@@ -1,73 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
async def get(self, db: AsyncSession, pk: int) -> GenBusiness | None:
"""
获取代码生成业务表
:param db:
:param pk:
:return:
"""
return await self.select_model(db, pk)
async def get_by_name(self, db: AsyncSession, name: str) -> GenBusiness | None:
"""
通过 name 获取代码生成业务表
:param db:
:param name:
:return:
"""
return await self.select_model_by_column(db, table_name_en=name)
async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]:
"""
获取所有代码生成业务表
:return:
"""
return await self.select_models(db)
async def create(self, db: AsyncSession, obj_in: CreateGenBusinessParam) -> None:
"""
创建代码生成业务表
:param db:
:param obj_in:
:return:
"""
await self.create_model(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenBusinessParam) -> int:
"""
更新代码生成业务表
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_model(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除代码生成业务表
:param db:
:param pk:
:return:
"""
return await self.delete_model(db, pk)
gen_business_dao: CRUDGenBusiness = CRUDGenBusiness(GenBusiness)
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenModel
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
class CRUDGenModel(CRUDPlus[GenModel]):
async def get(self, db: AsyncSession, pk: int) -> GenModel | None:
"""
获取代码生成模型列
:return:
"""
return await self.select_model(db, pk)
async def get_all_by_business_id(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
"""
获取所有代码生成模型列
:param db:
:param business_id:
:return:
"""
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:
"""
创建代码生成模型表
:param db:
:param obj_in:
:param pd_type:
:return:
"""
await self.create_model(db, obj_in, pd_type=pd_type)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenModelParam, pd_type: str | None = None) -> int:
"""
更细代码生成模型表
:param db:
:param pk:
:param obj_in:
:param pd_type:
:return:
"""
return await self.update_model(db, pk, obj_in, pd_type=pd_type)
async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除代码生成模型表
:param db:
:param pk:
:return:
"""
return await self.delete_model(db, pk)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenModel)
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.generator.model.gen_business import GenBusiness
from backend.app.generator.model.gen_model import GenModel
@@ -1,45 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field, model_validator
from typing_extensions import Self
from backend.app.generator.schema.gen_model import GetGenModelDetail
from backend.common.schema import SchemaBase
class GenBusinessSchemaBase(SchemaBase):
app_name: str
table_name_en: str
table_name_zh: str
table_simple_name_zh: str
table_comment: str | None = None
schema_name: str | None = None
default_datetime_column: bool = Field(default=True)
api_version: str = Field(default='v1')
gen_path: str | None = None
remark: str | None = None
@model_validator(mode='after')
def check_schema_name(self) -> Self:
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase):
pass
class UpdateGenBusinessParam(GenBusinessSchemaBase):
pass
class GetGenBusinessDetail(GenBusinessSchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
gen_model: list[GetGenModelDetail] | None = None
-38
View File
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pydantic import ConfigDict, Field, field_validator
from backend.common.schema import SchemaBase
from backend.utils.type_conversion import sql_type_to_sqlalchemy
class GenModelSchemaBase(SchemaBase):
name: str
comment: str | None = None
type: str
default: str | None = None
sort: int
length: int
is_pk: bool = Field(default=False)
is_nullable: bool = Field(default=False)
gen_business_id: int | None = Field(ge=1)
@field_validator('type')
@classmethod
def type_update(cls, v):
return sql_type_to_sqlalchemy(v)
class CreateGenModelParam(GenModelSchemaBase):
pass
class UpdateGenModelParam(GenModelSchemaBase):
pass
class GetGenModelDetail(GenModelSchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int
pd_type: str
@@ -1,48 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
from backend.common.exception import errors
from backend.database.db import async_db_session
class GenBusinessService:
@staticmethod
async def get(*, pk: int) -> GenBusiness:
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='代码生成业务不存在')
return business
@staticmethod
async def get_all() -> Sequence[GenBusiness]:
async with async_db_session() as db:
businesses = await gen_business_dao.get_all(db)
return businesses
@staticmethod
async def create(*, obj: CreateGenBusinessParam) -> None:
async with async_db_session.begin() as db:
business = await gen_business_dao.get_by_name(db, obj.table_name_en)
if business:
raise errors.ForbiddenError(msg='代码生成业务已存在')
await gen_business_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateGenBusinessParam) -> int:
async with async_db_session.begin() as db:
count = await gen_business_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: int) -> int:
async with async_db_session.begin() as db:
count = await gen_business_dao.delete(db, pk)
return count
gen_business_service: GenBusinessService = GenBusinessService()

Some files were not shown because too many files have changed in this diff Show More