mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 05:02:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36dcb3f5c | ||
|
|
63d088d62c | ||
|
|
a461f78224 | ||
|
|
c306432708 | ||
|
|
97f778cc90 | ||
|
|
88695ac6ad | ||
|
|
3b04329b04 | ||
|
|
bd4acf8121 | ||
|
|
b9a9b1efe6 | ||
|
|
c050f58ee9 | ||
|
|
d47375ae41 | ||
|
|
0602c6144d | ||
|
|
c84f0658fb | ||
|
|
a2902bd23a | ||
|
|
69a9b90711 | ||
|
|
b96402e11d | ||
|
|
0bc6f6a719 | ||
|
|
ebd65c8246 | ||
|
|
234bf708b3 | ||
|
|
6d5e741d94 | ||
|
|
f9bfe8f510 | ||
|
|
408c866dda | ||
|
|
bac41a46f8 | ||
|
|
8638c26db1 | ||
|
|
319ba13df1 | ||
|
|
0d1f05d307 | ||
|
|
e6608d18ce | ||
|
|
7afd8415cd | ||
|
|
845f2f0ff8 | ||
|
|
11d7792c0f | ||
|
|
6883ec34c6 | ||
|
|
4c45e7ff27 | ||
|
|
5621daea98 | ||
|
|
71d168dfea | ||
|
|
2d4365189d | ||
|
|
0c28a46e60 | ||
|
|
a4c7dae99c | ||
|
|
bfda0a226c | ||
|
|
c9bbb105a3 | ||
|
|
b415dc8912 | ||
|
|
05b74d71a3 | ||
|
|
10c0c691a7 | ||
|
|
0bc3786fc6 | ||
|
|
8e041ccb1b | ||
|
|
a82e9b7b9d | ||
|
|
7bab8cc636 | ||
|
|
7659b63d88 | ||
|
|
33ebc5723f | ||
|
|
86d9efd785 | ||
|
|
57ae7f03a6 | ||
|
|
6ee3983846 | ||
|
|
f05ed5cdda | ||
|
|
187dfa5344 | ||
|
|
918c8a9d33 | ||
|
|
ccfe35ea85 | ||
|
|
4a118c166a | ||
|
|
5f3823cb6d | ||
|
|
19ef52da9b | ||
|
|
39c3339a8d | ||
|
|
9f52629dcb | ||
|
|
1cddd6e2d5 | ||
|
|
ed10e6ff47 | ||
|
|
48574fb50a | ||
|
|
ee7fe3087d | ||
|
|
270ac3d941 | ||
|
|
2f76988076 | ||
|
|
b275481742 | ||
|
|
f3f1ceb595 | ||
|
|
bcc764142b | ||
|
|
9b0d9f8296 | ||
|
|
63ba7646c1 | ||
|
|
1a7734185f | ||
|
|
8541059d23 | ||
|
|
61edc81333 | ||
|
|
8998687adb | ||
|
|
85b0294da9 | ||
|
|
a8ecb4ae3b | ||
|
|
a87ff74b56 |
+170
@@ -1,3 +1,165 @@
|
||||
<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
|
||||
|
||||
@@ -494,6 +656,14 @@
|
||||
[Changes][v1.0.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
|
||||
|
||||
+8
-14
@@ -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,6 +10,10 @@ 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 \
|
||||
@@ -19,35 +22,27 @@ ENV UV_COMPILE_BYTECODE=1 \
|
||||
|
||||
# Install dependencies with cache
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-default-groups --group server
|
||||
|
||||
# === Runtime base server image ===
|
||||
FROM python:3.10-slim AS base_server
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . /fba
|
||||
COPY --from=builder /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
|
||||
|
||||
# === 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
|
||||
@@ -59,9 +54,8 @@ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port","8000"]
|
||||
# === Celery server image ===
|
||||
FROM base_server AS celery
|
||||
|
||||
WORKDIR /fba/backend/
|
||||
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
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
|
||||
# FastAPI Best Architecture
|
||||
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
Enterprise-level backend architecture solution
|
||||
|
||||
**🔥Continuously updated and maintained🔥**
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||
@@ -21,12 +19,10 @@ Enterprise-level backend architecture solution
|
||||

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

|
||||
[](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
|
||||
@@ -45,43 +41,7 @@ pattern, use templates to transform it to your heart's content!
|
||||
| data access | dao / mapper | crud |
|
||||
| model | model / entity | model |
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Global FastAPI PEP 593 Annotated parameter style
|
||||
- [x] Comprehensive async/await + asgiref asynchronous design
|
||||
- [x] Adheres to RESTful API specifications
|
||||
- [x] Uses SQLAlchemy 2.0 with new syntax
|
||||
- [x] Uses Pydantic v2 version
|
||||
- [x] Implements role-menu RBAC access control
|
||||
- [x] Integrates Casbin RBAC access control
|
||||
- [x] Supports Celery asynchronous tasks
|
||||
- [x] Custom-developed JWT authentication middleware
|
||||
- [x] Supports global custom time zones
|
||||
- [x] Supports Docker / Docker-compose deployment
|
||||
- [x] Integrates Pytest unit testing
|
||||
|
||||
## Built-in Functions
|
||||
|
||||
- [x] User Management: Assign roles and permissions
|
||||
- [x] Department Management: Configure organizational structure (company, department, team, etc.)
|
||||
- [x] Menu Management: Set up menus and button-level permissions
|
||||
- [x] Role Management: Configure roles, assign menus and permissions
|
||||
- [x] Dictionary Management: Maintain common parameters and configurations
|
||||
- [x] Parameter Management: Dynamically configure commonly used system parameters
|
||||
- [x] Notification Announcements: Publish and maintain system notification and announcement information
|
||||
- [x] Token Management: Detect online status, support forced logout
|
||||
- [x] Multi-device Login: Support one-click switching between multi-device login modes
|
||||
- [x] OAuth 2.0: Built-in custom-developed OAuth 2.0 authorization login
|
||||
- [x] Plugin System: Hot-swappable plugin design to reduce coupling
|
||||
- [x] Scheduled Tasks: Support scheduled, asynchronous tasks, and function calls
|
||||
- [x] Code Generation: Automatically generate code with preview, write, and download support
|
||||
- [x] Operation Logs: Record and query normal and abnormal operations
|
||||
- [x] Login Logs: Record and query normal and abnormal logins
|
||||
- [x] Cache Monitoring: Query system cache information and command statistics
|
||||
- [x] Service Monitoring: View server hardware information and status
|
||||
- [x] API Documentation: Automatically generate online interactive API documentation
|
||||
|
||||
## Development and deployment
|
||||
## Help
|
||||
|
||||
For more details, please check
|
||||
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||
|
||||
+3
-43
@@ -4,11 +4,9 @@
|
||||
|
||||
# FastAPI Best Architecture
|
||||
|
||||
简体中文 | [English](./README.md)
|
||||
|
||||
企业级后端架构解决方案
|
||||
|
||||
**🔥持续更新维护中🔥**
|
||||
简体中文 | [English](./README.md)
|
||||
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||
@@ -21,12 +19,10 @@
|
||||

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

|
||||
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> 此仓库作为模板库公开,任何人或企业均可免费使用!
|
||||
|
||||
## 伪三层架构
|
||||
|
||||
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
|
||||
@@ -43,43 +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 v2 版本
|
||||
- [x] 实现角色菜单 RBAC 访问控制
|
||||
- [x] 集成 Casbin RBAC 访问控制
|
||||
- [x] 支持 Celery 异步任务
|
||||
- [x] 自研 JWT 认证中间件
|
||||
- [x] 支持全局自定义时间时区
|
||||
- [x] 支持 Docker / Docker-compose 部署
|
||||
- [x] 集成 Pytest 单元测试
|
||||
|
||||
## 内置功能
|
||||
|
||||
- [x] 用户管理:分配角色和权限
|
||||
- [x] 部门管理:配置组织架构(公司、部门、小组等)
|
||||
- [x] 菜单管理:设置菜单及按钮级权限
|
||||
- [x] 角色管理:配置角色、分配菜单和权限
|
||||
- [x] 字典管理:维护常用参数和配置
|
||||
- [x] 参数管理:系统常用参数动态配置
|
||||
- [x] 通知公告:发布和维护系统通知公告信息
|
||||
- [x] 令牌管理:检测在线状态,支持强制下线
|
||||
- [x] 多端登录:支持一键切换多端登录模式
|
||||
- [x] OAuth 2.0:内置自研 OAuth 2.0 授权登录
|
||||
- [x] 插件系统:热插拔插件设计,降低耦合
|
||||
- [x] 定时任务:支持定时,异步任务及函数调用
|
||||
- [x] 代码生成:自动生成代码,支持预览、写入和下载
|
||||
- [x] 操作日志:记录和查询正常和异常操作
|
||||
- [x] 登录日志:记录和查询正常和异常登录
|
||||
- [x] 缓存监控:查询系统缓存信息和命令统计
|
||||
- [x] 服务监控:查看服务器硬件信息和状态
|
||||
- [x] 接口文档:自动生成在线交互式 API 文档
|
||||
|
||||
## 开发部署
|
||||
## 帮助
|
||||
|
||||
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||
|
||||
|
||||
+1
-1
@@ -72,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
|
||||
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend.utils.console import console
|
||||
|
||||
__version__ = '1.6.0'
|
||||
|
||||
|
||||
def get_version() -> str | None:
|
||||
console.print(f'[cyan]{__version__}[/]')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=['验证码'])
|
||||
|
||||
@@ -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) -> 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:
|
||||
async def logout(request: Request, response: Response) -> ResponseModel:
|
||||
await auth_service.logout(request=request, response=response)
|
||||
return response_base.success()
|
||||
|
||||
@@ -14,7 +14,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
'/captcha',
|
||||
summary='获取登录验证码',
|
||||
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@ router = APIRouter()
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_login_logs(
|
||||
async def get_login_logs_paged(
|
||||
db: CurrentSession,
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
@@ -43,8 +43,8 @@ async def get_pagination_login_logs(
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_login_log(pk: Annotated[list[int], Query(description='登录日志 ID 列表')]) -> 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,7 +54,7 @@ async def delete_login_log(pk: Annotated[list[int], Query(description='登录日
|
||||
'/all',
|
||||
summary='清空登录日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:login:empty')),
|
||||
Depends(RequestPermission('log:login:clear')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@ router = APIRouter()
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_opera_logs(
|
||||
async def get_opera_logs_paged(
|
||||
db: CurrentSession,
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
@@ -43,8 +43,8 @@ async def get_pagination_opera_logs(
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_opera_log(pk: Annotated[list[int], Query(description='操作日志 ID 列表')]) -> 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,7 +54,7 @@ async def delete_opera_log(pk: Annotated[list[int], Query(description='操作日
|
||||
'/all',
|
||||
summary='清空操作日志',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('log:opera:empty')),
|
||||
Depends(RequestPermission('log:opera:clear')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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=['会话监控'])
|
||||
|
||||
+13
-10
@@ -6,7 +6,7 @@ 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, revoke_token, superuser_verify
|
||||
@@ -18,8 +18,8 @@ from backend.database.redis import redis_client
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
|
||||
async def get_tokens(
|
||||
@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}:*')
|
||||
@@ -44,9 +44,10 @@ async def get_tokens(
|
||||
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='未知',
|
||||
@@ -58,7 +59,7 @@ async def get_tokens(
|
||||
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:
|
||||
extra_info = json.loads(extra_info)
|
||||
# 排除 swagger 登录生成的 token
|
||||
@@ -75,15 +76,17 @@ async def get_tokens(
|
||||
|
||||
@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(description='用户 ID')], obj: KickOutToken
|
||||
async def delete_session(
|
||||
request: Request,
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
session_uuid: Annotated[str, Query(description='会话 UUID')],
|
||||
) -> ResponseModel:
|
||||
superuser_verify(request)
|
||||
await revoke_token(str(pk), obj.session_uuid)
|
||||
await revoke_token(pk, session_uuid)
|
||||
return response_base.success()
|
||||
@@ -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'])
|
||||
@@ -5,11 +5,10 @@ from fastapi import APIRouter
|
||||
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
|
||||
from backend.app.admin.api.v1.sys.data_scope import router as data_scope_router
|
||||
from backend.app.admin.api.v1.sys.dept import router as dept_router
|
||||
from backend.app.admin.api.v1.sys.files import router as file_router
|
||||
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
||||
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
|
||||
from backend.app.admin.api.v1.sys.role import router as role_router
|
||||
from backend.app.admin.api.v1.sys.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')
|
||||
@@ -20,6 +19,5 @@ router.include_router(role_router, prefix='/roles', tags=['系统角色'])
|
||||
router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
||||
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
|
||||
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
|
||||
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
|
||||
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
|
||||
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
|
||||
router.include_router(file_router, prefix='/files', tags=['系统文件'])
|
||||
router.include_router(plugin_router, prefix='/plugins', tags=['系统插件'])
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.admin.schema.data_rule import (
|
||||
CreateDataRuleParam,
|
||||
DeleteDataRuleParam,
|
||||
GetDataRuleColumnDetail,
|
||||
GetDataRuleDetail,
|
||||
UpdateDataRuleParam,
|
||||
@@ -27,7 +28,7 @@ async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
||||
return response_base.success(data=models)
|
||||
|
||||
|
||||
@router.get('/model/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||
@router.get('/models/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||
async def get_data_rule_model_columns(
|
||||
model: Annotated[str, Path(description='模型名称')],
|
||||
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
||||
@@ -57,7 +58,7 @@ async def get_data_rule(
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_data_rules(
|
||||
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)
|
||||
@@ -103,8 +104,8 @@ async def update_data_rule(
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_data_rule(pk: Annotated[list[int], Query(description='数据规则 ID 列表')]) -> ResponseModel:
|
||||
count = await data_rule_service.delete(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()
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.admin.schema.data_scope import (
|
||||
CreateDataScopeParam,
|
||||
DeleteDataScopeParam,
|
||||
GetDataScopeDetail,
|
||||
GetDataScopeWithRelationDetail,
|
||||
UpdateDataScopeParam,
|
||||
@@ -22,6 +23,12 @@ 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')],
|
||||
@@ -46,7 +53,7 @@ async def get_data_scope_rules(
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_data_scopes(
|
||||
async def get_data_scopes_paged(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='范围名称')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
@@ -111,8 +118,8 @@ async def update_data_scope_rules(
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_data_scope(pk: Annotated[list[int], Query(description='数据范围 ID 列表')]) -> ResponseModel:
|
||||
count = await data_scope_service.delete(pk=pk)
|
||||
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()
|
||||
|
||||
@@ -20,15 +20,15 @@ async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> Respons
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
|
||||
async def get_all_depts(
|
||||
@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(request=request, 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)
|
||||
|
||||
|
||||
|
||||
@@ -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 file_verify, upload_file
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
'/upload',
|
||||
summary='文件上传',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:file:upload')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def upload_files(file: Annotated[UploadFile, File()]) -> ResponseSchemaModel[UploadUrl]:
|
||||
file_verify(file)
|
||||
filename = await upload_file(file)
|
||||
return response_base.success(data={'url': f'/static/upload/{filename}'})
|
||||
@@ -14,9 +14,9 @@ from backend.common.security.rbac import DependsRBAC
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
|
||||
async def get_user_sidebar(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)
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> Respons
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='获取所有菜单展示树', dependencies=[DependsJwtAuth])
|
||||
async def get_all_menus(
|
||||
@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)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
|
||||
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()
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated, Any
|
||||
|
||||
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.app.admin.service.plugin_service import plugin_service
|
||||
from backend.common.response.response_code import CustomResponseCode
|
||||
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
|
||||
@@ -22,36 +23,36 @@ async def get_all_plugins() -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||
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/zip',
|
||||
summary='安装 zip 插件',
|
||||
description='使用插件 zip 压缩包进行安装',
|
||||
'',
|
||||
summary='安装插件',
|
||||
description='使用插件 zip 压缩包或 git 仓库地址进行安装',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:install')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def install_zip_plugin(file: Annotated[UploadFile, File()]) -> ResponseModel:
|
||||
await plugin_service.install_zip(file=file)
|
||||
return response_base.success(res=CustomResponseCode.PLUGIN_INSTALL_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(
|
||||
'/install/git',
|
||||
summary='安装 git 插件',
|
||||
description='使用插件 git 仓库地址进行安装,不限制平台;如果需要凭证,需在 git 仓库地址中添加凭证信息',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:install')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def install_git_plugin(repo_url: Annotated[str, Query(description='插件 git 仓库地址')]) -> ResponseModel:
|
||||
await plugin_service.install_git(repo_url=repo_url)
|
||||
return response_base.success(res=CustomResponseCode.PLUGIN_INSTALL_SUCCESS)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/uninstall',
|
||||
@router.delete(
|
||||
'/{plugin}',
|
||||
summary='卸载插件',
|
||||
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
||||
dependencies=[
|
||||
@@ -59,35 +60,29 @@ async def install_git_plugin(repo_url: Annotated[str, Query(description='插件
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def uninstall_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> ResponseModel:
|
||||
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||
await plugin_service.uninstall(plugin=plugin)
|
||||
return response_base.success(res=CustomResponseCode.PLUGIN_UNINSTALL_SUCCESS)
|
||||
return response_base.success(
|
||||
res=CustomResponse(code=200, msg=f'插件 {plugin} 卸载成功,请根据插件说明(README.md)移除相关配置并重启服务')
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/status',
|
||||
@router.put(
|
||||
'/{plugin}/status',
|
||||
summary='更新插件状态',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:status')),
|
||||
Depends(RequestPermission('sys:plugin:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def update_plugin_status(plugin: Annotated[str, Query(description='插件名称')]) -> ResponseModel:
|
||||
async def update_plugin_status(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||
await plugin_service.update_status(plugin=plugin)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.post(
|
||||
'/zip',
|
||||
summary='打包插件',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:zip')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def build_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> StreamingResponse:
|
||||
@router.get('/{plugin}', summary='下载插件', dependencies=[DependsJwtAuth])
|
||||
async def download_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
|
||||
bio = await plugin_service.build(plugin=plugin)
|
||||
bio.seek(0)
|
||||
return StreamingResponse(
|
||||
bio,
|
||||
media_type='application/x-zip-compressed',
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
DeleteRoleParam,
|
||||
GetRoleDetail,
|
||||
GetRoleWithRelationDetail,
|
||||
UpdateRoleMenuParam,
|
||||
@@ -29,32 +30,22 @@ async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||
async def get_user_all_roles(
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
data = await role_service.get_users(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
||||
async def get_role_all_menus(
|
||||
@router.get('/{pk}/menus', summary='获取角色菜单树', dependencies=[DependsJwtAuth])
|
||||
async def get_role_menu_tree(
|
||||
pk: Annotated[int, Path(description='角色 ID')],
|
||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||
) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||
menu = await role_service.get_menu_tree(pk=pk)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
|
||||
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
|
||||
async def get_role_all_scopes(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
|
||||
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(description='角色 ID')],
|
||||
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
|
||||
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)
|
||||
|
||||
@@ -67,7 +58,7 @@ async def get_role(
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_roles(
|
||||
async def get_roles_paged(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='角色名称')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
@@ -106,7 +97,7 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}/menu',
|
||||
'/{pk}/menus',
|
||||
summary='更新角色菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:menu:edit')),
|
||||
@@ -123,7 +114,7 @@ async def update_role_menus(
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}/scope',
|
||||
'/{pk}/scopes',
|
||||
summary='更新角色数据范围',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:scope:edit')),
|
||||
@@ -147,8 +138,8 @@ async def update_role_scopes(
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_role(pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
|
||||
count = await role_service.delete(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()
|
||||
|
||||
@@ -1,27 +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}'})
|
||||
@@ -4,17 +4,16 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.role import GetRoleDetail
|
||||
from backend.app.admin.schema.user import (
|
||||
AddUserParam,
|
||||
AvatarParam,
|
||||
GetCurrentUserInfoWithRelationDetail,
|
||||
GetUserInfoWithRelationDetail,
|
||||
RegisterUserParam,
|
||||
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
|
||||
@@ -25,74 +24,24 @@ from backend.database.db import CurrentSession
|
||||
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[GetUserInfoWithRelationDetail]:
|
||||
await user_service.add(request=request, obj=obj)
|
||||
data = await user_service.get_userinfo(username=obj.username)
|
||||
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])
|
||||
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
|
||||
data = request.user.model_dump()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
|
||||
async def get_user(
|
||||
username: Annotated[str, Path(description='用户名')],
|
||||
@router.get('/{pk}', summary='获取用户信息', dependencies=[DependsJwtAuth])
|
||||
async def get_userinfo(
|
||||
pk: Annotated[int, Path(description='用户 ID')],
|
||||
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||
data = await user_service.get_userinfo(username=username)
|
||||
data = await user_service.get_userinfo(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
||||
async def update_user(
|
||||
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserParam
|
||||
) -> ResponseModel:
|
||||
count = await user_service.update(request=request, username=username, obj=obj)
|
||||
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(description='用户名')], 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(description='用户名')], 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('/{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.get(
|
||||
@@ -103,7 +52,7 @@ async def update_avatar(
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_users(
|
||||
async def get_users_paged(
|
||||
db: CurrentSession,
|
||||
dept: Annotated[int | None, Query(description='部门 ID')] = None,
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
@@ -115,49 +64,55 @@ async def get_pagination_users(
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
|
||||
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> 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=[DependsJwtAuth])
|
||||
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(description='用户 ID')]) -> 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(description='用户 ID')]) -> ResponseModel:
|
||||
count = await user_service.update_status(request=request, pk=pk)
|
||||
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(description='用户 ID')]) -> ResponseModel:
|
||||
count = await user_service.update_multi_login(request=request, pk=pk)
|
||||
@router.put('/{pk}/password', summary='重置用户密码', dependencies=[DependsJwtAuth])
|
||||
async def reset_user_password(
|
||||
pk: Annotated[int, Path(description='用户 ID')], obj: ResetPasswordParam
|
||||
) -> ResponseModel:
|
||||
count = await user_service.reset_pwd(pk=pk, obj=obj)
|
||||
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(description='用户名')]) -> 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()
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select, and_, 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
|
||||
@@ -31,16 +30,12 @@ class CRUDDataRule(CRUDPlus[DataRule]):
|
||||
:param name: 规则名称
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(noload(self.model.scope)).order_by(desc(self.model.created_time))
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
|
||||
|
||||
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
||||
"""
|
||||
@@ -82,15 +77,15 @@ class CRUDDataRule(CRUDPlus[DataRule]):
|
||||
"""
|
||||
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: 规则 ID 列表
|
||||
: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)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select, 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 DataRule, DataScope
|
||||
@@ -40,9 +41,16 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
||||
:param pk: 范围 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(selectinload(self.model.rules)).where(self.model.id == pk)
|
||||
data_scope = await db.execute(stmt)
|
||||
return data_scope.scalars().first()
|
||||
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:
|
||||
"""
|
||||
@@ -52,22 +60,14 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
||||
:param status: 范围状态
|
||||
:return:
|
||||
"""
|
||||
stmt = (
|
||||
select(self.model)
|
||||
.options(noload(self.model.rules), noload(self.model.roles))
|
||||
.order_by(desc(self.model.created_time))
|
||||
)
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
||||
"""
|
||||
@@ -105,15 +105,15 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
||||
current_data_scope.rules = rules.scalars().all()
|
||||
return len(current_data_scope.rules)
|
||||
|
||||
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
|
||||
async def delete(self, db: AsyncSession, pks: list[int]) -> int:
|
||||
"""
|
||||
删除数据范围
|
||||
批量删除数据范围
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 范围 ID 列表
|
||||
: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_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Dept
|
||||
@@ -56,16 +54,19 @@ class CRUDDept(CRUDPlus[Dept]):
|
||||
:param status: 部门状态
|
||||
:return:
|
||||
"""
|
||||
filters = {'del_flag__eq': 0}
|
||||
filters = {'del_flag': 0}
|
||||
|
||||
if name is not None:
|
||||
filters.update(name__like=f'%{name}%')
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if leader is not None:
|
||||
filters.update(leader__like=f'%{leader}%')
|
||||
filters['leader__like'] = f'%{leader}%'
|
||||
if phone is not None:
|
||||
filters.update(phone__startswith=phone)
|
||||
filters['phone__startswith'] = phone
|
||||
if status is not None:
|
||||
filters.update(status=status)
|
||||
return await self.select_models_order(db, 'sort', None, await filter_data_permission(db, request), **filters)
|
||||
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:
|
||||
"""
|
||||
@@ -106,9 +107,7 @@ class CRUDDept(CRUDPlus[Dept]):
|
||||
: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)
|
||||
return result.scalars().first()
|
||||
return await self.select_model(db, dept_id, load_strategies=['users'])
|
||||
|
||||
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
|
||||
"""
|
||||
@@ -118,9 +117,7 @@ class CRUDDept(CRUDPlus[Dept]):
|
||||
:param dept_id: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).where(self.model.parent_id == dept_id, self.model.del_flag == 0)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
return await self.select_models(db, parent_id=dept_id, del_flag=0)
|
||||
|
||||
|
||||
dept_dao: CRUDDept = CRUDDept(Dept)
|
||||
|
||||
@@ -21,12 +21,14 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
|
||||
: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: CreateLoginLogParam) -> None:
|
||||
@@ -39,15 +41,15 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
|
||||
"""
|
||||
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: 登录日志 ID 列表
|
||||
: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:
|
||||
"""
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import and_, asc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Menu
|
||||
@@ -44,28 +42,28 @@ class CRUDMenu(CRUDPlus[Menu]):
|
||||
: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: AsyncSession, 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: 菜单 ID 列表
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).order_by(asc(self.model.sort))
|
||||
filters = [self.model.type.in_([0, 1])]
|
||||
if not superuser:
|
||||
filters.append(self.model.id.in_(menu_ids))
|
||||
stmt = stmt.where(and_(*filters))
|
||||
menu = await db.execute(stmt)
|
||||
return menu.scalars().all()
|
||||
filters = {'type__in': [0, 1, 3, 4]}
|
||||
|
||||
if menu_ids:
|
||||
filters['id__in'] = menu_ids
|
||||
|
||||
return await self.select_models_order(db, 'sort', 'asc', **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||
"""
|
||||
@@ -106,9 +104,7 @@ class CRUDMenu(CRUDPlus[Menu]):
|
||||
: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
|
||||
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||
: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__eq'] = 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: CreateOperaLogParam) -> None:
|
||||
@@ -39,15 +41,15 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||
"""
|
||||
await self.create_model(db, 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: 操作日志 ID 列表
|
||||
: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:
|
||||
"""
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from sqlalchemy import Select, 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 DataScope, Menu, Role, User
|
||||
from backend.app.admin.model import DataScope, Menu, Role
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
UpdateRoleMenuParam,
|
||||
@@ -37,13 +36,7 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
:param role_id: 角色 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = (
|
||||
select(self.model)
|
||||
.options(selectinload(self.model.menus), selectinload(self.model.scopes))
|
||||
.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: AsyncSession) -> Sequence[Role]:
|
||||
"""
|
||||
@@ -54,18 +47,6 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
"""
|
||||
return await self.select_models(db)
|
||||
|
||||
async def get_users(self, db: AsyncSession, user_id: int) -> Sequence[Role]:
|
||||
"""
|
||||
获取用户角色列表
|
||||
|
||||
:param db: 数据库会话
|
||||
:param user_id: 用户 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:
|
||||
"""
|
||||
获取角色列表
|
||||
@@ -74,22 +55,23 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
:param status: 角色状态
|
||||
:return:
|
||||
"""
|
||||
stmt = (
|
||||
select(self.model)
|
||||
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.scopes))
|
||||
.order_by(desc(self.model.created_time))
|
||||
)
|
||||
|
||||
filters = []
|
||||
filters = {}
|
||||
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
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:
|
||||
"""
|
||||
@@ -152,15 +134,15 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
current_role.scopes = scopes.scalars().all()
|
||||
return len(current_role.scopes)
|
||||
|
||||
async def delete(self, db: AsyncSession, role_id: list[int]) -> int:
|
||||
async def delete(self, db: AsyncSession, role_ids: list[int]) -> int:
|
||||
"""
|
||||
删除角色
|
||||
批量删除角色
|
||||
|
||||
:param db: 数据库会话
|
||||
:param role_id: 角色 ID 列表
|
||||
: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)
|
||||
|
||||
@@ -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,11 +10,9 @@ 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
|
||||
@@ -63,26 +61,6 @@ class CRUDUser(CRUDPlus[User]):
|
||||
"""
|
||||
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: 是否社交用户
|
||||
: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:
|
||||
"""
|
||||
添加用户
|
||||
@@ -97,14 +75,33 @@ class CRUDUser(CRUDPlus[User]):
|
||||
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:
|
||||
"""
|
||||
salt = bcrypt.gensalt()
|
||||
obj.password = get_hash_password(obj.password, salt)
|
||||
dict_obj = obj.model_dump()
|
||||
dict_obj.update({'is_staff': True, 'salt': salt})
|
||||
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:
|
||||
"""
|
||||
更新用户信息
|
||||
|
||||
@@ -113,36 +110,25 @@ class CRUDUser(CRUDPlus[User]):
|
||||
: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
|
||||
|
||||
:param db: 数据库会话
|
||||
:param input_user: 用户对象
|
||||
:param obj: 更新角色参数
|
||||
: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)
|
||||
|
||||
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: 用户 ID
|
||||
:param user_id: 用户 ID
|
||||
:param avatar: 头像地址
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, input_user, {'avatar': str(avatar.url)})
|
||||
return await self.update_model(db, user_id, {'avatar': avatar})
|
||||
|
||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||
"""
|
||||
@@ -156,7 +142,7 @@ class CRUDUser(CRUDPlus[User]):
|
||||
|
||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||
"""
|
||||
检查邮箱是否已被注册
|
||||
检查邮箱是否已被绑定
|
||||
|
||||
:param db: 数据库会话
|
||||
:param email: 电子邮箱
|
||||
@@ -185,30 +171,26 @@ class CRUDUser(CRUDPlus[User]):
|
||||
: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))
|
||||
)
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if dept:
|
||||
filters.append(self.model.dept_id == dept)
|
||||
filters['dept_id'] = dept
|
||||
if username:
|
||||
filters.append(self.model.username.like(f'%{username}%'))
|
||||
filters['username__like'] = f'%{username}%'
|
||||
if phone:
|
||||
filters.append(self.model.phone.like(f'%{phone}%'))
|
||||
filters['phone_like'] = f'%{phone}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
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 get_super(self, db: AsyncSession, user_id: int) -> bool:
|
||||
"""
|
||||
@@ -309,22 +291,19 @@ class CRUDUser(CRUDPlus[User]):
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(
|
||||
selectinload(self.model.dept),
|
||||
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes)),
|
||||
)
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if user_id:
|
||||
filters.append(self.model.id == user_id)
|
||||
filters['id'] = user_id
|
||||
if username:
|
||||
filters.append(self.model.username == username)
|
||||
filters['username'] = username
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
user = await db.execute(stmt)
|
||||
return user.scalars().first()
|
||||
return await self.select_model_by_column(
|
||||
db,
|
||||
load_options=[selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes))],
|
||||
load_strategies=['dept'],
|
||||
**filters,
|
||||
)
|
||||
|
||||
|
||||
user_dao: CRUDUser = CRUDUser(User)
|
||||
|
||||
@@ -8,4 +8,3 @@ 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
|
||||
|
||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.admin.model.m2m import sys_data_scope_rule
|
||||
from backend.common.model import Base, id_key
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,8 +29,5 @@ class DataRule(Base):
|
||||
)
|
||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||
|
||||
# 数据范围规则一对多
|
||||
scope_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_data_scope.id', ondelete='SET NULL'), default=None, comment='数据范围关联 ID'
|
||||
)
|
||||
scope: Mapped[DataScope] = relationship(init=False, back_populates='rules')
|
||||
# 数据范围规则多对多
|
||||
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules')
|
||||
|
||||
@@ -7,7 +7,7 @@ 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_scope
|
||||
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:
|
||||
@@ -23,8 +23,8 @@ class DataScope(Base):
|
||||
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, back_populates='scope')
|
||||
# 数据范围规则多对多
|
||||
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')
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, String
|
||||
from sqlalchemy import BigInteger, Boolean, ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import INTEGER
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -32,7 +32,7 @@ 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[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
|
||||
|
||||
@@ -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_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_scope_id',
|
||||
Integer,
|
||||
BigInteger,
|
||||
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
comment='数据范围 ID',
|
||||
),
|
||||
)
|
||||
|
||||
sys_data_scope_rule = Table(
|
||||
'sys_data_scope_rule',
|
||||
MappedBase.metadata,
|
||||
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column(
|
||||
'data_scope_id',
|
||||
BigInteger,
|
||||
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
comment='数据范围 ID',
|
||||
),
|
||||
Column(
|
||||
'data_rule_id',
|
||||
BigInteger,
|
||||
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
comment='数据规则 ID',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
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
|
||||
@@ -24,10 +24,10 @@ 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] = mapped_column(String(200), comment='路由地址')
|
||||
path: Mapped[str | None] = mapped_column(String(200), comment='路由地址')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
|
||||
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
|
||||
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正常)')
|
||||
@@ -42,7 +42,7 @@ class Menu(Base):
|
||||
|
||||
# 父级菜单一对多
|
||||
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[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
|
||||
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
|
||||
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import String
|
||||
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_scope, sys_role_menu, sys_user_role
|
||||
@@ -24,6 +24,9 @@ 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='备注'
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from backend.database.db import uuid4_str
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import Dept, Role, UserSocial
|
||||
from backend.app.admin.model import Dept, Role
|
||||
|
||||
|
||||
class User(Base):
|
||||
@@ -26,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='昵称')
|
||||
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='邮箱')
|
||||
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
||||
password: Mapped[str] = mapped_column(String(255), comment='密码')
|
||||
salt: Mapped[bytes] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
||||
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
|
||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||
is_superuser: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
||||
)
|
||||
is_staff: Mapped[bool] = mapped_column(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
||||
)
|
||||
status: Mapped[int] = mapped_column(default=1, index=True, 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='注册时间'
|
||||
)
|
||||
@@ -55,8 +55,5 @@ class User(Base):
|
||||
)
|
||||
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||
|
||||
# 用户社交信息一对多
|
||||
socials: Mapped[list[UserSocial]] = relationship(init=False, back_populates='user')
|
||||
|
||||
# 用户角色多对多
|
||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
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 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[User | None] = relationship(init=False, back_populates='socials')
|
||||
@@ -27,6 +27,12 @@ class UpdateDataRuleParam(DataRuleSchemaBase):
|
||||
"""更新数据规则参数"""
|
||||
|
||||
|
||||
class DeleteDataRuleParam(SchemaBase):
|
||||
"""删除数据规则参数"""
|
||||
|
||||
pks: list[int] = Field(description='规则 ID 列表')
|
||||
|
||||
|
||||
class GetDataRuleDetail(DataRuleSchemaBase):
|
||||
"""数据规则详情"""
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ class UpdateDataScopeRuleParam(SchemaBase):
|
||||
rules: list[int] = Field(description='数据规则 ID 列表')
|
||||
|
||||
|
||||
class DeleteDataScopeParam(SchemaBase):
|
||||
"""删除数据范围参数"""
|
||||
|
||||
pks: list[int] = Field(description='数据范围 ID 列表')
|
||||
|
||||
|
||||
class GetDataScopeDetail(DataScopeBase):
|
||||
"""数据范围详情"""
|
||||
|
||||
|
||||
@@ -33,6 +33,12 @@ class UpdateLoginLogParam(LoginLogSchemaBase):
|
||||
"""更新登录日志参数"""
|
||||
|
||||
|
||||
class DeleteLoginLogParam(SchemaBase):
|
||||
"""删除登录日志参数"""
|
||||
|
||||
pks: list[int] = Field(description='登录日志 ID 列表')
|
||||
|
||||
|
||||
class GetLoginLogDetail(LoginLogSchemaBase):
|
||||
"""登录日志详情"""
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ class MenuSchemaBase(SchemaBase):
|
||||
|
||||
title: str = Field(description='菜单标题')
|
||||
name: str = Field(description='菜单名称')
|
||||
path: 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(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
|
||||
type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||
component: str | None = Field(None, description='组件路径')
|
||||
perms: str | None = Field(None, description='权限标识')
|
||||
status: StatusType = Field(StatusType.enable, description='状态')
|
||||
|
||||
@@ -41,6 +41,12 @@ class UpdateOperaLogParam(OperaLogSchemaBase):
|
||||
"""更新操作日志参数"""
|
||||
|
||||
|
||||
class DeleteOperaLogParam(SchemaBase):
|
||||
"""删除操作日志参数"""
|
||||
|
||||
pks: list[int] = Field(description='操作日志 ID 列表')
|
||||
|
||||
|
||||
class GetOperaLogDetail(OperaLogSchemaBase):
|
||||
"""操作日志详情"""
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ class RoleSchemaBase(SchemaBase):
|
||||
|
||||
name: str = Field(description='角色名称')
|
||||
status: StatusType = Field(StatusType.enable, description='状态')
|
||||
is_filter_scopes: bool = Field(True, description='过滤数据权限')
|
||||
remark: str | None = Field(None, description='备注')
|
||||
|
||||
|
||||
@@ -26,6 +27,12 @@ class UpdateRoleParam(RoleSchemaBase):
|
||||
"""更新角色参数"""
|
||||
|
||||
|
||||
class DeleteRoleParam(SchemaBase):
|
||||
"""删除角色参数"""
|
||||
|
||||
pks: list[int] = Field(description='角色 ID 列表')
|
||||
|
||||
|
||||
class UpdateRoleMenuParam(SchemaBase):
|
||||
"""更新角色菜单参数"""
|
||||
|
||||
|
||||
@@ -35,12 +35,6 @@ class GetLoginToken(AccessTokenBase):
|
||||
user: GetUserInfoDetail = Field(description='用户信息')
|
||||
|
||||
|
||||
class KickOutToken(SchemaBase):
|
||||
"""踢出令牌"""
|
||||
|
||||
session_uuid: str = Field(description='会话 UUID')
|
||||
|
||||
|
||||
class GetTokenDetail(SchemaBase):
|
||||
"""令牌详情"""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing_extensions import Self
|
||||
from backend.app.admin.schema.dept import GetDeptDetail
|
||||
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import CustomPhoneNumber, SchemaBase
|
||||
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
||||
|
||||
|
||||
class AuthSchemaBase(SchemaBase):
|
||||
@@ -25,20 +25,20 @@ class AuthLoginParam(AuthSchemaBase):
|
||||
captcha: str = Field(description='验证码')
|
||||
|
||||
|
||||
class RegisterUserParam(AuthSchemaBase):
|
||||
"""用户注册参数"""
|
||||
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||
|
||||
|
||||
class AddUserParam(AuthSchemaBase):
|
||||
"""添加用户参数"""
|
||||
|
||||
dept_id: int = Field(description='部门 ID')
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||
|
||||
|
||||
class AddOAuth2UserParam(AuthSchemaBase):
|
||||
"""添加 OAuth2 用户参数"""
|
||||
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: EmailStr = Field(description='邮箱')
|
||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||
|
||||
|
||||
class ResetPasswordParam(SchemaBase):
|
||||
@@ -55,26 +55,15 @@ class UserInfoSchemaBase(SchemaBase):
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
username: str = Field(description='用户名')
|
||||
nickname: str = Field(description='昵称')
|
||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||
|
||||
|
||||
class UpdateUserParam(UserInfoSchemaBase):
|
||||
"""更新用户参数"""
|
||||
|
||||
|
||||
class UpdateUserRoleParam(SchemaBase):
|
||||
"""更新用户角色参数"""
|
||||
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
|
||||
|
||||
class AvatarParam(SchemaBase):
|
||||
"""更新头像参数"""
|
||||
|
||||
url: HttpUrl = Field(description='头像 http 地址')
|
||||
|
||||
|
||||
class GetUserInfoDetail(UserInfoSchemaBase):
|
||||
"""用户信息详情"""
|
||||
|
||||
@@ -83,7 +72,8 @@ class GetUserInfoDetail(UserInfoSchemaBase):
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
id: int = Field(description='用户 ID')
|
||||
uuid: str = Field(description='用户 UUID')
|
||||
avatar: str | None = Field(None, description='头像')
|
||||
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||
status: StatusType = Field(StatusType.enable, description='状态')
|
||||
is_superuser: bool = Field(description='是否超级管理员')
|
||||
is_staff: bool = Field(description='是否管理员')
|
||||
|
||||
@@ -5,6 +5,7 @@ from fastapi.security import HTTPBasicCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.background import BackgroundTask, BackgroundTasks
|
||||
|
||||
from backend.app.admin.crud.crud_menu import menu_dao
|
||||
from backend.app.admin.crud.crud_user import user_dao
|
||||
from backend.app.admin.model import User
|
||||
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
|
||||
@@ -66,13 +67,13 @@ class AuthService:
|
||||
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
|
||||
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
|
||||
@@ -92,36 +93,36 @@ class AuthService:
|
||||
user = await self.user_verify(db, obj.username, obj.password)
|
||||
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='验证码失效,请重新获取')
|
||||
if captcha_code.lower() != obj.captcha.lower():
|
||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||
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(
|
||||
@@ -136,7 +137,7 @@ class AuthService:
|
||||
msg=e.msg,
|
||||
),
|
||||
)
|
||||
raise errors.AuthorizationError(msg=e.msg, background=task)
|
||||
raise errors.RequestError(msg=e.msg, background=task)
|
||||
except Exception as e:
|
||||
log.error(f'登陆错误: {e}')
|
||||
raise e
|
||||
@@ -154,17 +155,42 @@ class AuthService:
|
||||
),
|
||||
)
|
||||
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:
|
||||
@@ -172,24 +198,22 @@ class AuthService:
|
||||
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 无效')
|
||||
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='用户已被锁定, 请联系统管理员')
|
||||
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,
|
||||
@@ -211,22 +235,21 @@ class AuthService:
|
||||
:param response: FastAPI 响应对象
|
||||
:return:
|
||||
"""
|
||||
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)
|
||||
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()
|
||||
|
||||
@@ -6,7 +6,12 @@ from sqlalchemy import Select
|
||||
|
||||
from backend.app.admin.crud.crud_data_rule import data_rule_dao
|
||||
from backend.app.admin.model import DataRule
|
||||
from backend.app.admin.schema.data_rule import CreateDataRuleParam, GetDataRuleColumnDetail, 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
|
||||
@@ -82,7 +87,7 @@ class DataRuleService:
|
||||
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
|
||||
@@ -100,20 +105,20 @@ class DataRuleService:
|
||||
raise errors.NotFoundError(msg='数据规则不存在')
|
||||
if data_rule.name != obj.name:
|
||||
if await data_rule_dao.get_by_name(db, obj.name):
|
||||
raise errors.ForbiddenError(msg='数据规则已存在')
|
||||
raise errors.ConflictError(msg='数据规则已存在')
|
||||
count = await data_rule_dao.update(db, pk, obj)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, obj: DeleteDataRuleParam) -> int:
|
||||
"""
|
||||
删除数据规则
|
||||
批量删除数据规则
|
||||
|
||||
:param pk: 规则 ID 列表
|
||||
:param obj: 规则 ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await data_rule_dao.delete(db, pk)
|
||||
count = await data_rule_dao.delete(db, obj.pks)
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
#!/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, UpdateDataScopeParam, UpdateDataScopeRuleParam
|
||||
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
|
||||
@@ -28,6 +35,13 @@ class DataScopeService:
|
||||
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:
|
||||
"""
|
||||
@@ -64,7 +78,7 @@ class DataScopeService:
|
||||
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.ForbiddenError(msg='数据范围已存在')
|
||||
raise errors.ConflictError(msg='数据范围已存在')
|
||||
await data_scope_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
@@ -82,7 +96,7 @@ class DataScopeService:
|
||||
raise errors.NotFoundError(msg='数据范围不存在')
|
||||
if data_scope.name != obj.name:
|
||||
if await data_scope_dao.get_by_name(db, obj.name):
|
||||
raise errors.ForbiddenError(msg='数据范围已存在')
|
||||
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:
|
||||
@@ -103,17 +117,17 @@ class DataScopeService:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, obj: DeleteDataScopeParam) -> int:
|
||||
"""
|
||||
删除数据范围
|
||||
批量删除数据范围
|
||||
|
||||
:param pk: 范围 ID 列表
|
||||
:param obj: 范围 ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await data_scope_dao.delete(db, pk)
|
||||
for _pk in pk:
|
||||
data_rule = await data_scope_dao.get(db, _pk)
|
||||
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:
|
||||
|
||||
@@ -32,7 +32,7 @@ class DeptService:
|
||||
return dept
|
||||
|
||||
@staticmethod
|
||||
async def get_dept_tree(
|
||||
async def get_tree(
|
||||
*, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
@@ -61,7 +61,7 @@ class DeptService:
|
||||
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:
|
||||
@@ -83,7 +83,7 @@ class DeptService:
|
||||
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:
|
||||
@@ -104,10 +104,10 @@ class DeptService:
|
||||
async with async_db_session.begin() as db:
|
||||
dept = await dept_dao.get_with_relation(db, pk)
|
||||
if dept.users:
|
||||
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
|
||||
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)
|
||||
for user in dept.users:
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
|
||||
@@ -71,15 +71,15 @@ class LoginLogService:
|
||||
log.error(f'登录日志创建失败: {e}')
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, obj: DeleteLoginLogParam) -> int:
|
||||
"""
|
||||
删除登录日志
|
||||
批量删除登录日志
|
||||
|
||||
:param pk: 日志 ID 列表
|
||||
: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
|
||||
|
||||
@@ -32,7 +32,7 @@ class MenuService:
|
||||
return menu
|
||||
|
||||
@staticmethod
|
||||
async def get_menu_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
|
||||
async def get_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取菜单树形结构
|
||||
|
||||
@@ -41,27 +41,30 @@ class MenuService:
|
||||
: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_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
|
||||
async def get_sidebar(*, request: Request) -> list[dict[str, Any] | None]:
|
||||
"""
|
||||
获取用户的菜单树形结构
|
||||
获取用户的菜单侧边栏
|
||||
|
||||
: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_vben5_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
|
||||
@@ -75,7 +78,7 @@ class MenuService:
|
||||
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:
|
||||
@@ -97,7 +100,7 @@ class MenuService:
|
||||
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:
|
||||
@@ -121,7 +124,7 @@ class MenuService:
|
||||
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)
|
||||
if menu:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -34,15 +34,15 @@ class OperaLogService:
|
||||
await opera_log_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, obj: DeleteOperaLogParam) -> int:
|
||||
"""
|
||||
删除操作日志
|
||||
批量删除操作日志
|
||||
|
||||
:param pk: 日志 ID 列表
|
||||
:param obj: 日志 ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await opera_log_dao.delete(db, pk)
|
||||
count = await opera_log_dao.delete(db, obj.pks)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -8,17 +8,15 @@ import zipfile
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dulwich import porcelain
|
||||
from fastapi import UploadFile
|
||||
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.enums import PluginType, StatusType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.log import log
|
||||
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 install_requirements_async, uninstall_requirements_async
|
||||
from backend.utils.re_verify import is_git_url
|
||||
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
|
||||
|
||||
|
||||
@@ -40,78 +38,27 @@ class PluginService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def install_zip(*, file: UploadFile) -> None:
|
||||
"""
|
||||
通过 zip 压缩包安装插件
|
||||
|
||||
:param file: 插件 zip 压缩包
|
||||
:return:
|
||||
"""
|
||||
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 = file.filename[:-4]
|
||||
members_in_plugin_dir = [name for name in zf.namelist() if name.startswith(plugin_dir)]
|
||||
if not members_in_plugin_dir:
|
||||
raise errors.ForbiddenError(msg='插件压缩包内容非法')
|
||||
plugin_name = (
|
||||
members_in_plugin_dir[0]
|
||||
.replace('/', '')
|
||||
.replace('-master', '')
|
||||
.replace('-main', '')
|
||||
.replace('-dev', '')
|
||||
)
|
||||
if (
|
||||
len(members_in_plugin_dir) <= 3
|
||||
or f'{plugin_dir}/plugin.toml' not in members_in_plugin_dir
|
||||
or f'{plugin_dir}/README.md' not in members_in_plugin_dir
|
||||
):
|
||||
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
|
||||
|
||||
# 插件是否可安装
|
||||
full_plugin_path = os.path.join(PLUGIN_DIR, plugin_name)
|
||||
if os.path.exists(full_plugin_path):
|
||||
raise errors.ForbiddenError(msg='此插件已安装')
|
||||
else:
|
||||
os.makedirs(full_plugin_path, exist_ok=True)
|
||||
|
||||
# 解压(安装)
|
||||
members = []
|
||||
for member in zf.infolist():
|
||||
if member.filename.startswith(plugin_dir):
|
||||
new_filename = member.filename.replace(plugin_dir, '')
|
||||
if new_filename:
|
||||
member.filename = new_filename
|
||||
members.append(member)
|
||||
zf.extractall(os.path.join(PLUGIN_DIR, plugin_name), members)
|
||||
|
||||
await install_requirements_async(plugin_name)
|
||||
async def changed() -> str | None:
|
||||
"""检查插件是否发生变更"""
|
||||
return await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||
|
||||
@staticmethod
|
||||
async def install_git(*, repo_url: str):
|
||||
async def install(*, type: PluginType, file: UploadFile | None = None, repo_url: str | None = None) -> str:
|
||||
"""
|
||||
通过 git 安装插件
|
||||
安装插件
|
||||
|
||||
:param repo_url: git 存储库的 URL
|
||||
:param type: 插件类型
|
||||
:param file: 插件 zip 压缩包
|
||||
:param repo_url: git 仓库地址
|
||||
:return:
|
||||
"""
|
||||
match = is_git_url(repo_url)
|
||||
if not match:
|
||||
raise errors.ForbiddenError(msg='Git 仓库地址格式非法')
|
||||
repo_name = match.group('repo')
|
||||
plugins = await redis_client.lrange(settings.PLUGIN_REDIS_PREFIX, 0, -1)
|
||||
if repo_name in plugins:
|
||||
raise errors.ForbiddenError(msg=f'{repo_name} 插件已安装')
|
||||
try:
|
||||
porcelain.clone(repo_url, os.path.join(PLUGIN_DIR, repo_name), checkout=True)
|
||||
except Exception as e:
|
||||
log.error(f'插件安装失败: {e}')
|
||||
raise errors.ServerError(msg='插件安装失败,请稍后重试') from e
|
||||
else:
|
||||
await install_requirements_async(repo_name)
|
||||
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):
|
||||
@@ -123,10 +70,13 @@ class PluginService:
|
||||
"""
|
||||
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
||||
if not os.path.exists(plugin_dir):
|
||||
raise errors.ForbiddenError(msg='插件不存在')
|
||||
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}:info:{plugin}')
|
||||
await redis_client.hdel(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin)
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||
|
||||
@staticmethod
|
||||
async def update_status(*, plugin: str):
|
||||
@@ -138,18 +88,20 @@ class PluginService:
|
||||
"""
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
|
||||
if not plugin_info:
|
||||
raise errors.ForbiddenError(msg='插件不存在')
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
plugin_info = json.loads(plugin_info)
|
||||
|
||||
# 更新持久缓存状态
|
||||
new_status = (
|
||||
StatusType.enable.value
|
||||
if plugin_info.get('plugin', {}).get('enable') == StatusType.disable.value
|
||||
else StatusType.disable.value
|
||||
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}:info:{plugin}', json.dumps(plugin_info, ensure_ascii=False)
|
||||
)
|
||||
await redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin, str(new_status))
|
||||
await redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin, new_status)
|
||||
|
||||
@staticmethod
|
||||
async def build(*, plugin: str) -> io.BytesIO:
|
||||
@@ -161,7 +113,7 @@ class PluginService:
|
||||
"""
|
||||
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
|
||||
if not os.path.exists(plugin_dir):
|
||||
raise errors.ForbiddenError(msg='插件不存在')
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
|
||||
bio = io.BytesIO()
|
||||
with zipfile.ZipFile(bio, 'w') as zf:
|
||||
@@ -170,8 +122,9 @@ class PluginService:
|
||||
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)
|
||||
zf.write(file_path, os.path.join(plugin, arcname))
|
||||
|
||||
bio.seek(0)
|
||||
return bio
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from backend.app.admin.crud.crud_role import role_dao
|
||||
from backend.app.admin.model import Role
|
||||
from backend.app.admin.schema.role import (
|
||||
CreateRoleParam,
|
||||
DeleteRoleParam,
|
||||
UpdateRoleMenuParam,
|
||||
UpdateRoleParam,
|
||||
UpdateRoleScopeParam,
|
||||
@@ -45,18 +46,6 @@ class RoleService:
|
||||
roles = await role_dao.get_all(db)
|
||||
return roles
|
||||
|
||||
@staticmethod
|
||||
async def get_users(*, pk: int) -> Sequence[Role]:
|
||||
"""
|
||||
获取用户的角色列表
|
||||
|
||||
:param pk: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session() as db:
|
||||
roles = await role_dao.get_users(db, user_id=pk)
|
||||
return roles
|
||||
|
||||
@staticmethod
|
||||
async def get_select(*, name: str | None, status: int | None) -> Select:
|
||||
"""
|
||||
@@ -69,7 +58,7 @@ class RoleService:
|
||||
return await role_dao.get_list(name=name, status=status)
|
||||
|
||||
@staticmethod
|
||||
async def get_menu_tree(*, pk: int) -> list[dict[str, Any]]:
|
||||
async def get_menu_tree(*, pk: int) -> list[dict[str, Any] | None]:
|
||||
"""
|
||||
获取角色的菜单树形结构
|
||||
|
||||
@@ -80,9 +69,7 @@ class RoleService:
|
||||
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)
|
||||
menu_tree = get_tree_data(role.menus) if role.menus else []
|
||||
return menu_tree
|
||||
|
||||
@staticmethod
|
||||
@@ -111,7 +98,7 @@ class RoleService:
|
||||
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
|
||||
@@ -128,9 +115,8 @@ class RoleService:
|
||||
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}')
|
||||
@@ -146,7 +132,7 @@ class RoleService:
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
role = await role_dao.get_with_relation(db, pk)
|
||||
role = await role_dao.get(db, pk)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
for menu_id in menu_ids.menus:
|
||||
@@ -181,17 +167,17 @@ class RoleService:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, obj: DeleteRoleParam) -> int:
|
||||
"""
|
||||
删除角色
|
||||
批量删除角色
|
||||
|
||||
:param pk: 角色 ID 列表
|
||||
:param obj: 角色 ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await role_dao.delete(db, pk)
|
||||
for _pk in pk:
|
||||
role = await role_dao.get(db, _pk)
|
||||
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}')
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
# -*- 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.core.conf import settings
|
||||
@@ -28,176 +28,33 @@ class UserService:
|
||||
"""用户服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def register(*, obj: RegisterUserParam) -> None:
|
||||
"""
|
||||
注册新用户
|
||||
|
||||
:param obj: 用户注册参数
|
||||
:return:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
添加新用户
|
||||
|
||||
: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='用户已注册')
|
||||
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.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.add(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param obj: 密码重置参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
user = await user_dao.get(db, request.user.id)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if not password_verify(obj.old_password, user.password):
|
||||
raise errors.ForbiddenError(msg='原密码错误')
|
||||
if obj.new_password != obj.confirm_password:
|
||||
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)
|
||||
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}',
|
||||
]
|
||||
for prefix in key_prefix:
|
||||
await redis_client.delete_prefix(prefix)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def get_userinfo(*, username: str) -> User:
|
||||
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, username=username)
|
||||
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 update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
|
||||
async def get_roles(*, pk: int) -> Sequence[Role]:
|
||||
"""
|
||||
更新用户信息
|
||||
获取用户所有角色
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param username: 用户名
|
||||
:param obj: 用户更新参数
|
||||
:param pk: 用户 ID
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
if request.user.username != username:
|
||||
raise errors.ForbiddenError(msg='你只能修改自己的信息')
|
||||
user = await user_dao.get_with_relation(db, username=username)
|
||||
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='用户不存在')
|
||||
if user.username != obj.username:
|
||||
_username = await user_dao.get_by_username(db, obj.username)
|
||||
if _username:
|
||||
raise errors.ForbiddenError(msg='用户名已注册')
|
||||
if user.nickname != obj.nickname:
|
||||
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
||||
if nickname:
|
||||
raise errors.ForbiddenError(msg='昵称已注册')
|
||||
if 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, user.id, obj)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def update_roles(*, request: Request, username: str, obj: UpdateUserRoleParam) -> None:
|
||||
"""
|
||||
更新用户角色
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param username: 用户名
|
||||
:param obj: 角色更新参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
if not request.user.is_superuser and 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='用户不存在')
|
||||
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:
|
||||
"""
|
||||
更新用户头像
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param username: 用户名
|
||||
:param avatar: 头像参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
if request.user.username != username:
|
||||
raise errors.AuthorizationError(msg='你只能修改自己的信息')
|
||||
user = await user_dao.get_by_username(db, username)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
count = await user_dao.update_avatar(db, user.id, avatar)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
return count
|
||||
return user.roles
|
||||
|
||||
@staticmethod
|
||||
async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
|
||||
@@ -213,9 +70,58 @@ class UserService:
|
||||
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 def create(*, request: Request, obj: AddUserParam) -> None:
|
||||
"""
|
||||
更新用户权限
|
||||
创建用户
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param obj: 用户添加参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
superuser_verify(request)
|
||||
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)}'
|
||||
if not obj.password:
|
||||
raise errors.RequestError(msg='密码不允许为空')
|
||||
if not await dept_dao.get(db, obj.dept_id):
|
||||
raise errors.NotFoundError(msg='部门不存在')
|
||||
for role_id in obj.roles:
|
||||
if not await role_dao.get(db, role_id):
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
await user_dao.add(db, obj)
|
||||
|
||||
@staticmethod
|
||||
async def update(*, 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_with_relation(db, user_id=pk)
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if request.user.username != user.username:
|
||||
raise errors.ForbiddenError(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_superuser(*, request: Request, pk: int) -> int:
|
||||
"""
|
||||
更新用户管理员状态
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
@@ -227,7 +133,7 @@ class UserService:
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='非法操作')
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
super_status = await user_dao.get_super(db, pk)
|
||||
count = await user_dao.set_super(db, pk, not super_status)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
@@ -248,7 +154,7 @@ class UserService:
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='非法操作')
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
staff_status = await user_dao.get_staff(db, pk)
|
||||
count = await user_dao.set_staff(db, pk, not staff_status)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
@@ -269,7 +175,7 @@ class UserService:
|
||||
if not user:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if pk == request.user.id:
|
||||
raise errors.ForbiddenError(msg='非法操作')
|
||||
raise errors.ForbiddenError(msg='禁止修改自身权限')
|
||||
status = await user_dao.get_status(db, pk)
|
||||
count = await user_dao.set_status(db, pk, 0 if status == 1 else 1)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
@@ -307,16 +213,66 @@ class UserService:
|
||||
await redis_client.delete_prefix(key_prefix)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, username: str) -> int:
|
||||
async def update_permission(self, *, request: Request, pk: int, type: UserPermissionType) -> int:
|
||||
"""
|
||||
删除用户
|
||||
更新用户权限
|
||||
|
||||
:param username: 用户名
|
||||
:param request: FastAPI 请求对象
|
||||
:param pk: 用户 ID
|
||||
:param type: 权限类型
|
||||
:return:
|
||||
"""
|
||||
match type:
|
||||
case UserPermissionType.superuser:
|
||||
count = await self.update_superuser(request=request, pk=pk)
|
||||
case UserPermissionType.staff:
|
||||
count = await self.update_staff(request=request, pk=pk)
|
||||
case UserPermissionType.status:
|
||||
count = await self.update_status(request=request, pk=pk)
|
||||
case UserPermissionType.multi_login:
|
||||
count = await self.update_multi_login(request=request, pk=pk)
|
||||
case _:
|
||||
raise errors.RequestError(msg='权限类型不存在')
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def reset_pwd(*, pk: int, obj: ResetPasswordParam) -> int:
|
||||
"""
|
||||
重置用户密码
|
||||
|
||||
:param pk: 用户 ID
|
||||
:param obj: 密码重置参数
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
user = await user_dao.get_by_username(db, username)
|
||||
user = await user_dao.get(db, pk)
|
||||
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='密码输入不一致')
|
||||
new_pwd = get_hash_password(obj.new_password, user.salt)
|
||||
count = await user_dao.reset_password(db, user.id, new_pwd)
|
||||
key_prefix = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||
]
|
||||
for prefix in key_prefix:
|
||||
await redis_client.delete_prefix(prefix)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, 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='用户不存在')
|
||||
count = await user_dao.delete(db, user.id)
|
||||
|
||||
@@ -14,12 +14,6 @@ from backend.common.security.rbac import DependsRBAC
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('', summary='获取可执行任务', dependencies=[DependsJwtAuth])
|
||||
async def get_all_tasks() -> ResponseSchemaModel[list[str]]:
|
||||
tasks = await task_service.get_list()
|
||||
return response_base.success(data=tasks)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{tid}',
|
||||
summary='获取任务详情',
|
||||
@@ -27,12 +21,18 @@ async def get_all_tasks() -> ResponseSchemaModel[list[str]]:
|
||||
description='此接口被视为作废,建议使用 flower 查看任务详情',
|
||||
dependencies=[DependsJwtAuth],
|
||||
)
|
||||
async def get_task_detail(tid: Annotated[str, Path(description='任务 UUID')]) -> ResponseSchemaModel[TaskResult]:
|
||||
status = task_service.get_detail(tid=tid)
|
||||
async def get_task(tid: Annotated[str, Path(description='任务 UUID')]) -> ResponseSchemaModel[TaskResult]:
|
||||
status = task_service.get(tid=tid)
|
||||
return response_base.success(data=status)
|
||||
|
||||
|
||||
@router.post(
|
||||
@router.get('', summary='获取所有任务', dependencies=[DependsJwtAuth])
|
||||
async def get_all_tasks() -> ResponseSchemaModel[list[str]]:
|
||||
tasks = await task_service.get_all()
|
||||
return response_base.success(data=tasks)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{tid}',
|
||||
summary='撤销任务',
|
||||
dependencies=[
|
||||
@@ -46,8 +46,8 @@ async def revoke_task(tid: Annotated[str, Path(description='任务 UUID')]) -> R
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='执行任务',
|
||||
'/runs',
|
||||
summary='运行任务',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:task:run')),
|
||||
DependsRBAC,
|
||||
|
||||
@@ -19,11 +19,11 @@ class TaskResult(SchemaBase):
|
||||
"""任务执行结果"""
|
||||
|
||||
result: str = Field(description='任务执行结果')
|
||||
traceback: str = Field(description='错误堆栈信息')
|
||||
traceback: str | None = Field(None, description='错误堆栈信息')
|
||||
status: str = Field(description='任务状态')
|
||||
name: str = Field(description='任务名称')
|
||||
name: str | None = Field(None, description='任务名称')
|
||||
args: list[Any] | None = Field(None, description='任务函数位置参数')
|
||||
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
|
||||
worker: str = Field(description='执行任务的 worker')
|
||||
worker: str | None = Field(None, description='执行任务的 worker')
|
||||
retries: int | None = Field(None, description='重试次数')
|
||||
queue: str | None = Field(None, description='任务队列')
|
||||
|
||||
@@ -7,21 +7,11 @@ from starlette.concurrency import run_in_threadpool
|
||||
from backend.app.task.celery import celery_app
|
||||
from backend.app.task.schema.task import RunParam, TaskResult
|
||||
from backend.common.exception import errors
|
||||
from backend.common.exception.errors import NotFoundError
|
||||
|
||||
|
||||
class TaskService:
|
||||
@staticmethod
|
||||
async def get_list() -> list[str]:
|
||||
"""获取所有已注册的 Celery 任务列表"""
|
||||
registered_tasks = await run_in_threadpool(celery_app.control.inspect().registered)
|
||||
if not registered_tasks:
|
||||
raise errors.ForbiddenError(msg='Celery 服务未启动')
|
||||
tasks = list(registered_tasks.values())[0]
|
||||
return tasks
|
||||
|
||||
@staticmethod
|
||||
def get_detail(*, tid: str) -> TaskResult:
|
||||
def get(*, tid: str) -> TaskResult:
|
||||
"""
|
||||
获取指定任务的详细信息
|
||||
|
||||
@@ -31,7 +21,7 @@ class TaskService:
|
||||
try:
|
||||
result = AsyncResult(id=tid, app=celery_app)
|
||||
except NotRegistered:
|
||||
raise NotFoundError(msg='任务不存在')
|
||||
raise errors.NotFoundError(msg='任务不存在')
|
||||
return TaskResult(
|
||||
result=result.result,
|
||||
traceback=result.traceback,
|
||||
@@ -44,6 +34,15 @@ class TaskService:
|
||||
queue=result.queue,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> list[str]:
|
||||
"""获取所有已注册的 Celery 任务列表"""
|
||||
registered_tasks = await run_in_threadpool(celery_app.control.inspect().registered)
|
||||
if not registered_tasks:
|
||||
raise errors.ServerError(msg='Celery 服务未启动')
|
||||
tasks = list(registered_tasks.values())[0]
|
||||
return tasks
|
||||
|
||||
@staticmethod
|
||||
def revoke(*, tid: str) -> None:
|
||||
"""
|
||||
@@ -55,7 +54,7 @@ class TaskService:
|
||||
try:
|
||||
result = AsyncResult(id=tid, app=celery_app)
|
||||
except NotRegistered:
|
||||
raise NotFoundError(msg='任务不存在')
|
||||
raise errors.NotFoundError(msg='任务不存在')
|
||||
result.revoke(terminate=True)
|
||||
|
||||
@staticmethod
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated
|
||||
|
||||
import cappa
|
||||
import uvicorn
|
||||
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from backend import console, get_version
|
||||
from backend.common.exception.errors import BaseExceptionMixin
|
||||
from backend.core.conf import settings
|
||||
from backend.utils._await import run_await
|
||||
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
|
||||
|
||||
|
||||
def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
||||
url = f'http://{host}:{port}'
|
||||
docs_url = url + settings.FASTAPI_DOCS_URL
|
||||
redoc_url = url + settings.FASTAPI_REDOC_URL
|
||||
openapi_url = url + settings.FASTAPI_OPENAPI_URL
|
||||
|
||||
panel_content = Text()
|
||||
panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue')
|
||||
panel_content.append(f'📚 Redoc 文档: {redoc_url}\n', style='yellow')
|
||||
panel_content.append(f'📡 OpenAPI JSON: {openapi_url}\n', style='green')
|
||||
panel_content.append(
|
||||
'🌍 fba 官方文档: https://fastapi-practices.github.io/fastapi_best_architecture_docs/',
|
||||
style='cyan',
|
||||
)
|
||||
|
||||
console.print(Panel(panel_content, title='fba 服务信息', border_style='purple', padding=(1, 2)))
|
||||
uvicorn.run(
|
||||
app='backend.main:app',
|
||||
host=host,
|
||||
port=port,
|
||||
reload=not reload,
|
||||
reload_excludes=[os.path.abspath('../.venv' if 'backend' in os.getcwd() else '.venv')],
|
||||
workers=workers,
|
||||
)
|
||||
|
||||
|
||||
def install_plugin(path: str, repo_url: str) -> None:
|
||||
if not path and not repo_url:
|
||||
raise cappa.Exit('path 或 repo_url 必须指定其中一项', code=1)
|
||||
if path and repo_url:
|
||||
raise cappa.Exit('path 和 repo_url 不能同时指定', code=1)
|
||||
|
||||
plugin_name = None
|
||||
console.print(Text('开始安装插件...', style='bold cyan'))
|
||||
|
||||
try:
|
||||
if path:
|
||||
plugin_name = run_await(install_zip_plugin)(file=path)
|
||||
if repo_url:
|
||||
plugin_name = run_await(install_git_plugin)(repo_url=repo_url)
|
||||
except Exception as e:
|
||||
raise cappa.Exit(e.msg if isinstance(e, BaseExceptionMixin) else str(e), code=1)
|
||||
|
||||
console.print(Text(f'插件 {plugin_name} 安装成功', style='bold cyan'))
|
||||
|
||||
|
||||
@cappa.command(help='运行服务')
|
||||
@dataclass
|
||||
class Run:
|
||||
host: Annotated[
|
||||
str,
|
||||
cappa.Arg(
|
||||
long=True,
|
||||
default='127.0.0.1',
|
||||
help='提供服务的主机 IP 地址,对于本地开发,请使用 `127.0.0.1`。'
|
||||
'要启用公共访问,例如在局域网中,请使用 `0.0.0.0`',
|
||||
),
|
||||
]
|
||||
port: Annotated[
|
||||
int,
|
||||
cappa.Arg(long=True, default=8000, help='提供服务的主机端口号'),
|
||||
]
|
||||
no_reload: Annotated[
|
||||
bool,
|
||||
cappa.Arg(long=True, default=False, help='禁用在(代码)文件更改时自动重新加载服务器'),
|
||||
]
|
||||
workers: Annotated[
|
||||
int | None,
|
||||
cappa.Arg(long=True, default=None, help='使用多个工作进程,必须与 `--no-reload` 同时使用'),
|
||||
]
|
||||
|
||||
def __call__(self):
|
||||
run(host=self.host, port=self.port, reload=self.no_reload, workers=self.workers)
|
||||
|
||||
|
||||
@cappa.command(help='新增插件')
|
||||
@dataclass
|
||||
class Add:
|
||||
path: Annotated[
|
||||
str | None,
|
||||
cappa.Arg(long=True, help='ZIP 插件的本地完整路径'),
|
||||
]
|
||||
repo_url: Annotated[
|
||||
str | None,
|
||||
cappa.Arg(long=True, help='Git 插件的仓库地址'),
|
||||
]
|
||||
|
||||
def __call__(self):
|
||||
install_plugin(path=self.path, repo_url=self.repo_url)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FbaCli:
|
||||
version: Annotated[
|
||||
bool,
|
||||
cappa.Arg(short='-V', long=True, default=False, help='打印当前版本号'),
|
||||
]
|
||||
subcmd: cappa.Subcommands[Run | Add | None] = None
|
||||
|
||||
def __call__(self):
|
||||
if self.version:
|
||||
get_version()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
output = cappa.Output(error_format='[red]Error[/]: {message}\n\n更多信息,尝试 "[cyan]--help[/]"')
|
||||
cappa.invoke(FbaCli, output=output)
|
||||
@@ -34,13 +34,6 @@ class RequestCallNext:
|
||||
response: Response
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NewToken:
|
||||
new_access_token: str
|
||||
new_access_token_expire_time: datetime
|
||||
session_uuid: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AccessToken:
|
||||
access_token: str
|
||||
@@ -54,6 +47,15 @@ class RefreshToken:
|
||||
refresh_token_expire_time: datetime
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NewToken:
|
||||
new_access_token: str
|
||||
new_access_token_expire_time: datetime
|
||||
new_refresh_token: str
|
||||
new_refresh_token_expire_time: datetime
|
||||
session_uuid: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TokenPayload:
|
||||
id: int
|
||||
@@ -64,3 +66,12 @@ class TokenPayload:
|
||||
@dataclasses.dataclass
|
||||
class UploadUrl:
|
||||
url: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SnowflakeInfo:
|
||||
timestamp: int
|
||||
datetime: str
|
||||
cluster_id: int
|
||||
node_id: int
|
||||
sequence: int
|
||||
|
||||
@@ -44,6 +44,8 @@ class MenuType(IntEnum):
|
||||
directory = 0
|
||||
menu = 1
|
||||
button = 2
|
||||
embedded = 3
|
||||
link = 4
|
||||
|
||||
|
||||
class RoleDataRuleOperatorType(IntEnum):
|
||||
@@ -119,3 +121,19 @@ class FileType(StrEnum):
|
||||
|
||||
image = 'image'
|
||||
video = 'video'
|
||||
|
||||
|
||||
class PluginType(StrEnum):
|
||||
"""插件类型"""
|
||||
|
||||
zip = 'zip'
|
||||
git = 'git'
|
||||
|
||||
|
||||
class UserPermissionType(StrEnum):
|
||||
"""用户权限类型"""
|
||||
|
||||
superuser = 'superuser'
|
||||
staff = 'staff'
|
||||
status = 'status'
|
||||
multi_login = 'multi_login'
|
||||
|
||||
@@ -85,7 +85,7 @@ class GatewayError(BaseExceptionMixin):
|
||||
class AuthorizationError(BaseExceptionMixin):
|
||||
"""授权异常"""
|
||||
|
||||
code = StandardResponseCode.HTTP_401
|
||||
code = StandardResponseCode.HTTP_403
|
||||
|
||||
def __init__(self, *, msg: str = 'Permission Denied', data: Any = None, background: BackgroundTask | None = None):
|
||||
super().__init__(msg=msg, data=data, background=background)
|
||||
@@ -98,3 +98,12 @@ class TokenError(HTTPError):
|
||||
|
||||
def __init__(self, *, msg: str = 'Not Authenticated', headers: dict[str, Any] | None = None):
|
||||
super().__init__(code=self.code, msg=msg, headers=headers or {'WWW-Authenticate': 'Bearer'})
|
||||
|
||||
|
||||
class ConflictError(BaseExceptionMixin):
|
||||
"""资源冲突异常"""
|
||||
|
||||
code = StandardResponseCode.HTTP_409
|
||||
|
||||
def __init__(self, *, msg: str = 'Conflict', data: Any = None, background: BackgroundTask | None = None):
|
||||
super().__init__(msg=msg, data=data, background=background)
|
||||
|
||||
@@ -76,7 +76,7 @@ async def _validation_exception_handler(request: Request, exc: RequestValidation
|
||||
}
|
||||
request.state.__request_validation_exception__ = content # 用于在中间件中获取异常信息
|
||||
content.update(trace_id=get_request_trace_id(request))
|
||||
return MsgSpecJSONResponse(status_code=422, content=content)
|
||||
return MsgSpecJSONResponse(status_code=StandardResponseCode.HTTP_422, content=content)
|
||||
|
||||
|
||||
def register_exception(app: FastAPI):
|
||||
|
||||
+28
-2
@@ -3,17 +3,43 @@
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy import BigInteger, DateTime
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column
|
||||
|
||||
from backend.utils.snowflake import snowflake
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
# 通用 Mapped 类型主键, 需手动添加,参考以下使用方式
|
||||
# MappedBase -> id: Mapped[id_key]
|
||||
# DataClassBase && Base -> id: Mapped[id_key] = mapped_column(init=False)
|
||||
id_key = Annotated[
|
||||
int, mapped_column(primary_key=True, index=True, autoincrement=True, sort_order=-999, comment='主键 ID')
|
||||
int,
|
||||
mapped_column(
|
||||
BigInteger,
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
index=True,
|
||||
autoincrement=True,
|
||||
sort_order=-999,
|
||||
comment='主键 ID',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# 雪花算法 Mapped 类型主键,使用方法与 id_key 相同
|
||||
# 详情:https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/pk.html
|
||||
snowflake_id_key = Annotated[
|
||||
int,
|
||||
mapped_column(
|
||||
BigInteger,
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
index=True,
|
||||
default=snowflake.generate,
|
||||
sort_order=-999,
|
||||
comment='雪花算法主键 ID',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -23,25 +23,8 @@ class CustomResponseCode(CustomCodeBase):
|
||||
"""自定义响应状态码"""
|
||||
|
||||
HTTP_200 = (200, '请求成功')
|
||||
HTTP_201 = (201, '新建请求成功')
|
||||
HTTP_202 = (202, '请求已接受,但处理尚未完成')
|
||||
HTTP_204 = (204, '请求成功,但没有返回内容')
|
||||
HTTP_400 = (400, '请求错误')
|
||||
HTTP_401 = (401, '未经授权')
|
||||
HTTP_403 = (403, '禁止访问')
|
||||
HTTP_404 = (404, '请求的资源不存在')
|
||||
HTTP_410 = (410, '请求的资源已永久删除')
|
||||
HTTP_422 = (422, '请求参数非法')
|
||||
HTTP_425 = (425, '无法执行请求,由于服务器无法满足要求')
|
||||
HTTP_429 = (429, '请求过多,服务器限制')
|
||||
HTTP_500 = (500, '服务器内部错误')
|
||||
HTTP_502 = (502, '网关错误')
|
||||
HTTP_503 = (503, '服务器暂时无法处理请求')
|
||||
HTTP_504 = (504, '网关超时')
|
||||
|
||||
# Plugin
|
||||
PLUGIN_INSTALL_SUCCESS = (200, '插件安装成功,请根据插件说明(README.md)进行相关配置并重启服务')
|
||||
PLUGIN_UNINSTALL_SUCCESS = (200, '插件卸载成功,请根据插件说明(README.md)移除相关配置并重启服务')
|
||||
|
||||
|
||||
class CustomErrorCode(CustomCodeBase):
|
||||
|
||||
@@ -6,8 +6,9 @@ from datetime import timedelta
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi.security import HTTPBearer
|
||||
from fastapi.security.http import HTTPAuthorizationCredentials
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from jose import ExpiredSignatureError, JWTError, jwt
|
||||
from pwdlib import PasswordHash
|
||||
@@ -18,15 +19,33 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from backend.app.admin.model import User
|
||||
from backend.app.admin.schema.user import GetUserInfoWithRelationDetail
|
||||
from backend.common.dataclasses import AccessToken, NewToken, RefreshToken, TokenPayload
|
||||
from backend.common.exception.errors import AuthorizationError, TokenError
|
||||
from backend.common.exception import errors
|
||||
from backend.common.exception.errors import TokenError
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import async_db_session
|
||||
from backend.database.redis import redis_client
|
||||
from backend.utils.serializers import select_as_dict
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
class CustomHTTPBearer(HTTPBearer):
|
||||
"""
|
||||
自定义 HTTPBearer 认证类
|
||||
|
||||
Issues: https://github.com/fastapi/fastapi/issues/10177
|
||||
"""
|
||||
|
||||
async def __call__(self, request: Request) -> HTTPAuthorizationCredentials | None:
|
||||
try:
|
||||
return await super().__call__(request)
|
||||
except HTTPException as e:
|
||||
if e.status_code == 403:
|
||||
raise TokenError()
|
||||
raise e
|
||||
|
||||
|
||||
# JWT authorizes dependency injection
|
||||
DependsJwtAuth = Depends(HTTPBearer())
|
||||
DependsJwtAuth = Depends(CustomHTTPBearer())
|
||||
|
||||
password_hash = PasswordHash((BcryptHasher(),))
|
||||
|
||||
@@ -60,11 +79,7 @@ def jwt_encode(payload: dict[str, Any]) -> str:
|
||||
:param payload: 载荷
|
||||
:return:
|
||||
"""
|
||||
return jwt.encode(
|
||||
payload,
|
||||
settings.TOKEN_SECRET_KEY,
|
||||
settings.TOKEN_ALGORITHM,
|
||||
)
|
||||
return jwt.encode(payload, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM)
|
||||
|
||||
|
||||
def jwt_decode(token: str) -> TokenPayload:
|
||||
@@ -75,20 +90,27 @@ def jwt_decode(token: str) -> TokenPayload:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.TOKEN_SECRET_KEY, algorithms=[settings.TOKEN_ALGORITHM])
|
||||
session_uuid = payload.get('session_uuid') or 'debug'
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.TOKEN_SECRET_KEY,
|
||||
algorithms=[settings.TOKEN_ALGORITHM],
|
||||
options={'verify_exp': True},
|
||||
)
|
||||
session_uuid = payload.get('session_uuid')
|
||||
user_id = payload.get('sub')
|
||||
expire_time = payload.get('exp')
|
||||
if not user_id:
|
||||
raise TokenError(msg='Token 无效')
|
||||
expire = payload.get('exp')
|
||||
if not session_uuid or not user_id or not expire:
|
||||
raise errors.TokenError(msg='Token 无效')
|
||||
except ExpiredSignatureError:
|
||||
raise TokenError(msg='Token 已过期')
|
||||
raise errors.TokenError(msg='Token 已过期')
|
||||
except (JWTError, Exception):
|
||||
raise TokenError(msg='Token 无效')
|
||||
return TokenPayload(id=int(user_id), session_uuid=session_uuid, expire_time=expire_time)
|
||||
raise errors.TokenError(msg='Token 无效')
|
||||
return TokenPayload(
|
||||
id=int(user_id), session_uuid=session_uuid, expire_time=timezone.from_datetime(timezone.to_utc(expire))
|
||||
)
|
||||
|
||||
|
||||
async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> AccessToken:
|
||||
async def create_access_token(user_id: int, multi_login: bool, **kwargs) -> AccessToken:
|
||||
"""
|
||||
生成加密 token
|
||||
|
||||
@@ -101,8 +123,8 @@ async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> Acce
|
||||
session_uuid = str(uuid4())
|
||||
access_token = jwt_encode({
|
||||
'session_uuid': session_uuid,
|
||||
'exp': expire,
|
||||
'sub': user_id,
|
||||
'exp': timezone.to_utc(expire).timestamp(),
|
||||
'sub': str(user_id),
|
||||
})
|
||||
|
||||
if not multi_login:
|
||||
@@ -117,7 +139,7 @@ async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> Acce
|
||||
# Token 附加信息单独存储
|
||||
if kwargs:
|
||||
await redis_client.setex(
|
||||
f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}',
|
||||
f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{user_id}:{session_uuid}',
|
||||
settings.TOKEN_EXPIRE_SECONDS,
|
||||
json.dumps(kwargs, ensure_ascii=False),
|
||||
)
|
||||
@@ -125,51 +147,65 @@ async def create_access_token(user_id: str, multi_login: bool, **kwargs) -> Acce
|
||||
return AccessToken(access_token=access_token, access_token_expire_time=expire, session_uuid=session_uuid)
|
||||
|
||||
|
||||
async def create_refresh_token(user_id: str, multi_login: bool) -> RefreshToken:
|
||||
async def create_refresh_token(session_uuid: str, user_id: int, multi_login: bool) -> RefreshToken:
|
||||
"""
|
||||
生成加密刷新 token,仅用于创建新的 token
|
||||
|
||||
:param session_uuid: 会话 UUID
|
||||
:param user_id: 用户 ID
|
||||
:param multi_login: 是否允许多端登录
|
||||
:return:
|
||||
"""
|
||||
expire = timezone.now() + timedelta(seconds=settings.TOKEN_REFRESH_EXPIRE_SECONDS)
|
||||
refresh_token = jwt_encode({'exp': expire, 'sub': user_id})
|
||||
refresh_token = jwt_encode({
|
||||
'session_uuid': session_uuid,
|
||||
'exp': timezone.to_utc(expire).timestamp(),
|
||||
'sub': str(user_id),
|
||||
})
|
||||
|
||||
if not multi_login:
|
||||
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}'
|
||||
await redis_client.delete_prefix(key_prefix)
|
||||
await redis_client.delete_prefix(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}')
|
||||
|
||||
await redis_client.setex(
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}',
|
||||
settings.TOKEN_REFRESH_EXPIRE_SECONDS,
|
||||
refresh_token,
|
||||
)
|
||||
return RefreshToken(refresh_token=refresh_token, refresh_token_expire_time=expire)
|
||||
|
||||
|
||||
async def create_new_token(user_id: str, refresh_token: str, multi_login: bool, **kwargs) -> NewToken:
|
||||
async def create_new_token(
|
||||
refresh_token: str, session_uuid: str, user_id: int, multi_login: bool, **kwargs
|
||||
) -> NewToken:
|
||||
"""
|
||||
生成新的 token
|
||||
|
||||
:param user_id: 用户 ID
|
||||
:param refresh_token: 刷新 token
|
||||
:param session_uuid: 会话 UUID
|
||||
:param user_id: 用户 ID
|
||||
:param multi_login: 是否允许多端登录
|
||||
:param kwargs: token 附加信息
|
||||
:return:
|
||||
"""
|
||||
redis_refresh_token = await redis_client.get(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{refresh_token}')
|
||||
redis_refresh_token = await redis_client.get(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||
if not redis_refresh_token or redis_refresh_token != refresh_token:
|
||||
raise TokenError(msg='Refresh Token 已过期,请重新登录')
|
||||
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
|
||||
|
||||
await redis_client.delete(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}')
|
||||
|
||||
new_access_token = await create_access_token(user_id, multi_login, **kwargs)
|
||||
new_refresh_token = await create_refresh_token(new_access_token.session_uuid, user_id, multi_login)
|
||||
return NewToken(
|
||||
new_access_token=new_access_token.access_token,
|
||||
new_access_token_expire_time=new_access_token.access_token_expire_time,
|
||||
new_refresh_token=new_refresh_token.refresh_token,
|
||||
new_refresh_token_expire_time=new_refresh_token.refresh_token_expire_time,
|
||||
session_uuid=new_access_token.session_uuid,
|
||||
)
|
||||
|
||||
|
||||
async def revoke_token(user_id: str, session_uuid: str) -> None:
|
||||
async def revoke_token(user_id: int, session_uuid: str) -> None:
|
||||
"""
|
||||
撤销 token
|
||||
|
||||
@@ -177,8 +213,8 @@ async def revoke_token(user_id: str, session_uuid: str) -> None:
|
||||
:param session_uuid: 会话 ID
|
||||
:return:
|
||||
"""
|
||||
token_key = f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{session_uuid}'
|
||||
await redis_client.delete(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}')
|
||||
|
||||
|
||||
def get_token(request: Request) -> str:
|
||||
@@ -191,7 +227,7 @@ def get_token(request: Request) -> str:
|
||||
authorization = request.headers.get('Authorization')
|
||||
scheme, token = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != 'bearer':
|
||||
raise TokenError(msg='Token 无效')
|
||||
raise errors.TokenError(msg='Token 无效')
|
||||
return token
|
||||
|
||||
|
||||
@@ -207,18 +243,18 @@ async def get_current_user(db: AsyncSession, pk: int) -> User:
|
||||
|
||||
user = await user_dao.get_with_relation(db, user_id=pk)
|
||||
if not user:
|
||||
raise TokenError(msg='Token 无效')
|
||||
raise errors.TokenError(msg='Token 无效')
|
||||
if not user.status:
|
||||
raise AuthorizationError(msg='用户已被锁定,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户已被锁定,请联系系统管理员')
|
||||
if user.dept_id:
|
||||
if not user.dept.status:
|
||||
raise AuthorizationError(msg='用户所属部门已被锁定,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户所属部门已被锁定,请联系系统管理员')
|
||||
if user.dept.del_flag:
|
||||
raise AuthorizationError(msg='用户所属部门已被删除,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户所属部门已被删除,请联系系统管理员')
|
||||
if user.roles:
|
||||
role_status = [role.status for role in user.roles]
|
||||
if all(status == 0 for status in role_status):
|
||||
raise AuthorizationError(msg='用户所属角色已被锁定,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户所属角色已被锁定,请联系系统管理员')
|
||||
return user
|
||||
|
||||
|
||||
@@ -231,7 +267,7 @@ def superuser_verify(request: Request) -> bool:
|
||||
"""
|
||||
superuser = request.user.is_superuser
|
||||
if not superuser or not request.user.is_staff:
|
||||
raise AuthorizationError
|
||||
raise errors.AuthorizationError()
|
||||
return superuser
|
||||
|
||||
|
||||
@@ -246,10 +282,10 @@ async def jwt_authentication(token: str) -> GetUserInfoWithRelationDetail:
|
||||
user_id = token_payload.id
|
||||
redis_token = await redis_client.get(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
|
||||
if not redis_token:
|
||||
raise TokenError(msg='Token 已过期')
|
||||
raise errors.TokenError(msg='Token 已过期')
|
||||
|
||||
if token != redis_token:
|
||||
raise TokenError(msg='Token 已失效')
|
||||
raise errors.TokenError(msg='Token 已失效')
|
||||
|
||||
cache_user = await redis_client.get(f'{settings.JWT_USER_REDIS_PREFIX}:{user_id}')
|
||||
if not cache_user:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import ColumnElement, and_, or_
|
||||
@@ -9,13 +8,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
||||
from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperatorType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.exception.errors import ServerError
|
||||
from backend.core.conf import settings
|
||||
from backend.utils.import_parse import dynamic_import_data_model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import DataRule
|
||||
|
||||
|
||||
class RequestPermission:
|
||||
"""
|
||||
@@ -44,7 +39,7 @@ class RequestPermission:
|
||||
"""
|
||||
if settings.RBAC_ROLE_MENU_MODE:
|
||||
if not isinstance(self.value, str):
|
||||
raise ServerError
|
||||
raise errors.ServerError
|
||||
# 附加权限标识到请求状态
|
||||
request.state.permission = self.value
|
||||
|
||||
@@ -60,35 +55,39 @@ async def filter_data_permission(db: AsyncSession, request: Request) -> ColumnEl
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
# 获取用户角色和数据范围
|
||||
data_scopes = []
|
||||
# 是否过滤数据权限
|
||||
if request.user.is_superuser:
|
||||
return or_(1 == 1)
|
||||
|
||||
for role in request.user.roles:
|
||||
if not role.is_filter_scopes:
|
||||
return or_(1 == 1)
|
||||
|
||||
# 获取数据范围
|
||||
data_scope_ids = set()
|
||||
for role in request.user.roles:
|
||||
for scope in role.scopes:
|
||||
if scope.status:
|
||||
data_scopes.append(scope)
|
||||
data_scope_ids.add(scope.id)
|
||||
|
||||
# 超级管理员和无规则用户不做过滤
|
||||
if request.user.is_superuser or not data_scopes:
|
||||
# 无规则用户不做过滤
|
||||
if not list(data_scope_ids):
|
||||
return or_(1 == 1)
|
||||
|
||||
# 获取数据范围规则
|
||||
data_rule_list: list[DataRule] = []
|
||||
for data_scope in data_scopes:
|
||||
data_scope_with_relation = await data_scope_dao.get_with_relation(db, data_scope.id)
|
||||
data_rule_list.extend(data_scope_with_relation.rules)
|
||||
unique_data_rules = {}
|
||||
for data_scope_id in list(data_scope_ids):
|
||||
data_scope_with_relation = await data_scope_dao.get_with_relation(db, data_scope_id)
|
||||
for rule in data_scope_with_relation.rules:
|
||||
unique_data_rules[rule.id] = rule
|
||||
|
||||
# 去重
|
||||
seen_data_rule_ids = set()
|
||||
new_data_rule_list = []
|
||||
for rule in data_rule_list:
|
||||
if rule.id not in seen_data_rule_ids:
|
||||
seen_data_rule_ids.add(rule.id)
|
||||
new_data_rule_list.append(rule)
|
||||
# 转换为列表
|
||||
data_rule_list = list(unique_data_rules.values())
|
||||
|
||||
where_and_list = []
|
||||
where_or_list = []
|
||||
|
||||
for data_rule in new_data_rule_list:
|
||||
for data_rule in data_rule_list:
|
||||
# 验证规则模型
|
||||
rule_model = data_rule.model
|
||||
if rule_model not in settings.DATA_PERMISSION_MODELS:
|
||||
|
||||
@@ -4,7 +4,6 @@ from fastapi import Depends, Request
|
||||
|
||||
from backend.common.enums import MethodType, StatusType
|
||||
from backend.common.exception import errors
|
||||
from backend.common.exception.errors import AuthorizationError, TokenError
|
||||
from backend.common.log import log
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
from backend.core.conf import settings
|
||||
@@ -27,7 +26,7 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
|
||||
|
||||
# JWT 授权状态强制校验
|
||||
if not request.auth.scopes:
|
||||
raise TokenError
|
||||
raise errors.TokenError
|
||||
|
||||
# 超级管理员免校验
|
||||
if request.user.is_superuser:
|
||||
@@ -36,17 +35,17 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
|
||||
# 检测用户角色
|
||||
user_roles = request.user.roles
|
||||
if not user_roles or all(status == 0 for status in user_roles):
|
||||
raise AuthorizationError(msg='用户未分配角色,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户未分配角色,请联系系统管理员')
|
||||
|
||||
# 检测用户所属角色菜单
|
||||
if not any(len(role.menus) > 0 for role in user_roles):
|
||||
raise AuthorizationError(msg='用户未分配菜单,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户未分配菜单,请联系系统管理员')
|
||||
|
||||
# 检测后台管理操作权限
|
||||
method = request.method
|
||||
if method != MethodType.GET or method != MethodType.OPTIONS:
|
||||
if not request.user.is_staff:
|
||||
raise AuthorizationError(msg='用户已被禁止后台管理操作,请联系系统管理员')
|
||||
raise errors.AuthorizationError(msg='用户已被禁止后台管理操作,请联系系统管理员')
|
||||
|
||||
# RBAC 鉴权
|
||||
if settings.RBAC_ROLE_MENU_MODE:
|
||||
@@ -60,14 +59,19 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
|
||||
if path_auth_perm in settings.RBAC_ROLE_MENU_EXCLUDE:
|
||||
return
|
||||
|
||||
# 已分配菜单权限校验
|
||||
allow_perms = []
|
||||
# 菜单去重
|
||||
unique_menus = {}
|
||||
for role in user_roles:
|
||||
for menu in role.menus:
|
||||
if menu.perms and menu.status == StatusType.enable:
|
||||
allow_perms.extend(menu.perms.split(','))
|
||||
unique_menus[menu.id] = menu
|
||||
|
||||
# 已分配菜单权限校验
|
||||
allow_perms = []
|
||||
for menu in list(unique_menus.values()):
|
||||
if menu.perms and menu.status == StatusType.enable:
|
||||
allow_perms.extend(menu.perms.split(','))
|
||||
if path_auth_perm not in allow_perms:
|
||||
raise AuthorizationError
|
||||
raise errors.AuthorizationError
|
||||
else:
|
||||
try:
|
||||
casbin_rbac = import_module_cached('backend.plugin.casbin_rbac.rbac')
|
||||
|
||||
@@ -45,7 +45,7 @@ class Settings(BaseSettings):
|
||||
# FastAPI
|
||||
FASTAPI_API_V1_PATH: str = '/api/v1'
|
||||
FASTAPI_TITLE: str = 'FastAPI'
|
||||
FASTAPI_VERSION: str = '0.0.1'
|
||||
FASTAPI_VERSION: str = '1.5.0'
|
||||
FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture'
|
||||
FASTAPI_DOCS_URL: str = '/docs'
|
||||
FASTAPI_REDOC_URL: str = '/redoc'
|
||||
@@ -71,6 +71,7 @@ class Settings(BaseSettings):
|
||||
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba:refresh_token'
|
||||
TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT / RBAC 路由白名单
|
||||
f'{FASTAPI_API_V1_PATH}/auth/login',
|
||||
f'{FASTAPI_API_V1_PATH}/auth/logout',
|
||||
]
|
||||
|
||||
# JWT
|
||||
@@ -241,9 +242,6 @@ class Settings(BaseSettings):
|
||||
# Plugin Code Generator
|
||||
CODE_GENERATOR_DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
|
||||
|
||||
# Plugin Config
|
||||
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def check_env(cls, values: Any) -> Any:
|
||||
|
||||
@@ -40,8 +40,6 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
# 创建数据库表
|
||||
await create_table()
|
||||
# 连接 redis
|
||||
await redis_client.open()
|
||||
# 初始化 limiter
|
||||
await FastAPILimiter.init(
|
||||
redis=redis_client,
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
||||
from rich.text import Text
|
||||
|
||||
from backend import console
|
||||
from backend.core.registrar import register_app
|
||||
from backend.plugin.tools import get_plugins, install_requirements
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
_print_log_style = f'{timezone.to_str(timezone.now(), "%Y-%m-%d %H:%M:%S.%M0")} | fba | - | '
|
||||
console.print(Text(f'{_print_log_style}检测插件依赖...', style='bold cyan'))
|
||||
|
||||
_plugins = get_plugins()
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(finished_text=f'[bold green]{_print_log_style}插件准备就绪[/]'),
|
||||
TextColumn('[bold green]{task.completed}/{task.total}[/]'),
|
||||
TimeElapsedColumn(),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task('安装插件依赖...', total=len(_plugins))
|
||||
for plugin in _plugins:
|
||||
install_requirements(plugin)
|
||||
progress.advance(task)
|
||||
|
||||
console.print(Text(f'{_print_log_style}启动服务...', style='bold magenta'))
|
||||
|
||||
app = register_app()
|
||||
|
||||
@@ -9,6 +9,6 @@ from backend.plugin.code_generator.api.v1.gen import router as gen_router
|
||||
|
||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
|
||||
|
||||
v1.include_router(gen_router)
|
||||
v1.include_router(business_router, prefix='/businesses')
|
||||
v1.include_router(model_router, prefix='/models')
|
||||
v1.include_router(gen_router, prefix='/codes')
|
||||
|
||||
@@ -20,12 +20,6 @@ from backend.plugin.code_generator.service.column_service import gen_model_servi
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
|
||||
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
|
||||
data = await gen_business_service.get_all()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
|
||||
async def get_business(
|
||||
pk: Annotated[int, Path(description='业务 ID')],
|
||||
@@ -34,11 +28,17 @@ async def get_business(
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
|
||||
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
|
||||
data = await gen_business_service.get_all()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
|
||||
async def get_business_all_models(
|
||||
pk: Annotated[int, Path(description='业务 ID')],
|
||||
) -> ResponseSchemaModel[list[GetGenModelDetail]]:
|
||||
data = await gen_model_service.get_by_business(business_id=pk)
|
||||
data = await gen_model_service.get_models(business_id=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async def get_business_all_models(
|
||||
summary='创建代码生成业务',
|
||||
deprecated=True,
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:business:add')),
|
||||
Depends(RequestPermission('codegen:business:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -60,7 +60,7 @@ async def create_business(obj: CreateGenBusinessParam) -> ResponseModel:
|
||||
'/{pk}',
|
||||
summary='更新代码生成业务',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:business:edit')),
|
||||
Depends(RequestPermission('codegen:business:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -77,7 +77,7 @@ async def update_business(
|
||||
'/{pk}',
|
||||
summary='删除代码生成业务',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:business:del')),
|
||||
Depends(RequestPermission('codegen:business:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ async def get_model(pk: Annotated[int, Path(description='模型 ID')]) -> Respon
|
||||
'',
|
||||
summary='创建代码生成模型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:model:add')),
|
||||
Depends(RequestPermission('codegen:model:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -43,7 +43,7 @@ async def create_model(obj: CreateGenModelParam) -> ResponseModel:
|
||||
'/{pk}',
|
||||
summary='更新代码生成模型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:model:edit')),
|
||||
Depends(RequestPermission('codegen:model:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -58,7 +58,7 @@ async def update_model(pk: Annotated[int, Path(description='模型 ID')], obj: U
|
||||
'/{pk}',
|
||||
summary='删除代码生成模型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:model:del')),
|
||||
Depends(RequestPermission('codegen:model:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -25,10 +25,10 @@ async def get_all_tables(
|
||||
|
||||
|
||||
@router.post(
|
||||
'/import',
|
||||
'/imports',
|
||||
summary='导入代码生成业务和模型列',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:import')),
|
||||
Depends(RequestPermission('codegen:table:import')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -37,24 +37,24 @@ async def import_table(obj: ImportParam) -> ResponseModel:
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth])
|
||||
@router.get('/{pk}/previews', summary='代码生成预览', dependencies=[DependsJwtAuth])
|
||||
async def preview_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
|
||||
data = await gen_service.preview(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
|
||||
async def generate_path(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
|
||||
@router.get('/{pk}/paths', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
|
||||
async def get_generate_paths(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
|
||||
data = await gen_service.get_generate_path(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/generate/{pk}',
|
||||
'/{pk}/generation',
|
||||
summary='代码生成',
|
||||
description='文件磁盘写入,请谨慎操作',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:generate')),
|
||||
Depends(RequestPermission('codegen:local:write')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -63,7 +63,7 @@ async def generate_code(pk: Annotated[int, Path(description='业务 ID')]) -> Re
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
|
||||
@router.get('/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
|
||||
async def download_code(pk: Annotated[int, Path(description='业务 ID')]):
|
||||
bio = await gen_service.download(pk=pk)
|
||||
return StreamingResponse(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
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
|
||||
@@ -33,6 +33,6 @@ class GenColumn(DataClassBase):
|
||||
|
||||
# 代码生成业务模型一对多
|
||||
gen_business_id: Mapped[int] = mapped_column(
|
||||
ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
|
||||
BigInteger, ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
|
||||
)
|
||||
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_column')
|
||||
|
||||
@@ -43,7 +43,7 @@ class GenBusinessService:
|
||||
async with async_db_session.begin() as db:
|
||||
business = await gen_business_dao.get_by_name(db, obj.table_name)
|
||||
if business:
|
||||
raise errors.ForbiddenError(msg='代码生成业务已存在')
|
||||
raise errors.ConflictError(msg='代码生成业务已存在')
|
||||
await gen_business_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -36,7 +36,7 @@ class GenModelService:
|
||||
return types
|
||||
|
||||
@staticmethod
|
||||
async def get_by_business(*, business_id: int) -> Sequence[GenColumn]:
|
||||
async def get_models(*, business_id: int) -> Sequence[GenColumn]:
|
||||
"""
|
||||
获取指定业务的所有模型
|
||||
|
||||
@@ -76,7 +76,7 @@ class GenModelService:
|
||||
if obj.name != model.name:
|
||||
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
|
||||
if obj.name in [gen_model.name for gen_model in gen_models]:
|
||||
raise errors.ForbiddenError(msg='模型列名已存在')
|
||||
raise errors.ConflictError(msg='模型列名已存在')
|
||||
|
||||
pd_type = sql_type_to_pydantic(obj.type)
|
||||
return await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
|
||||
|
||||
@@ -55,7 +55,7 @@ class GenService:
|
||||
|
||||
business_info = await gen_business_dao.get_by_name(db, obj.table_name)
|
||||
if business_info:
|
||||
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
|
||||
raise errors.ConflictError(msg='已存在相同数据库表业务')
|
||||
|
||||
table_name = table_info[0]
|
||||
new_business = GenBusiness(
|
||||
@@ -99,7 +99,7 @@ class GenService:
|
||||
:param business: 业务对象
|
||||
:return:
|
||||
"""
|
||||
gen_models = await gen_model_service.get_by_business(business_id=business.id)
|
||||
gen_models = await gen_model_service.get_models(business_id=business.id)
|
||||
if not gen_models:
|
||||
raise errors.NotFoundError(msg='代码生成模型表为空')
|
||||
|
||||
@@ -175,8 +175,9 @@ class GenService:
|
||||
code_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
init_filepath = code_folder.joinpath('__init__.py')
|
||||
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
||||
await f.write(gen_template.init_content)
|
||||
if not os.path.exists(init_filepath):
|
||||
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
||||
await f.write(gen_template.init_content)
|
||||
|
||||
# api __init__.py
|
||||
if 'api' in str_code_filepath:
|
||||
|
||||
@@ -4,7 +4,12 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
|
||||
from backend.app.{{ app_name }}.schema.{{ table_name }} import Create{{ schema_name }}Param, Get{{ schema_name }}Detail, Update{{ schema_name }}Param
|
||||
from backend.app.{{ app_name }}.schema.{{ table_name }} import (
|
||||
Create{{ schema_name }}Param,
|
||||
Delete{{ schema_name }}Param,
|
||||
Get{{ schema_name }}Detail,
|
||||
Update{{ schema_name }}Param,
|
||||
)
|
||||
from backend.app.{{ app_name }}.service.{{ table_name }}_service import {{ table_name }}_service
|
||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
@@ -30,7 +35,7 @@ async def get_{{ table_name }}(pk: Annotated[int, Path(description='{{ doc_comme
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_{{ table_name }}s(db: CurrentSession) -> ResponseSchemaModel[PageData[Get{{ schema_name }}Detail]]:
|
||||
async def get_{{ table_name }}s_paged(db: CurrentSession) -> ResponseSchemaModel[PageData[Get{{ schema_name }}Detail]]:
|
||||
{{ table_name }}_select = await {{ table_name }}_service.get_select()
|
||||
page_data = await paging_data(db, {{ table_name }}_select)
|
||||
return response_base.success(data=page_data)
|
||||
@@ -72,8 +77,8 @@ async def update_{{ table_name }}(pk: Annotated[int, Path(description='{{ doc_co
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_{{ table_name }}(pk: Annotated[list[int], Query(description='{{ doc_comment }} ID 列表')]) -> ResponseModel:
|
||||
count = await {{ table_name }}_service.delete(pk=pk)
|
||||
async def delete_{{ table_name }}s(obj: Delete{{ schema_name }}Param) -> ResponseModel:
|
||||
count = await {{ table_name }}_service.delete(obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
@@ -55,15 +55,15 @@ class CRUD{{ class_name }}(CRUDPlus[{{ schema_name }}]):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
删除{{ doc_comment }}
|
||||
批量删除{{ doc_comment }}
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: {{ doc_comment }} ID
|
||||
:param pks: {{ doc_comment }} 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)
|
||||
|
||||
|
||||
{{ instance_name }}_dao: CRUD{{ class_name }} = CRUD{{ class_name }}({{ class_name }})
|
||||
|
||||
@@ -23,6 +23,10 @@ class Update{{ schema_name }}Param({{ schema_name }}SchemaBase):
|
||||
"""更新{{ doc_comment }}参数"""
|
||||
|
||||
|
||||
class Delete{{ schema_name }}Param({{ schema_name }}SchemaBase):
|
||||
"""删除{{ doc_comment }}参数"""
|
||||
|
||||
|
||||
class Get{{ schema_name }}Detail({{ schema_name }}SchemaBase):
|
||||
"""{{ doc_comment }}详情"""
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy import Select
|
||||
|
||||
from backend.app.{{ app_name }}.crud.crud_{{ table_name }} import {{ table_name }}_dao
|
||||
from backend.app.{{ app_name }}.model import {{ class_name }}
|
||||
from backend.app.{{ app_name }}.schema.{{ table_name }} import Create{{ schema_name }}Param, Update{{ schema_name }}Param
|
||||
from backend.app.{{ app_name }}.schema.{{ table_name }} import Create{{ schema_name }}Param, Delete{{ schema_name }}Param, Update{{ schema_name }}Param
|
||||
from backend.common.exception import errors
|
||||
from backend.database.db import async_db_session
|
||||
|
||||
@@ -63,15 +63,15 @@ class {{ class_name }}Service:
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, obj: Delete{{ schema_name }}Param) -> int:
|
||||
"""
|
||||
删除{{ doc_comment }}
|
||||
|
||||
:param pk: {{ doc_comment }} ID 列表
|
||||
:param obj: {{ doc_comment }} ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await {{ table_name }}_dao.delete(db, pk)
|
||||
count = await {{ table_name }}_dao.delete(db, obj.pks)
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## 参数配置
|
||||
|
||||
内置插件,可直接使用
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query
|
||||
|
||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
@@ -13,7 +13,6 @@ from backend.database.db import CurrentSession
|
||||
from backend.plugin.config.schema.config import (
|
||||
CreateConfigParam,
|
||||
GetConfigDetail,
|
||||
SaveBuiltInConfigParam,
|
||||
UpdateConfigParam,
|
||||
)
|
||||
from backend.plugin.config.service.config_service import config_service
|
||||
@@ -21,66 +20,9 @@ from backend.plugin.config.service.config_service import config_service
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/website', summary='获取网站参数配置', dependencies=[DependsJwtAuth])
|
||||
async def get_website_config() -> ResponseSchemaModel[list[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[list[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[list[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(description='参数配置 ID')]) -> ResponseSchemaModel[GetConfigDetail]:
|
||||
config = await config_service.get(pk)
|
||||
config = await config_service.get(pk=pk)
|
||||
return response_base.success(data=config)
|
||||
|
||||
|
||||
@@ -92,10 +34,10 @@ async def get_config(pk: Annotated[int, Path(description='参数配置 ID')]) ->
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_configs(
|
||||
async def get_configs_paged(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='参数配置名称')] = None,
|
||||
type: Annotated[str | None, Query()] = None,
|
||||
type: Annotated[str | None, Query(description='参数配置类型')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetConfigDetail]]:
|
||||
config_select = await config_service.get_select(name=name, type=type)
|
||||
page_data = await paging_data(db, config_select)
|
||||
@@ -138,8 +80,8 @@ async def update_config(pk: Annotated[int, Path(description='参数配置 ID')],
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_config(pk: Annotated[list[int], Query(description='参数配置 ID 列表')]) -> ResponseModel:
|
||||
count = await config_service.delete(pk=pk)
|
||||
async def delete_configs(pks: Annotated[list[int], Body(description='参数配置 ID 列表')]) -> ResponseModel:
|
||||
count = await config_service.delete(pks=pks)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#!/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.core.conf import settings
|
||||
from backend.plugin.config.model import Config
|
||||
from backend.plugin.config.schema.config import CreateConfigParam, UpdateConfigParam
|
||||
|
||||
@@ -22,28 +20,7 @@ class CRUDConfig(CRUDPlus[Config]):
|
||||
:param pk: 参数配置 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, id=pk, type__not_in=settings.CONFIG_BUILT_IN_TYPES)
|
||||
|
||||
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
|
||||
"""
|
||||
通过类型获取参数配置
|
||||
|
||||
: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:
|
||||
"""
|
||||
通过键名和类型获取参数配置
|
||||
|
||||
:param db: 数据库会话
|
||||
:param key: 参数配置键名
|
||||
:param type: 参数配置类型
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model_by_column(db, key=key, type=type)
|
||||
return await self.select_model_by_column(db, id=pk)
|
||||
|
||||
async def get_by_key(self, db: AsyncSession, key: str) -> Config | None:
|
||||
"""
|
||||
@@ -63,11 +40,13 @@ class CRUDConfig(CRUDPlus[Config]):
|
||||
:param type: 参数配置类型
|
||||
:return:
|
||||
"""
|
||||
filters = {'type__not_in': settings.CONFIG_BUILT_IN_TYPES}
|
||||
filters = {}
|
||||
|
||||
if name is not None:
|
||||
filters.update(name__like=f'%{name}%')
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if type is not None:
|
||||
filters.update(type__like=f'%{type}%')
|
||||
filters['type__like'] = f'%{type}%'
|
||||
|
||||
return await self.select_order('created_time', 'desc', **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateConfigParam) -> None:
|
||||
@@ -91,17 +70,15 @@ class CRUDConfig(CRUDPlus[Config]):
|
||||
"""
|
||||
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: 参数配置 ID 列表
|
||||
:param pks: 参数配置 ID 列表
|
||||
:return:
|
||||
"""
|
||||
return await self.delete_model_by_column(
|
||||
db, allow_multiple=True, id__in=pk, type__not_in=settings.CONFIG_BUILT_IN_TYPES
|
||||
)
|
||||
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pks)
|
||||
|
||||
|
||||
config_dao: CRUDConfig = CRUDConfig(Config)
|
||||
|
||||
@@ -5,7 +5,7 @@ description = '通常用于前端工程数据展示'
|
||||
author = 'wu-clan'
|
||||
|
||||
[app]
|
||||
include = 'admin'
|
||||
extend = 'admin'
|
||||
|
||||
[api.config]
|
||||
prefix = '/configs'
|
||||
|
||||
@@ -7,14 +7,6 @@ from pydantic import ConfigDict, Field
|
||||
from backend.common.schema import SchemaBase
|
||||
|
||||
|
||||
class SaveBuiltInConfigParam(SchemaBase):
|
||||
"""保存内置参数配置参数"""
|
||||
|
||||
name: str = Field(description='参数配置名称')
|
||||
key: str = Field(description='参数配置键名')
|
||||
value: str = Field(description='参数配置值')
|
||||
|
||||
|
||||
class ConfigSchemaBase(SchemaBase):
|
||||
"""参数配置基础模型"""
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import async_db_session
|
||||
from backend.plugin.config.crud.crud_config import config_dao
|
||||
from backend.plugin.config.model import Config
|
||||
from backend.plugin.config.schema.config import (
|
||||
CreateConfigParam,
|
||||
SaveBuiltInConfigParam,
|
||||
UpdateConfigParam,
|
||||
)
|
||||
|
||||
@@ -20,37 +17,7 @@ class ConfigService:
|
||||
"""参数配置服务类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_built_in_config(type: str) -> Sequence[Config]:
|
||||
"""
|
||||
获取内置参数配置
|
||||
|
||||
:param type: 参数配置类型
|
||||
:return:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
保存内置参数配置
|
||||
|
||||
:param objs: 参数配置参数列表
|
||||
:param type: 参数配置类型
|
||||
:return:
|
||||
"""
|
||||
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):
|
||||
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: int) -> Config:
|
||||
async def get(*, pk: int) -> Config:
|
||||
"""
|
||||
获取参数配置详情
|
||||
|
||||
@@ -83,11 +50,9 @@ class ConfigService:
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
if obj.type in 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} 已存在')
|
||||
raise errors.ConflictError(msg=f'参数配置 {obj.key} 已存在')
|
||||
await config_dao.create(db, obj)
|
||||
|
||||
@staticmethod
|
||||
@@ -106,20 +71,20 @@ class ConfigService:
|
||||
if config.key != obj.key:
|
||||
config = await config_dao.get_by_key(db, obj.key)
|
||||
if config:
|
||||
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
|
||||
raise errors.ConflictError(msg=f'参数配置 {obj.key} 已存在')
|
||||
count = await config_dao.update(db, pk, obj)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
async def delete(*, pk: list[int]) -> int:
|
||||
async def delete(*, pks: list[int]) -> int:
|
||||
"""
|
||||
删除参数配置
|
||||
批量删除参数配置
|
||||
|
||||
:param pk: 参数配置 ID 列表
|
||||
:param pks: 参数配置 ID 列表
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
count = await config_dao.delete(db, pk)
|
||||
count = await config_dao.delete(db, pks)
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## 字典管理
|
||||
|
||||
内置插件,可直接使用
|
||||
@@ -12,8 +12,8 @@ from backend.common.security.rbac import DependsRBAC
|
||||
from backend.database.db import CurrentSession
|
||||
from backend.plugin.dict.schema.dict_data import (
|
||||
CreateDictDataParam,
|
||||
DeleteDictDataParam,
|
||||
GetDictDataDetail,
|
||||
GetDictDataWithRelation,
|
||||
UpdateDictDataParam,
|
||||
)
|
||||
from backend.plugin.dict.service.dict_data_service import dict_data_service
|
||||
@@ -21,38 +21,48 @@ from backend.plugin.dict.service.dict_data_service import dict_data_service
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth])
|
||||
@router.get('/all', summary='获取所有字典数据', dependencies=[DependsJwtAuth])
|
||||
async def get_all_dict_datas() -> ResponseSchemaModel[list[GetDictDataDetail]]:
|
||||
data = await dict_data_service.get_all()
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取字典数据详情', dependencies=[DependsJwtAuth])
|
||||
async def get_dict_data(
|
||||
pk: Annotated[int, Path(description='字典数据 ID')],
|
||||
) -> ResponseSchemaModel[GetDictDataWithRelation]:
|
||||
) -> ResponseSchemaModel[GetDictDataDetail]:
|
||||
data = await dict_data_service.get(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有字典',
|
||||
summary='分页获取所有字典数据',
|
||||
dependencies=[
|
||||
DependsJwtAuth,
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_dict_datas(
|
||||
async def get_dict_datas_paged(
|
||||
db: CurrentSession,
|
||||
type_code: Annotated[str | None, Query(description='字典类型编码')] = None,
|
||||
label: Annotated[str | None, Query(description='字典数据标签')] = None,
|
||||
value: Annotated[str | None, Query(description='字典数据键值')] = None,
|
||||
status: Annotated[int | None, Query(description='状态')] = None,
|
||||
type_id: Annotated[int | None, Query(description='字典类型 ID')] = None,
|
||||
) -> ResponseSchemaModel[PageData[GetDictDataDetail]]:
|
||||
dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status)
|
||||
dict_data_select = await dict_data_service.get_select(
|
||||
type_code=type_code, label=label, value=value, status=status, type_id=type_id
|
||||
)
|
||||
page_data = await paging_data(db, dict_data_select)
|
||||
return response_base.success(data=page_data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'',
|
||||
summary='创建字典',
|
||||
summary='创建字典数据',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:data:add')),
|
||||
Depends(RequestPermission('dict:data:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -63,9 +73,9 @@ async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel:
|
||||
|
||||
@router.put(
|
||||
'/{pk}',
|
||||
summary='更新字典',
|
||||
summary='更新字典数据',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:data:edit')),
|
||||
Depends(RequestPermission('dict:data:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -80,14 +90,14 @@ async def update_dict_data(
|
||||
|
||||
@router.delete(
|
||||
'',
|
||||
summary='批量删除字典',
|
||||
summary='批量删除字典数据',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:data:del')),
|
||||
Depends(RequestPermission('dict:data:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_dict_data(pk: Annotated[list[int], Query(description='字典数据 ID 列表')]) -> ResponseModel:
|
||||
count = await dict_data_service.delete(pk=pk)
|
||||
async def delete_dict_datas(obj: DeleteDictDataParam) -> ResponseModel:
|
||||
count = await dict_data_service.delete(obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
@@ -10,12 +10,25 @@ 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.plugin.dict.schema.dict_type import CreateDictTypeParam, GetDictTypeDetail, UpdateDictTypeParam
|
||||
from backend.plugin.dict.schema.dict_type import (
|
||||
CreateDictTypeParam,
|
||||
DeleteDictTypeParam,
|
||||
GetDictTypeDetail,
|
||||
UpdateDictTypeParam,
|
||||
)
|
||||
from backend.plugin.dict.service.dict_type_service import dict_type_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/{pk}', summary='获取字典类型详情', dependencies=[DependsJwtAuth])
|
||||
async def get_dict_type(
|
||||
pk: Annotated[int, Path(description='字典类型 ID')],
|
||||
) -> ResponseSchemaModel[GetDictTypeDetail]:
|
||||
data = await dict_type_service.get(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
'',
|
||||
summary='分页获取所有字典类型',
|
||||
@@ -24,7 +37,7 @@ router = APIRouter()
|
||||
DependsPagination,
|
||||
],
|
||||
)
|
||||
async def get_pagination_dict_types(
|
||||
async def get_dict_types_paged(
|
||||
db: CurrentSession,
|
||||
name: Annotated[str | None, Query(description='字典类型名称')] = None,
|
||||
code: Annotated[str | None, Query(description='字典类型编码')] = None,
|
||||
@@ -39,7 +52,7 @@ async def get_pagination_dict_types(
|
||||
'',
|
||||
summary='创建字典类型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:type:add')),
|
||||
Depends(RequestPermission('dict:type:add')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -52,7 +65,7 @@ async def create_dict_type(obj: CreateDictTypeParam) -> ResponseModel:
|
||||
'/{pk}',
|
||||
summary='更新字典类型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:type:edit')),
|
||||
Depends(RequestPermission('dict:type:edit')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -69,12 +82,12 @@ async def update_dict_type(
|
||||
'',
|
||||
summary='批量删除字典类型',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:dict:type:del')),
|
||||
Depends(RequestPermission('dict:type:del')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
async def delete_dict_type(pk: Annotated[list[int], Query(description='字典类型 ID 列表')]) -> ResponseModel:
|
||||
count = await dict_type_service.delete(pk=pk)
|
||||
async def delete_dict_types(obj: DeleteDictTypeParam) -> ResponseModel:
|
||||
count = await dict_type_service.delete(obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload, selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.plugin.dict.model import DictData
|
||||
@@ -20,31 +21,44 @@ class CRUDDictData(CRUDPlus[DictData]):
|
||||
:param pk: 字典数据 ID
|
||||
:return:
|
||||
"""
|
||||
return await self.select_model(db, pk)
|
||||
return await self.select_model(db, pk, load_strategies={'type': 'noload'})
|
||||
|
||||
async def get_list(self, label: str | None, value: str | None, status: int | None) -> Select:
|
||||
async def get_all(self, db: AsyncSession) -> Sequence[DictData]:
|
||||
"""
|
||||
获取所有字典数据
|
||||
|
||||
:param db: 数据库会话
|
||||
:return:
|
||||
"""
|
||||
return await self.select_models(db, load_strategies={'type': 'noload'})
|
||||
|
||||
async def get_list(
|
||||
self, type_code: str | None, label: str | None, value: str | None, status: int | None, type_id: int | None
|
||||
) -> Select:
|
||||
"""
|
||||
获取字典数据列表
|
||||
|
||||
:param type_code: 字典类型编码
|
||||
:param label: 字典数据标签
|
||||
:param value: 字典数据键值
|
||||
:param status: 字典状态
|
||||
:param type_id: 字典类型 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if type_code is not None:
|
||||
filters['type_code'] = type_code
|
||||
if label is not None:
|
||||
filters.append(self.model.label.like(f'%{label}%'))
|
||||
filters['label__like'] = f'%{label}%'
|
||||
if value is not None:
|
||||
filters.append(self.model.value.like(f'%{value}%'))
|
||||
filters['value__like'] = f'%{value}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
if type_id is not None:
|
||||
filters['type_id'] = type_id
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', 'desc', load_strategies={'type': 'noload'}, **filters)
|
||||
|
||||
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
|
||||
"""
|
||||
@@ -56,36 +70,43 @@ class CRUDDictData(CRUDPlus[DictData]):
|
||||
"""
|
||||
return await self.select_model_by_column(db, label=label)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateDictDataParam) -> None:
|
||||
async def create(self, db: AsyncSession, obj: CreateDictDataParam, type_code: str) -> None:
|
||||
"""
|
||||
创建字典数据
|
||||
|
||||
:param db: 数据库会话
|
||||
:param obj: 创建字典数据参数
|
||||
:param type_code: 字典类型编码
|
||||
:return:
|
||||
"""
|
||||
await self.create_model(db, obj)
|
||||
dict_obj = obj.model_dump()
|
||||
dict_obj.update({'type_code': type_code})
|
||||
new_data = self.model(**dict_obj)
|
||||
db.add(new_data)
|
||||
|
||||
async def update(self, db: AsyncSession, pk: int, obj: UpdateDictDataParam) -> int:
|
||||
async def update(self, db: AsyncSession, pk: int, obj: UpdateDictDataParam, type_code: str) -> int:
|
||||
"""
|
||||
更新字典数据
|
||||
|
||||
:param db: 数据库会话
|
||||
:param pk: 字典数据 ID
|
||||
:param obj: 更新字典数据参数
|
||||
:param type_code: 字典类型编码
|
||||
:return:
|
||||
"""
|
||||
return await self.update_model(db, pk, obj)
|
||||
dict_obj = obj.model_dump()
|
||||
dict_obj.update({'type_code': type_code})
|
||||
return await self.update_model(db, pk, dict_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: 字典数据 ID 列表
|
||||
: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 get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
|
||||
"""
|
||||
@@ -95,9 +116,7 @@ class CRUDDictData(CRUDPlus[DictData]):
|
||||
:param pk: 字典数据 ID
|
||||
: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()
|
||||
return await self.select_model(db, pk, load_strategies=['type'])
|
||||
|
||||
|
||||
dict_data_dao: CRUDDictData = CRUDDictData(DictData)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select, and_, 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.plugin.dict.model import DictType
|
||||
@@ -31,20 +30,16 @@ class CRUDDictType(CRUDPlus[DictType]):
|
||||
:param status: 字典状态
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(noload(self.model.datas)).order_by(desc(self.model.created_time))
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if code is not None:
|
||||
filters.append(self.model.code.like(f'%{code}%'))
|
||||
filters['code__like'] = f'%{code}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', 'desc', load_strategies={'datas': 'noload'}, **filters)
|
||||
|
||||
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
|
||||
"""
|
||||
@@ -77,15 +72,15 @@ class CRUDDictType(CRUDPlus[DictType]):
|
||||
"""
|
||||
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: 字典类型 ID 列表
|
||||
: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)
|
||||
|
||||
|
||||
dict_type_dao: CRUDDictType = CRUDDictType(DictType)
|
||||
|
||||
@@ -21,7 +21,8 @@ class DictData(Base):
|
||||
__tablename__ = 'sys_dict_data'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
label: Mapped[str] = mapped_column(String(32), unique=True, comment='字典标签')
|
||||
type_code: Mapped[str] = mapped_column(String(32), comment='对应的字典类型编码')
|
||||
label: Mapped[str] = mapped_column(String(32), comment='字典标签')
|
||||
value: Mapped[str] = mapped_column(String(32), comment='字典值')
|
||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user