first commit

main
zc 3 months ago
commit 743fe1017b
  1. 1
      .dockerignore
  2. 1
      .gitattributes
  3. 16
      .gitignore
  4. 33
      Dockerfile
  5. 21
      LICENSE
  6. 87
      Makefile
  7. 219
      README-en.md
  8. 244
      README.md
  9. 61
      app/__init__.py
  10. 11
      app/api/__init__.py
  11. 8
      app/api/chat/__init__.py
  12. 8
      app/api/chat/ai/__init__.py
  13. 104
      app/api/chat/ai/chat_router.py
  14. 161
      app/api/chat/ai/chat_service.py
  15. 22
      app/api/v1/__init__.py
  16. 8
      app/api/v1/apis/__init__.py
  17. 67
      app/api/v1/apis/apis.py
  18. 8
      app/api/v1/auditlog/__init__.py
  19. 48
      app/api/v1/auditlog/auditlog.py
  20. 8
      app/api/v1/base/__init__.py
  21. 103
      app/api/v1/base/base.py
  22. 8
      app/api/v1/depts/__init__.py
  23. 48
      app/api/v1/depts/depts.py
  24. 8
      app/api/v1/menus/__init__.py
  25. 63
      app/api/v1/menus/menus.py
  26. 8
      app/api/v1/roles/__init__.py
  27. 73
      app/api/v1/roles/roles.py
  28. 8
      app/api/v1/users/__init__.py
  29. 81
      app/api/v1/users/users.py
  30. 2
      app/controllers/__init__.py
  31. 45
      app/controllers/api.py
  32. 86
      app/controllers/dept.py
  33. 16
      app/controllers/menu.py
  34. 27
      app/controllers/role.py
  35. 60
      app/controllers/user.py
  36. 31
      app/core/bgtask.py
  37. 45
      app/core/crud.py
  38. 6
      app/core/ctx.py
  39. 53
      app/core/dependency.py
  40. 43
      app/core/exceptions.py
  41. 234
      app/core/init_app.py
  42. 182
      app/core/middlewares.py
  43. 1
      app/log/__init__.py
  44. 25
      app/log/log.py
  45. 8
      app/models/ChatIn.py
  46. 2
      app/models/__init__.py
  47. 89
      app/models/admin.py
  48. 62
      app/models/base.py
  49. 19
      app/models/enums.py
  50. 1
      app/schemas/__init__.py
  51. 17
      app/schemas/apis.py
  52. 52
      app/schemas/base.py
  53. 18
      app/schemas/depts.py
  54. 20
      app/schemas/login.py
  55. 52
      app/schemas/menus.py
  56. 32
      app/schemas/roles.py
  57. 44
      app/schemas/users.py
  58. 3
      app/settings/__init__.py
  59. 104
      app/settings/config.py
  60. 10
      app/utils/jwt_utils.py
  61. 16
      app/utils/password.py
  62. 5
      deploy/entrypoint.sh
  63. BIN
      deploy/sample-picture/1.jpg
  64. BIN
      deploy/sample-picture/2.jpg
  65. BIN
      deploy/sample-picture/3.jpg
  66. BIN
      deploy/sample-picture/api.jpg
  67. BIN
      deploy/sample-picture/group.jpg
  68. BIN
      deploy/sample-picture/login.jpg
  69. 1
      deploy/sample-picture/logo.svg
  70. BIN
      deploy/sample-picture/menu.jpg
  71. BIN
      deploy/sample-picture/role.jpg
  72. BIN
      deploy/sample-picture/user.jpg
  73. BIN
      deploy/sample-picture/workbench.jpg
  74. 13
      deploy/web.conf
  75. 95
      pyproject.toml
  76. 63
      requirements.txt
  77. 13
      run.py
  78. 1202
      uv.lock
  79. 69
      vue_fastapi_admin.egg-info/PKG-INFO
  80. 9
      vue_fastapi_admin.egg-info/SOURCES.txt
  81. 1
      vue_fastapi_admin.egg-info/dependency_links.txt
  82. 63
      vue_fastapi_admin.egg-info/requires.txt
  83. 1
      vue_fastapi_admin.egg-info/top_level.txt
  84. 3
      web/.env
  85. 8
      web/.env.development
  86. 11
      web/.env.production
  87. 62
      web/.eslint-global-variables.json
  88. 4
      web/.eslintignore
  89. 26
      web/.gitignore
  90. 3
      web/.prettierignore
  91. 6
      web/.prettierrc.json
  92. 20
      web/README.md
  93. 13
      web/build/config/define.js
  94. 1
      web/build/config/index.js
  95. 23
      web/build/constant.js
  96. 15
      web/build/plugin/html.js
  97. 35
      web/build/plugin/index.js
  98. 46
      web/build/plugin/unplugin.js
  99. 15
      web/build/script/build-cname.js
  100. 14
      web/build/script/index.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1 @@
web/node_modules

1
.gitattributes vendored

@ -0,0 +1 @@
*.html linguist-language=python

16
.gitignore vendored

@ -0,0 +1,16 @@
__pycache__/
.idea/
venv/
.mypy_cache/
.vscode
.ruff_cache/
.pytest_cache/
migrations/
db.sqlite3
db.sqlite3-journal
db.sqlite3-shm
db.sqlite3-wal
.DS_Store
._.DS_Store

@ -0,0 +1,33 @@
FROM node:18.12.0-alpine3.16 AS web
WORKDIR /opt/vue-fastapi-admin
COPY /web ./web
RUN cd /opt/vue-fastapi-admin/web && npm i --registry=https://registry.npmmirror.com && npm run build
FROM python:3.11-slim-bullseye
WORKDIR /opt/vue-fastapi-admin
ADD . .
COPY /deploy/entrypoint.sh .
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@http://mirrors.ustc.edu.cn@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apt-get update \
&& apt-get install -y --no-install-recommends gcc python3-dev bash nginx vim curl procps net-tools
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY --from=web /opt/vue-fastapi-admin/web/dist /opt/vue-fastapi-admin/web/dist
ADD /deploy/web.conf /etc/nginx/sites-available/web.conf
RUN rm -f /etc/nginx/sites-enabled/default \
&& ln -s /etc/nginx/sites-available/web.conf /etc/nginx/sites-enabled/
ENV LANG=zh_CN.UTF-8
EXPOSE 80
ENTRYPOINT [ "sh", "entrypoint.sh" ]

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 mizhexiaoxiao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,87 @@
# Build configuration
# -------------------
APP_NAME := `sed -n 's/^ *name.*=.*"\([^"]*\)".*/\1/p' pyproject.toml`
APP_VERSION := `sed -n 's/^ *version.*=.*"\([^"]*\)".*/\1/p' pyproject.toml`
GIT_REVISION = `git rev-parse HEAD`
# Introspection targets
# ---------------------
.PHONY: help
help: header targets
.PHONY: header
header:
@echo "\033[34mEnvironment\033[0m"
@echo "\033[34m---------------------------------------------------------------\033[0m"
@printf "\033[33m%-23s\033[0m" "APP_NAME"
@printf "\033[35m%s\033[0m" $(APP_NAME)
@echo ""
@printf "\033[33m%-23s\033[0m" "APP_VERSION"
@printf "\033[35m%s\033[0m" $(APP_VERSION)
@echo ""
@printf "\033[33m%-23s\033[0m" "GIT_REVISION"
@printf "\033[35m%s\033[0m" $(GIT_REVISION)
@echo "\n"
.PHONY: targets
targets:
@echo "\033[34mDevelopment Targets\033[0m"
@echo "\033[34m---------------------------------------------------------------\033[0m"
@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}'
# Development targets
# -------------
.PHONY: install
install: ## Install dependencies
uv add pyproject.toml
.PHONY: run
run: start
.PHONY: start
start: ## Starts the server
python run.py
# Check, lint and format targets
# ------------------------------
.PHONY: check
check: check-format lint
.PHONY: check-format
check-format: ## Dry-run code formatter
black ./ --check
isort ./ --profile black --check
.PHONY: lint
lint: ## Run ruff
ruff check ./app
.PHONY: format
format: ## Run code formatter
black ./
isort ./ --profile black
.PHONY: test
test: ## Run the test suite
$(eval include .env)
$(eval export $(sh sed 's/=.*//' .env))
pytest -vv -s --cache-clear ./
.PHONY: clean-db
clean-db: ## 删除migrations文件夹和db.sqlite3
find . -type d -name "migrations" -exec rm -rf {} +
rm -f db.sqlite3 db.sqlite3-shm db.sqlite3-wal
.PHONY: migrate
migrate: ## 运行aerich migrate命令生成迁移文件
aerich migrate
.PHONY: upgrade
upgrade: ## 运行aerich upgrade命令应用迁移
aerich upgrade

@ -0,0 +1,219 @@
<p align="center">
<a href="https://github.com/mizhexiaoxiao/vue-fastapi-admin">
<img alt="Vue FastAPI Admin Logo" width="200" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/logo.svg">
</a>
</p>
<h1 align="center">vue-fastapi-admin</h1>
English | [简体中文](./README.md)
vue-fastapi-admin is a modern front-end and back-end separation development platform that combines FastAPI, Vue3, and Naive UI. It incorporates RBAC (Role-Based Access Control) management, dynamic routing, and JWT (JSON Web Token) authentication, making it ideal for rapid development of small to medium-sized applications and also serves as a valuable learning resource.
### Features
- **Popular Tech Stack**: The backend is developed with the high-performance asynchronous framework FastAPI using Python 3.11, while the front-end is powered by cutting-edge technologies such as Vue3 and Vite, complemented by the efficient package manager, pnpm.
- **Code Standards**: The project is equipped with various plugins for code standardization and quality control, ensuring consistency and enhancing team collaboration efficiency.
- **Dynamic Routing**: Backend dynamic routing combined with the RBAC model allows for fine-grained control of menus and routing.
- **JWT Authentication**: User identity verification and authorization are handled through JWT, enhancing the application's security.
- **Granular Permission Control**: Implements detailed permission management including button and interface level controls, ensuring different roles and users have appropriate permissions.
### Live Demo
- URL: http://139.9.100.77:9999
- Username: admin
- Password: 123456
### Screenshots
#### Login Page
![Login Page](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/login.jpg)
#### Workbench
![Workbench](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/workbench.jpg)
#### User Management
![User Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/user.jpg)
#### Role Management
![Role Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/role.jpg)
#### Menu Management
![Menu Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/menu.jpg)
#### API Management
![API Management](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/api.jpg)
### Quick Start
Please follow the instructions below for installation and configuration:
#### Method 1:dockerhub pull image
```sh
docker pull mizhexiaoxiao/vue-fastapi-admin:latest
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 mizhexiaoxiao/vue-fastapi-admin
```
#### Method 2: Build Image Using Dockerfile
##### Install Docker
```sh
yum install -y docker-ce
systemctl start docker
```
##### Build the Image
```sh
git clone https://github.com/mizhexiaoxiao/vue-fastapi-admin.git
cd vue-fastapi-admin
docker build --no-cache . -t vue-fastapi-admin
```
##### Start the Container
```sh
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 vue-fastapi-admin
```
##### Access the Service
http://localhost:9999
username:admin
password:123456
### Local Setup
#### Backend
The backend service requires the following environment:
- Python 3.11
#### Method 1 (Recommended): Install Dependencies with uv
1. Install uv
```sh
pip install uv
```
2. Create and activate virtual environment
```sh
uv venv
source .venv/bin/activate # Linux/Mac
# or
.\.venv\Scripts\activate # Windows
```
3. Install dependencies
```sh
uv add pyproject.toml
```
4. Start the backend service
```sh
python run.py
```
#### Method 2: Install Dependencies with Pip
1. Create a Python virtual environment:
```sh
python3 -m venv venv
source venv/bin/activate # Linux/Mac
# or
.\venv\Scripts\activate # Windows
```
2. Install project dependencies:
```sh
pip install -r requirements.txt
```
3. Start the backend service:
```sh
python run.py
```
The backend service is now running, and you can visit http://localhost:9999/docs to view the API documentation.
#### Frontend
The frontend project requires a Node.js environment (recommended version 18.8.0 or higher).
- node v18.8.0+
1. Navigate to the frontend project directory:
```sh
cd web
```
2. Install project dependencies (pnpm is recommended: https://pnpm.io/zh/installation)
```sh
npm i -g pnpm # If pnpm is already installed, skip this step
pnpm i # Or use npm i
```
3. Start the frontend development server:
```sh
pnpm dev
```
### Directory Structure Explanation
```
├── app // Application directory
│ ├── api // API interface directory
│ │ └── v1 // Version 1 of the API interfaces
│ │ ├── apis // API-related interfaces
│ │ ├── base // Base information interfaces
│ │ ├── menus // Menu related interfaces
│ │ ├── roles // Role related interfaces
│ │ └── users // User related interfaces
│ ├── controllers // Controllers directory
│ ├── core // Core functionality module
│ ├── log // Log directory
│ ├── models // Data models directory
│ ├── schemas // Data schema/structure definitions
│ ├── settings // Configuration settings directory
│ └── utils // Utilities directory
├── deploy // Deployment related directory
│ └── sample-picture // Sample picture directory
└── web // Front-end web directory
├── build // Build scripts and configuration directory
│ ├── config // Build configurations
│ ├── plugin // Build plugins
│ └── script // Build scripts
├── public // Public resources directory
│ └── resource // Public resource files
├── settings // Front-end project settings
└── src // Source code directory
├── api // API interface definitions
├── assets // Static resources directory
│ ├── images // Image resources
│ ├── js // JavaScript files
│ └── svg // SVG vector files
├── components // Components directory
│ ├── common // Common components
│ ├── icon // Icon components
│ ├── page // Page components
│ ├── query-bar // Query bar components
│ └── table // Table components
├── composables // Composable functionalities
├── directives // Directives directory
├── layout // Layout directory
│ └── components // Layout components
├── router // Routing directory
│ ├── guard // Route guards
│ └── routes // Route definitions
├── store // State management (pinia)
│ └── modules // State modules
├── styles // Style files directory
├── utils // Utilities directory
│ ├── auth // Authentication related utilities
│ ├── common // Common utilities
│ ├── http // Encapsulated axios
│ └── storage // Encapsulated localStorage and sessionStorage
└── views // Views/Pages directory
├── error-page // Error pages
├── login // Login page
├── profile // Profile page
├── system // System management page
└── workbench // Workbench page
```
### Visitors Count
<img align="left" src = "https://profile-counter.glitch.me/vue-fastapi-admin/count.svg" alt="Loading">

@ -0,0 +1,244 @@
<p align="center">
<a href="https://github.com/mizhexiaoxiao/vue-fastapi-admin">
<img alt="Vue FastAPI Admin Logo" width="200" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/logo.svg">
</a>
</p>
<h1 align="center">vue-fastapi-admin</h1>
[English](./README-en.md) | 简体中文
基于 FastAPI + Vue3 + Naive UI 的现代化前后端分离开发平台,融合了 RBAC 权限管理、动态路由和 JWT 鉴权,助力中小型应用快速搭建,也可用于学习参考。
### 特性
- **最流行技术栈**:基于 Python 3.11 和 FastAPI 高性能异步框架,结合 Vue3 和 Vite 等前沿技术进行开发,同时使用高效的 npm 包管理器 pnpm。
- **代码规范**:项目内置丰富的规范插件,确保代码质量和一致性,有效提高团队协作效率。
- **动态路由**:后端动态路由,结合 RBAC(Role-Based Access Control)权限模型,提供精细的菜单路由控制。
- **JWT鉴权**:使用 JSON Web Token(JWT)进行身份验证和授权,增强应用的安全性。
- **细粒度权限控制**:实现按钮和接口级别的权限控制,确保不同用户或角色在界面操作和接口访问时具有不同的权限限制。
### 在线预览
- [http://47.111.145.81:3000](http://47.111.145.81:3000)
- username: admin
- password: 123456
### 登录页
![image](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/login.jpg)
### 工作台
![image](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/workbench.jpg)
### 用户管理
![image](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/user.jpg)
### 角色管理
![image](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/role.jpg)
### 菜单管理
![image](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/menu.jpg)
### API管理
![image](https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/api.jpg)
### 快速开始
#### 方法一:dockerhub拉取镜像
```sh
docker pull mizhexiaoxiao/vue-fastapi-admin:latest
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 mizhexiaoxiao/vue-fastapi-admin
```
#### 方法二:dockerfile构建镜像
##### docker安装(版本17.05+)
```sh
yum install -y docker-ce
systemctl start docker
```
##### 构建镜像
```sh
git clone https://github.com/mizhexiaoxiao/vue-fastapi-admin.git
cd vue-fastapi-admin
docker build --no-cache . -t vue-fastapi-admin
```
##### 启动容器
```sh
docker run -d --restart=always --name=vue-fastapi-admin -p 9999:80 vue-fastapi-admin
```
##### 访问
http://localhost:9999
username:admin
password:123456
### 本地启动
#### 后端
启动项目需要以下环境:
- Python 3.11
#### 方法一(推荐):使用 uv 安装依赖
1. 安装 uv
```sh
pip install uv
```
2. 创建并激活虚拟环境
```sh
uv venv
source .venv/bin/activate # Linux/Mac
# 或
.\.venv\Scripts\activate # Windows
```
3. 安装依赖
```sh
uv add pyproject.toml
```
4. 启动服务
```sh
python run.py
```
#### 方法二:使用 Pip 安装依赖
1. 创建虚拟环境
```sh
python3 -m venv venv
```
2. 激活虚拟环境
```sh
source venv/bin/activate # Linux/Mac
# 或
.\venv\Scripts\activate # Windows
```
3. 安装依赖
```sh
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
```
4. 启动服务
```sh
python run.py
```
服务现在应该正在运行,访问 http://localhost:9999/docs 查看API文档
#### 前端
启动项目需要以下环境:
- node v18.8.0+
1. 进入前端目录
```sh
cd web
```
2. 安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
```sh
npm i -g pnpm # 已安装可忽略
pnpm i # 或者 npm i
```
3. 启动
```sh
pnpm dev
```
### 目录说明
```
├── app // 应用程序目录
│ ├── api // API接口目录
│ │ └── v1 // 版本1的API接口
│ │ ├── apis // API相关接口
│ │ ├── base // 基础信息接口
│ │ ├── menus // 菜单相关接口
│ │ ├── roles // 角色相关接口
│ │ └── users // 用户相关接口
│ ├── controllers // 控制器目录
│ ├── core // 核心功能模块
│ ├── log // 日志目录
│ ├── models // 数据模型目录
│ ├── schemas // 数据模式/结构定义
│ ├── settings // 配置设置目录
│ └── utils // 工具类目录
├── deploy // 部署相关目录
│ └── sample-picture // 示例图片目录
└── web // 前端网页目录
├── build // 构建脚本和配置目录
│ ├── config // 构建配置
│ ├── plugin // 构建插件
│ └── script // 构建脚本
├── public // 公共资源目录
│ └── resource // 公共资源文件
├── settings // 前端项目配置
└── src // 源代码目录
├── api // API接口定义
├── assets // 静态资源目录
│ ├── images // 图片资源
│ ├── js // JavaScript文件
│ └── svg // SVG矢量图文件
├── components // 组件目录
│ ├── common // 通用组件
│ ├── icon // 图标组件
│ ├── page // 页面组件
│ ├── query-bar // 查询栏组件
│ └── table // 表格组件
├── composables // 可组合式功能块
├── directives // 指令目录
├── layout // 布局目录
│ └── components // 布局组件
├── router // 路由目录
│ ├── guard // 路由守卫
│ └── routes // 路由定义
├── store // 状态管理(pinia)
│ └── modules // 状态模块
├── styles // 样式文件目录
├── utils // 工具类目录
│ ├── auth // 认证相关工具
│ ├── common // 通用工具
│ ├── http // 封装axios
│ └── storage // 封装localStorage和sessionStorage
└── views // 视图/页面目录
├── error-page // 错误页面
├── login // 登录页面
├── profile // 个人资料页面
├── system // 系统管理页面
└── workbench // 工作台页面
```
### 进群交流
进群的条件是给项目一个star,小小的star是作者维护下去的动力。
你可以在群里提出任何疑问,我会尽快回复答疑。
<img width="300" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/group.jpg">
## 打赏
如果项目有帮助到你,可以请作者喝杯咖啡~
<div style="display: flex">
<img src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/1.jpg" width="300">
<img src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/2.jpg" width="300">
</div>
## 定制开发
如果有基于该项目的定制需求或其他合作,请添加下方微信,备注来意
<img width="300" src="https://github.com/mizhexiaoxiao/vue-fastapi-admin/blob/main/deploy/sample-picture/3.jpg">
### Visitors Count
<img align="left" src = "https://profile-counter.glitch.me/vue-fastapi-admin/count.svg" alt="Loading">

@ -0,0 +1,61 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from tortoise import Tortoise
from redis.asyncio import Redis
from app.core.exceptions import SettingNotFound
from app.core.init_app import (
init_data,
make_middlewares,
register_exceptions,
register_routers,
)
try:
from app.settings.config import settings
except ImportError:
raise SettingNotFound("Can not import settings")
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
# 初始化 Redis 连接
app.state.redis_client = Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
decode_responses=True,
password=settings.REDIS_PASSWORD
)
# 测试 Redis 连接
await app.state.redis_client.ping()
print("✅ Redis 连接成功")
except Exception as e:
print(f"❌ Redis 连接失败: {e}")
# 执行其他初始化操作
await init_data()
yield
# 关闭 Redis 连接
if hasattr(app.state, 'redis_client'):
await app.state.redis_client.close()
print("🛑 Redis 连接关闭")
# 关闭 Tortoise 连接
await Tortoise.close_connections()
def create_app() -> FastAPI:
app = FastAPI(
title=settings.APP_TITLE,
description=settings.APP_DESCRIPTION,
version=settings.VERSION,
openapi_url="/openapi.json",
middleware=make_middlewares(),
lifespan=lifespan,
)
register_exceptions(app)
register_routers(app, prefix="/api")
return app
app = create_app()

@ -0,0 +1,11 @@
from fastapi import APIRouter
from .chat import chat_router
from .v1 import v1_router
api_router = APIRouter()
api_router.include_router(v1_router, prefix="/v1")
api_router.include_router(chat_router, prefix="")
__all__ = ["api_router"]

@ -0,0 +1,8 @@
from fastapi import APIRouter
from app.core.dependency import DependPermission
from .ai import ai_api_router
chat_router = APIRouter()
chat_router.include_router(ai_api_router, prefix="/model")

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .chat_router import router
ai_api_router = APIRouter()
ai_api_router.include_router(router, tags=["ai对话接口模块"])
__all__ = ["ai_api_router"]

@ -0,0 +1,104 @@
from fastapi import APIRouter, HTTPException
from starlette.requests import Request
from fastapi.responses import StreamingResponse
from typing import AsyncGenerator
from .chat_service import classify, extract_spot, query_flow, gen_markdown_stream, ai_chat_stream
from app.models.ChatIn import ChatIn
import hmac
import hashlib
import time
import json
from pydantic import BaseModel
from app.settings.config import settings
router = APIRouter()
SECRET_KEY = settings.SIGN_KEY # 约定的密钥
TIMESTAMP_TOLERANCE = 60 # 时间戳容忍度,单位:秒
CONVERSATION_EXPIRE_TIME = 600 # 对话历史过期时间,单位:秒
@router.post("/chat", summary="ai对话")
async def h5_chat_stream(request: Request, inp: ChatIn):
if inp.sign is None:
raise HTTPException(status_code=401, detail="缺少签名参数")
if not verify_signature(inp.dict(), inp.sign):
raise HTTPException(status_code=401, detail="无效的签名")
if not verify_timestamp(inp.timestamp):
raise HTTPException(status_code=401, detail="时间戳无效")
if inp.language is None:
inp.language = "zh_cn"
# 获取用户 ID
user_id = inp.user_id
if not user_id:
raise HTTPException(status_code=400, detail="缺少用户 ID")
# 从 Redis 中获取用户的对话历史
redis_client = request.app.state.redis_client
conversation_history_key = f"conversation:{user_id}"
conversation_history_str = await redis_client.get(conversation_history_key)
conversation_history = json.loads(conversation_history_str) if conversation_history_str else []
# 分类阶段(保留同步调用)
cat = await classify(inp.message)
async def content_stream() -> AsyncGenerator[str, None]:
nonlocal conversation_history
try:
if cat == "游玩判断":
spot = await extract_spot(inp.message)
data = await query_flow(request, spot)
async for chunk in gen_markdown_stream(inp.message, data, inp.language, conversation_history):
yield chunk
else:
async for chunk in ai_chat_stream(inp, conversation_history):
yield chunk
# 将更新后的对话历史存回 Redis,并设置过期时间
await redis_client.setex(conversation_history_key, CONVERSATION_EXPIRE_TIME, json.dumps(conversation_history))
except Exception as e:
print(f"Error in content_stream: {e}")
raise
try:
return StreamingResponse(
content_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
except Exception as e:
print(f"Error in StreamingResponse: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")
class ClearConversationRequest(BaseModel):
user_id: int
@router.post("/clear_conversation", summary="清除对话记录")
async def clear_conversation(request: Request, body: ClearConversationRequest):
user_id = body.user_id
if not user_id:
raise HTTPException(status_code=400, detail="缺少用户 ID")
redis_client = request.app.state.redis_client
conversation_history_key = f"conversation:{user_id}"
await redis_client.delete(conversation_history_key)
return {"message": "对话历史已清除"}
def verify_signature(data: dict, sign: str) -> bool:
sorted_keys = sorted(data.keys())
sign_str = '&'.join([f'{key}={data[key]}' for key in sorted_keys if key != 'sign'])
sign_str += f'&secret={SECRET_KEY}'
calculated_sign = hmac.new(SECRET_KEY.encode(), sign_str.encode(), hashlib.sha256).hexdigest()
return calculated_sign == sign
def verify_timestamp(timestamp: int) -> bool:
current_timestamp = int(time.time() * 1000)
return abs(current_timestamp - timestamp) <= TIMESTAMP_TOLERANCE * 1000

@ -0,0 +1,161 @@
import os, openai, aiomysql
from openai import OpenAI
from dotenv import load_dotenv
import redis.asyncio as redis
from typing import AsyncGenerator
from app.models.ChatIn import ChatIn
from fastapi import Request
from app.settings.config import settings
load_dotenv()
client = OpenAI(api_key=settings.DEEPSEEK_API_KEY, base_url=settings.DEEPSEEK_API_URL)
CATEGORY_PROMPT = """你是一个分类助手,请根据用户的问题判断属于哪一类:
1. 如果用户是问某个保定市景区现在适不适合去或者某个保定市景区现在人多么此类涉及某个景区人数或者客流量的注意只有保定的景区请返回游玩判断
2. 其他均返回保定文旅
只能返回以上两个分类词之一不能多说话不回复其他多余内容"""
EXTRACT_PROMPT = """你是一个中文旅游助手,请从用户的问题中提取出景区名称,仅返回景区名,不要多余文字;如果没有提到具体景区,返回空字符串。"""
async def classify(msg: str) -> str:
print(f"Starting classification for message: {msg}")
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "system", "content": CATEGORY_PROMPT}, {"role": "user", "content": msg}]
)
result = response.choices[0].message.content
print(f"Classification result: {result}")
return result
except Exception as e:
print(f"Error in classification: {e}")
raise
async def ai_chat_stream(inp: ChatIn, conversation_history: list) -> AsyncGenerator[str, None]:
chat_prompt = (
f"你是一个河北省保定市的旅游助手,为游客提供保定市文旅相关问答服务,包括但不限于行程规划、"
f"景点推荐等,不提供其他任何非保定文旅相关信息。输出语言为:{inp.language}"
)
messages = [{"role": "system", "content": chat_prompt}] + conversation_history
messages.append({"role": "user", "content": inp.message})
print(f"Starting AI chat stream with input: {inp.message}")
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True
)
full_response = ""
for chunk in response:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
full_response += delta.content
# 直接输出纯文本内容,无SSE格式
yield delta.content
# 保留缓冲区刷新
import sys
sys.stdout.flush()
# 可选:添加结束标记(如需要)
# yield "[STREAM_END]"
except Exception as e:
print(f"Error in AI chat stream: {e}")
raise
# 将 AI 的回复添加到对话历史中
conversation_history.append({"role": "assistant", "content": full_response})
print("AI chat stream finished.")
async def extract_spot(msg: str) -> str:
print(f"Starting spot extraction for message: {msg}")
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "system", "content": EXTRACT_PROMPT}, {"role": "user", "content": msg}]
)
result = response.choices[0].message.content
print(f"Extracted spot: {result}")
return result
except Exception as e:
print(f"Error in spot extraction: {e}")
raise
async def query_flow(request: Request, spot: str) -> str:
if not spot:
print("No spot found, returning default message.")
return "**未找到景区信息,请检查名称是否正确。\n\n(内容由AI生成,仅供参考)"
cache_key = f"flow:{spot}"
# Step 1: Redis 缓存查询
print(f"Querying Redis cache for key: {cache_key}")
try:
redis_client = request.app.state.redis_client
cached = await redis_client.get(cache_key)
if cached:
print(f"Found cached data for key: {cache_key}")
return cached
except Exception as e:
print(f"[Redis] 查询缓存失败: {e}")
# Step 2: MySQL 查询(使用连接池)
print(f"Querying MySQL for spot: {spot}")
try:
pool = request.app.state.mysql_pool
async with pool.acquire() as conn:
async with conn.cursor() as cur:
query = "SELECT SUM(t1.init_num) AS init_num, SUM(t1.out_num) AS out_num FROM equipment_passenger_flow.flow_current_video t1 LEFT JOIN cyjcpt_bd.zhly_video_manage t2 ON t1.mac_address = t2.mac_address LEFT JOIN cyjcpt_bd.zhly_scenic_basic t3 ON t2.video_scenic_id = t3.id WHERE t3.`name` LIKE %s"
search_spot = f"%{spot}%"
await cur.execute(query, (search_spot,))
row = await cur.fetchone()
except Exception as e:
print(f"[MySQL] 查询失败: {e}")
return f"**未找到景区【{spot}】的信息,请检查名称是否正确。\n\n(内容由AI生成,仅供参考)"
print("数据库查询结果", row)
if not row:
print(f"No data found for spot: {spot}")
return f"**未找到景区【{spot}】的信息,请检查名称是否正确。\n\n(内容由AI生成,仅供参考)"
# 修改结果拼接部分,使用新的列名
result = f"**{spot} 客流**\n\n进入人数: {row[0]}\n离开人数: {row[1]}\n\n(内容由AI生成,仅供参考)"
# Step 3: 写入 Redis 缓存
print(f"Writing data to Redis cache for key: {cache_key}")
try:
await redis_client.setex(cache_key, 120, result)
except Exception as e:
print(f"[Redis] 写缓存失败: {e}")
return result
async def gen_markdown_stream(msg: str, data: str, language: str, conversation_history: list) -> AsyncGenerator[str, None]:
prompt = f"用户问题:{msg}\n查询到的内容为:{data}\n请结合用户问题和查询到的内容,自行优化回复内容,如果内容格式允许的话加上markdown语法。输出语言为:{language}"
messages = conversation_history + [{"role": "user", "content": prompt}]
print(f"Starting markdown stream with message: {msg} and data: {data}")
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True
)
full_response = ""
for chunk in response:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
full_response += delta.content
# 删除SSE格式封装,改为纯文本输出
yield delta.content
# 添加缓冲区刷新
import sys
sys.stdout.flush()
# 删除SSE结束标记
# yield "data: [DONE]\n\n"
except Exception as e:
print(f"Error in markdown stream: {e}")
raise
# 将 AI 的回复添加到对话历史中
conversation_history.append({"role": "assistant", "content": full_response})
print("Markdown stream finished.")

@ -0,0 +1,22 @@
from fastapi import APIRouter
from app.core.dependency import DependPermission
from .apis import apis_router
from .auditlog import auditlog_router
from .base import base_router
from .depts import depts_router
from .menus import menus_router
from .roles import roles_router
from .users import users_router
v1_router = APIRouter()
v1_router.include_router(base_router, prefix="/base")
v1_router.include_router(users_router, prefix="/user", dependencies=[DependPermission])
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermission])
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermission])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermission])
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermission])
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependPermission])

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .apis import router
apis_router = APIRouter()
apis_router.include_router(router, tags=["API模块"])
__all__ = ["apis_router"]

@ -0,0 +1,67 @@
from fastapi import APIRouter, Query
from tortoise.expressions import Q
from app.controllers.api import api_controller
from app.schemas import Success, SuccessExtra
from app.schemas.apis import *
router = APIRouter()
@router.get("/list", summary="查看API列表")
async def list_api(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
path: str = Query(None, description="API路径"),
summary: str = Query(None, description="API简介"),
tags: str = Query(None, description="API模块"),
):
q = Q()
if path:
q &= Q(path__contains=path)
if summary:
q &= Q(summary__contains=summary)
if tags:
q &= Q(tags__contains=tags)
total, api_objs = await api_controller.list(page=page, page_size=page_size, search=q, order=["tags", "id"])
data = [await obj.to_dict() for obj in api_objs]
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看Api")
async def get_api(
id: int = Query(..., description="Api"),
):
api_obj = await api_controller.get(id=id)
data = await api_obj.to_dict()
return Success(data=data)
@router.post("/create", summary="创建Api")
async def create_api(
api_in: ApiCreate,
):
await api_controller.create(obj_in=api_in)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新Api")
async def update_api(
api_in: ApiUpdate,
):
await api_controller.update(id=api_in.id, obj_in=api_in)
return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除Api")
async def delete_api(
api_id: int = Query(..., description="ApiID"),
):
await api_controller.remove(id=api_id)
return Success(msg="Deleted Success")
@router.post("/refresh", summary="刷新API列表")
async def refresh_api():
await api_controller.refresh_api()
return Success(msg="OK")

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .auditlog import router
auditlog_router = APIRouter()
auditlog_router.include_router(router, tags=["审计日志模块"])
__all__ = ["auditlog_router"]

@ -0,0 +1,48 @@
from datetime import datetime
from fastapi import APIRouter, Query
from tortoise.expressions import Q
from app.models.admin import AuditLog
from app.schemas import SuccessExtra
from app.schemas.apis import *
router = APIRouter()
@router.get("/list", summary="查看操作日志")
async def get_audit_log_list(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
username: str = Query("", description="操作人名称"),
module: str = Query("", description="功能模块"),
method: str = Query("", description="请求方法"),
summary: str = Query("", description="接口描述"),
path: str = Query("", description="请求路径"),
status: int = Query(None, description="状态码"),
start_time: datetime = Query("", description="开始时间"),
end_time: datetime = Query("", description="结束时间"),
):
q = Q()
if username:
q &= Q(username__icontains=username)
if module:
q &= Q(module__icontains=module)
if method:
q &= Q(method__icontains=method)
if summary:
q &= Q(summary__icontains=summary)
if path:
q &= Q(path__icontains=path)
if status:
q &= Q(status=status)
if start_time and end_time:
q &= Q(created_at__range=[start_time, end_time])
elif start_time:
q &= Q(created_at__gte=start_time)
elif end_time:
q &= Q(created_at__lte=end_time)
audit_log_objs = await AuditLog.filter(q).offset((page - 1) * page_size).limit(page_size).order_by("-created_at")
total = await AuditLog.filter(q).count()
data = [await audit_log.to_dict() for audit_log in audit_log_objs]
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .base import router
base_router = APIRouter()
base_router.include_router(router, tags=["基础模块"])
__all__ = ["base_router"]

@ -0,0 +1,103 @@
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter
from app.controllers.user import user_controller
from app.core.ctx import CTX_USER_ID
from app.core.dependency import DependAuth
from app.models.admin import Api, Menu, Role, User
from app.schemas.base import Fail, Success
from app.schemas.login import *
from app.schemas.users import UpdatePassword
from app.settings import settings
from app.utils.jwt_utils import create_access_token
from app.utils.password import get_password_hash, verify_password
router = APIRouter()
@router.post("/access_token", summary="获取token")
async def login_access_token(credentials: CredentialsSchema):
user: User = await user_controller.authenticate(credentials)
await user_controller.update_last_login(user.id)
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.now(timezone.utc) + access_token_expires
data = JWTOut(
access_token=create_access_token(
data=JWTPayload(
user_id=user.id,
username=user.username,
is_superuser=user.is_superuser,
exp=expire,
)
),
username=user.username,
)
return Success(data=data.model_dump())
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth])
async def get_userinfo():
user_id = CTX_USER_ID.get()
user_obj = await user_controller.get(id=user_id)
data = await user_obj.to_dict(exclude_fields=["password"])
data["avatar"] = "https://avatars.githubusercontent.com/u/54677442?v=4"
return Success(data=data)
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth])
async def get_user_menu():
user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first()
menus: list[Menu] = []
if user_obj.is_superuser:
menus = await Menu.all()
else:
role_objs: list[Role] = await user_obj.roles
for role_obj in role_objs:
menu = await role_obj.menus
menus.extend(menu)
menus = list(set(menus))
parent_menus: list[Menu] = []
for menu in menus:
if menu.parent_id == 0:
parent_menus.append(menu)
res = []
for parent_menu in parent_menus:
parent_menu_dict = await parent_menu.to_dict()
parent_menu_dict["children"] = []
for menu in menus:
if menu.parent_id == parent_menu.id:
parent_menu_dict["children"].append(await menu.to_dict())
res.append(parent_menu_dict)
return Success(data=res)
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth])
async def get_user_api():
user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first()
if user_obj.is_superuser:
api_objs: list[Api] = await Api.all()
apis = [api.method.lower() + api.path for api in api_objs]
return Success(data=apis)
role_objs: list[Role] = await user_obj.roles
apis = []
for role_obj in role_objs:
api_objs: list[Api] = await role_obj.apis
apis.extend([api.method.lower() + api.path for api in api_objs])
apis = list(set(apis))
return Success(data=apis)
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth])
async def update_user_password(req_in: UpdatePassword):
user_id = CTX_USER_ID.get()
user = await user_controller.get(user_id)
verified = verify_password(req_in.old_password, user.password)
if not verified:
return Fail(msg="旧密码验证错误!")
user.password = get_password_hash(req_in.new_password)
await user.save()
return Success(msg="修改成功")

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .depts import router
depts_router = APIRouter()
depts_router.include_router(router, tags=["部门模块"])
__all__ = ["depts_router"]

@ -0,0 +1,48 @@
from fastapi import APIRouter, Query
from app.controllers.dept import dept_controller
from app.schemas import Success
from app.schemas.depts import *
router = APIRouter()
@router.get("/list", summary="查看部门列表")
async def list_dept(
name: str = Query(None, description="部门名称"),
):
dept_tree = await dept_controller.get_dept_tree(name)
return Success(data=dept_tree)
@router.get("/get", summary="查看部门")
async def get_dept(
id: int = Query(..., description="部门ID"),
):
dept_obj = await dept_controller.get(id=id)
data = await dept_obj.to_dict()
return Success(data=data)
@router.post("/create", summary="创建部门")
async def create_dept(
dept_in: DeptCreate,
):
await dept_controller.create_dept(obj_in=dept_in)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新部门")
async def update_dept(
dept_in: DeptUpdate,
):
await dept_controller.update_dept(obj_in=dept_in)
return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除部门")
async def delete_dept(
dept_id: int = Query(..., description="部门ID"),
):
await dept_controller.delete_dept(dept_id=dept_id)
return Success(msg="Deleted Success")

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .menus import router
menus_router = APIRouter()
menus_router.include_router(router, tags=["菜单模块"])
__all__ = ["menus_router"]

@ -0,0 +1,63 @@
import logging
from fastapi import APIRouter, Query
from app.controllers.menu import menu_controller
from app.schemas.base import Fail, Success, SuccessExtra
from app.schemas.menus import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看菜单列表")
async def list_menu(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
):
async def get_menu_with_children(menu_id: int):
menu = await menu_controller.model.get(id=menu_id)
menu_dict = await menu.to_dict()
child_menus = await menu_controller.model.filter(parent_id=menu_id).order_by("order")
menu_dict["children"] = [await get_menu_with_children(child.id) for child in child_menus]
return menu_dict
parent_menus = await menu_controller.model.filter(parent_id=0).order_by("order")
res_menu = [await get_menu_with_children(menu.id) for menu in parent_menus]
return SuccessExtra(data=res_menu, total=len(res_menu), page=page, page_size=page_size)
@router.get("/get", summary="查看菜单")
async def get_menu(
menu_id: int = Query(..., description="菜单id"),
):
result = await menu_controller.get(id=menu_id)
return Success(data=result)
@router.post("/create", summary="创建菜单")
async def create_menu(
menu_in: MenuCreate,
):
await menu_controller.create(obj_in=menu_in)
return Success(msg="Created Success")
@router.post("/update", summary="更新菜单")
async def update_menu(
menu_in: MenuUpdate,
):
await menu_controller.update(id=menu_in.id, obj_in=menu_in)
return Success(msg="Updated Success")
@router.delete("/delete", summary="删除菜单")
async def delete_menu(
id: int = Query(..., description="菜单id"),
):
child_menu_count = await menu_controller.model.filter(parent_id=id).count()
if child_menu_count > 0:
return Fail(msg="Cannot delete a menu with child menus")
await menu_controller.remove(id=id)
return Success(msg="Deleted Success")

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .roles import router
roles_router = APIRouter()
roles_router.include_router(router, tags=["角色模块"])
__all__ = ["roles_router"]

@ -0,0 +1,73 @@
import logging
from fastapi import APIRouter, Query
from fastapi.exceptions import HTTPException
from tortoise.expressions import Q
from app.controllers import role_controller
from app.schemas.base import Success, SuccessExtra
from app.schemas.roles import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看角色列表")
async def list_role(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
role_name: str = Query("", description="角色名称,用于查询"),
):
q = Q()
if role_name:
q = Q(name__contains=role_name)
total, role_objs = await role_controller.list(page=page, page_size=page_size, search=q)
data = [await obj.to_dict() for obj in role_objs]
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看角色")
async def get_role(
role_id: int = Query(..., description="角色ID"),
):
role_obj = await role_controller.get(id=role_id)
return Success(data=await role_obj.to_dict())
@router.post("/create", summary="创建角色")
async def create_role(role_in: RoleCreate):
if await role_controller.is_exist(name=role_in.name):
raise HTTPException(
status_code=400,
detail="The role with this rolename already exists in the system.",
)
await role_controller.create(obj_in=role_in)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新角色")
async def update_role(role_in: RoleUpdate):
await role_controller.update(id=role_in.id, obj_in=role_in)
return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除角色")
async def delete_role(
role_id: int = Query(..., description="角色ID"),
):
await role_controller.remove(id=role_id)
return Success(msg="Deleted Success")
@router.get("/authorized", summary="查看角色权限")
async def get_role_authorized(id: int = Query(..., description="角色ID")):
role_obj = await role_controller.get(id=id)
data = await role_obj.to_dict(m2m=True)
return Success(data=data)
@router.post("/authorized", summary="更新角色权限")
async def update_role_authorized(role_in: RoleUpdateMenusApis):
role_obj = await role_controller.get(id=role_in.id)
await role_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)
return Success(msg="Updated Successfully")

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .users import router
users_router = APIRouter()
users_router.include_router(router, tags=["用户模块"])
__all__ = ["users_router"]

@ -0,0 +1,81 @@
import logging
from fastapi import APIRouter, Body, Query
from tortoise.expressions import Q
from app.controllers.dept import dept_controller
from app.controllers.user import user_controller
from app.schemas.base import Fail, Success, SuccessExtra
from app.schemas.users import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看用户列表")
async def list_user(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
username: str = Query("", description="用户名称,用于搜索"),
email: str = Query("", description="邮箱地址"),
dept_id: int = Query(None, description="部门ID"),
):
q = Q()
if username:
q &= Q(username__contains=username)
if email:
q &= Q(email__contains=email)
if dept_id is not None:
q &= Q(dept_id=dept_id)
total, user_objs = await user_controller.list(page=page, page_size=page_size, search=q)
data = [await obj.to_dict(m2m=True, exclude_fields=["password"]) for obj in user_objs]
for item in data:
dept_id = item.pop("dept_id", None)
item["dept"] = await (await dept_controller.get(id=dept_id)).to_dict() if dept_id else {}
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看用户")
async def get_user(
user_id: int = Query(..., description="用户ID"),
):
user_obj = await user_controller.get(id=user_id)
user_dict = await user_obj.to_dict(exclude_fields=["password"])
return Success(data=user_dict)
@router.post("/create", summary="创建用户")
async def create_user(
user_in: UserCreate,
):
user = await user_controller.get_by_email(user_in.email)
if user:
return Fail(code=400, msg="The user with this email already exists in the system.")
new_user = await user_controller.create_user(obj_in=user_in)
await user_controller.update_roles(new_user, user_in.role_ids)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新用户")
async def update_user(
user_in: UserUpdate,
):
user = await user_controller.update(id=user_in.id, obj_in=user_in)
await user_controller.update_roles(user, user_in.role_ids)
return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除用户")
async def delete_user(
user_id: int = Query(..., description="用户ID"),
):
await user_controller.remove(id=user_id)
return Success(msg="Deleted Successfully")
@router.post("/reset_password", summary="重置密码")
async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)):
await user_controller.reset_password(user_id)
return Success(msg="密码已重置为123456")

@ -0,0 +1,2 @@
from .role import role_controller as role_controller
from .user import user_controller as user_controller

@ -0,0 +1,45 @@
from fastapi.routing import APIRoute
from app.core.crud import CRUDBase
from app.log import logger
from app.models.admin import Api
from app.schemas.apis import ApiCreate, ApiUpdate
class ApiController(CRUDBase[Api, ApiCreate, ApiUpdate]):
def __init__(self):
super().__init__(model=Api)
async def refresh_api(self):
from app import app
# 删除废弃API数据
all_api_list = []
for route in app.routes:
# 只更新有鉴权的API
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
all_api_list.append((list(route.methods)[0], route.path_format))
delete_api = []
for api in await Api.all():
if (api.method, api.path) not in all_api_list:
delete_api.append((api.method, api.path))
for item in delete_api:
method, path = item
logger.debug(f"API Deleted {method} {path}")
await Api.filter(method=method, path=path).delete()
for route in app.routes:
if isinstance(route, APIRoute) and len(route.dependencies) > 0:
method = list(route.methods)[0]
path = route.path_format
summary = route.summary
tags = list(route.tags)[0]
api_obj = await Api.filter(method=method, path=path).first()
if api_obj:
await api_obj.update_from_dict(dict(method=method, path=path, summary=summary, tags=tags)).save()
else:
logger.debug(f"API Created {method} {path}")
await Api.create(**dict(method=method, path=path, summary=summary, tags=tags))
api_controller = ApiController()

@ -0,0 +1,86 @@
from tortoise.expressions import Q
from tortoise.transactions import atomic
from app.core.crud import CRUDBase
from app.models.admin import Dept, DeptClosure
from app.schemas.depts import DeptCreate, DeptUpdate
class DeptController(CRUDBase[Dept, DeptCreate, DeptUpdate]):
def __init__(self):
super().__init__(model=Dept)
async def get_dept_tree(self, name):
q = Q()
# 获取所有未被软删除的部门
q &= Q(is_deleted=False)
if name:
q &= Q(name__contains=name)
all_depts = await self.model.filter(q).order_by("order")
# 辅助函数,用于递归构建部门树
def build_tree(parent_id):
return [
{
"id": dept.id,
"name": dept.name,
"desc": dept.desc,
"order": dept.order,
"parent_id": dept.parent_id,
"children": build_tree(dept.id), # 递归构建子部门
}
for dept in all_depts
if dept.parent_id == parent_id
]
# 从顶级部门(parent_id=0)开始构建部门树
dept_tree = build_tree(0)
return dept_tree
async def get_dept_info(self):
pass
async def update_dept_closure(self, obj: Dept):
parent_depts = await DeptClosure.filter(descendant=obj.parent_id)
for i in parent_depts:
print(i.ancestor, i.descendant)
dept_closure_objs: list[DeptClosure] = []
# 插入父级关系
for item in parent_depts:
dept_closure_objs.append(DeptClosure(ancestor=item.ancestor, descendant=obj.id, level=item.level + 1))
# 插入自身x
dept_closure_objs.append(DeptClosure(ancestor=obj.id, descendant=obj.id, level=0))
# 创建关系
await DeptClosure.bulk_create(dept_closure_objs)
@atomic()
async def create_dept(self, obj_in: DeptCreate):
# 创建
if obj_in.parent_id != 0:
await self.get(id=obj_in.parent_id)
new_obj = await self.create(obj_in=obj_in)
await self.update_dept_closure(new_obj)
@atomic()
async def update_dept(self, obj_in: DeptUpdate):
dept_obj = await self.get(id=obj_in.id)
# 更新部门关系
if dept_obj.parent_id != obj_in.parent_id:
await DeptClosure.filter(ancestor=dept_obj.id).delete()
await DeptClosure.filter(descendant=dept_obj.id).delete()
await self.update_dept_closure(dept_obj)
# 更新部门信息
dept_obj.update_from_dict(obj_in.model_dump(exclude_unset=True))
await dept_obj.save()
@atomic()
async def delete_dept(self, dept_id: int):
# 删除部门
obj = await self.get(id=dept_id)
obj.is_deleted = True
await obj.save()
# 删除关系
await DeptClosure.filter(descendant=dept_id).delete()
dept_controller = DeptController()

@ -0,0 +1,16 @@
from typing import Optional
from app.core.crud import CRUDBase
from app.models.admin import Menu
from app.schemas.menus import MenuCreate, MenuUpdate
class MenuController(CRUDBase[Menu, MenuCreate, MenuUpdate]):
def __init__(self):
super().__init__(model=Menu)
async def get_by_menu_path(self, path: str) -> Optional["Menu"]:
return await self.model.filter(path=path).first()
menu_controller = MenuController()

@ -0,0 +1,27 @@
from typing import List
from app.core.crud import CRUDBase
from app.models.admin import Api, Menu, Role
from app.schemas.roles import RoleCreate, RoleUpdate
class RoleController(CRUDBase[Role, RoleCreate, RoleUpdate]):
def __init__(self):
super().__init__(model=Role)
async def is_exist(self, name: str) -> bool:
return await self.model.filter(name=name).exists()
async def update_roles(self, role: Role, menu_ids: List[int], api_infos: List[dict]) -> None:
await role.menus.clear()
for menu_id in menu_ids:
menu_obj = await Menu.filter(id=menu_id).first()
await role.menus.add(menu_obj)
await role.apis.clear()
for item in api_infos:
api_obj = await Api.filter(path=item.get("path"), method=item.get("method")).first()
await role.apis.add(api_obj)
role_controller = RoleController()

@ -0,0 +1,60 @@
from datetime import datetime
from typing import List, Optional
from fastapi.exceptions import HTTPException
from app.core.crud import CRUDBase
from app.models.admin import User
from app.schemas.login import CredentialsSchema
from app.schemas.users import UserCreate, UserUpdate
from app.utils.password import get_password_hash, verify_password
from .role import role_controller
class UserController(CRUDBase[User, UserCreate, UserUpdate]):
def __init__(self):
super().__init__(model=User)
async def get_by_email(self, email: str) -> Optional[User]:
return await self.model.filter(email=email).first()
async def get_by_username(self, username: str) -> Optional[User]:
return await self.model.filter(username=username).first()
async def create_user(self, obj_in: UserCreate) -> User:
obj_in.password = get_password_hash(password=obj_in.password)
obj = await self.create(obj_in)
return obj
async def update_last_login(self, id: int) -> None:
user = await self.model.get(id=id)
user.last_login = datetime.now()
await user.save()
async def authenticate(self, credentials: CredentialsSchema) -> Optional["User"]:
user = await self.model.filter(username=credentials.username).first()
if not user:
raise HTTPException(status_code=400, detail="无效的用户名")
verified = verify_password(credentials.password, user.password)
if not verified:
raise HTTPException(status_code=400, detail="密码错误!")
if not user.is_active:
raise HTTPException(status_code=400, detail="用户已被禁用")
return user
async def update_roles(self, user: User, role_ids: List[int]) -> None:
await user.roles.clear()
for role_id in role_ids:
role_obj = await role_controller.get(id=role_id)
await user.roles.add(role_obj)
async def reset_password(self, user_id: int):
user_obj = await self.get(id=user_id)
if user_obj.is_superuser:
raise HTTPException(status_code=403, detail="不允许重置超级管理员密码")
user_obj.password = get_password_hash(password="123456")
await user_obj.save()
user_controller = UserController()

@ -0,0 +1,31 @@
from starlette.background import BackgroundTasks
from .ctx import CTX_BG_TASKS
class BgTasks:
"""后台任务统一管理"""
@classmethod
async def init_bg_tasks_obj(cls):
"""实例化后台任务,并设置到上下文"""
bg_tasks = BackgroundTasks()
CTX_BG_TASKS.set(bg_tasks)
@classmethod
async def get_bg_tasks_obj(cls):
"""从上下文中获取后台任务实例"""
return CTX_BG_TASKS.get()
@classmethod
async def add_task(cls, func, *args, **kwargs):
"""添加后台任务"""
bg_tasks = await cls.get_bg_tasks_obj()
bg_tasks.add_task(func, *args, **kwargs)
@classmethod
async def execute_tasks(cls):
"""执行后台任务,一般是请求结果返回之后执行"""
bg_tasks = await cls.get_bg_tasks_obj()
if bg_tasks.tasks:
await bg_tasks()

@ -0,0 +1,45 @@
from typing import Any, Dict, Generic, List, NewType, Tuple, Type, TypeVar, Union
from pydantic import BaseModel
from tortoise.expressions import Q
from tortoise.models import Model
Total = NewType("Total", int)
ModelType = TypeVar("ModelType", bound=Model)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
self.model = model
async def get(self, id: int) -> ModelType:
return await self.model.get(id=id)
async def list(self, page: int, page_size: int, search: Q = Q(), order: list = []) -> Tuple[Total, List[ModelType]]:
query = self.model.filter(search)
return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order)
async def create(self, obj_in: CreateSchemaType) -> ModelType:
if isinstance(obj_in, Dict):
obj_dict = obj_in
else:
obj_dict = obj_in.model_dump()
obj = self.model(**obj_dict)
await obj.save()
return obj
async def update(self, id: int, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> ModelType:
if isinstance(obj_in, Dict):
obj_dict = obj_in
else:
obj_dict = obj_in.model_dump(exclude_unset=True, exclude={"id"})
obj = await self.get(id=id)
obj = obj.update_from_dict(obj_dict)
await obj.save()
return obj
async def remove(self, id: int) -> None:
obj = await self.get(id=id)
await obj.delete()

@ -0,0 +1,6 @@
import contextvars
from starlette.background import BackgroundTasks
CTX_USER_ID: contextvars.ContextVar[int] = contextvars.ContextVar("user_id", default=0)
CTX_BG_TASKS: contextvars.ContextVar[BackgroundTasks] = contextvars.ContextVar("bg_task", default=None)

@ -0,0 +1,53 @@
from typing import Optional
import jwt
from fastapi import Depends, Header, HTTPException, Request
from app.core.ctx import CTX_USER_ID
from app.models import Role, User
from app.settings import settings
class AuthControl:
@classmethod
async def is_authed(cls, token: str = Header(..., description="token验证")) -> Optional["User"]:
try:
if token == "dev":
user = await User.filter().first()
user_id = user.id
else:
decode_data = jwt.decode(token, settings.SECRET_KEY, algorithms=settings.JWT_ALGORITHM)
user_id = decode_data.get("user_id")
user = await User.filter(id=user_id).first()
if not user:
raise HTTPException(status_code=401, detail="Authentication failed")
CTX_USER_ID.set(int(user_id))
return user
except jwt.DecodeError:
raise HTTPException(status_code=401, detail="无效的Token")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="登录已过期")
except Exception as e:
raise HTTPException(status_code=500, detail=f"{repr(e)}")
class PermissionControl:
@classmethod
async def has_permission(cls, request: Request, current_user: User = Depends(AuthControl.is_authed)) -> None:
if current_user.is_superuser:
return
method = request.method
path = request.url.path
roles: list[Role] = await current_user.roles
if not roles:
raise HTTPException(status_code=403, detail="The user is not bound to a role")
apis = [await role.apis for role in roles]
permission_apis = list(set((api.method, api.path) for api in sum(apis, [])))
# path = "/api/v1/auth/userinfo"
# method = "GET"
if (method, path) not in permission_apis:
raise HTTPException(status_code=403, detail=f"Permission denied method:{method} path:{path}")
DependAuth = Depends(AuthControl.is_authed)
DependPermission = Depends(PermissionControl.has_permission)

@ -0,0 +1,43 @@
from fastapi.exceptions import (
HTTPException,
RequestValidationError,
ResponseValidationError,
)
from fastapi.requests import Request
from fastapi.responses import JSONResponse
from tortoise.exceptions import DoesNotExist, IntegrityError
class SettingNotFound(Exception):
pass
async def DoesNotExistHandle(req: Request, exc: DoesNotExist) -> JSONResponse:
content = dict(
code=404,
msg=f"Object has not found, exc: {exc}, query_params: {req.query_params}",
)
return JSONResponse(content=content, status_code=404)
async def IntegrityHandle(_: Request, exc: IntegrityError) -> JSONResponse:
content = dict(
code=500,
msg=f"IntegrityError,{exc}",
)
return JSONResponse(content=content, status_code=500)
async def HttpExcHandle(_: Request, exc: HTTPException) -> JSONResponse:
content = dict(code=exc.status_code, msg=exc.detail, data=None)
return JSONResponse(content=content, status_code=exc.status_code)
async def RequestValidationHandle(_: Request, exc: RequestValidationError) -> JSONResponse:
content = dict(code=422, msg=f"RequestValidationError, {exc}")
return JSONResponse(content=content, status_code=422)
async def ResponseValidationHandle(_: Request, exc: ResponseValidationError) -> JSONResponse:
content = dict(code=500, msg=f"ResponseValidationError, {exc}")
return JSONResponse(content=content, status_code=500)

@ -0,0 +1,234 @@
import shutil
from aerich import Command
from fastapi import FastAPI
from fastapi.middleware import Middleware
from fastapi.middleware.cors import CORSMiddleware
from tortoise.expressions import Q
from app.api import api_router
from app.controllers.api import api_controller
from app.controllers.user import UserCreate, user_controller
from app.core.exceptions import (
DoesNotExist,
DoesNotExistHandle,
HTTPException,
HttpExcHandle,
IntegrityError,
IntegrityHandle,
RequestValidationError,
RequestValidationHandle,
ResponseValidationError,
ResponseValidationHandle,
)
from app.log import logger
from app.models.admin import Api, Menu, Role
from app.schemas.menus import MenuType
from app.settings.config import settings
from .middlewares import BackGroundTaskMiddleware, HttpAuditLogMiddleware
def make_middlewares():
middleware = [
Middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
allow_methods=settings.CORS_ALLOW_METHODS,
allow_headers=settings.CORS_ALLOW_HEADERS,
),
Middleware(BackGroundTaskMiddleware),
Middleware(
HttpAuditLogMiddleware,
methods=["GET", "POST", "PUT", "DELETE"],
exclude_paths=[
"/api/v1/base/access_token",
"/docs",
"/openapi.json",
"/api/model/"
],
),
]
return middleware
def register_exceptions(app: FastAPI):
app.add_exception_handler(DoesNotExist, DoesNotExistHandle)
app.add_exception_handler(HTTPException, HttpExcHandle)
app.add_exception_handler(IntegrityError, IntegrityHandle)
app.add_exception_handler(RequestValidationError, RequestValidationHandle)
app.add_exception_handler(ResponseValidationError, ResponseValidationHandle)
def register_routers(app: FastAPI, prefix: str = "/api"):
app.include_router(api_router, prefix=prefix)
async def init_superuser():
user = await user_controller.model.exists()
if not user:
await user_controller.create_user(
UserCreate(
username="admin",
email="admin@admin.com",
password="123456",
is_active=True,
is_superuser=True,
)
)
async def init_menus():
menus = await Menu.exists()
if not menus:
parent_menu = await Menu.create(
menu_type=MenuType.CATALOG,
name="系统管理",
path="/system",
order=1,
parent_id=0,
icon="carbon:gui-management",
is_hidden=False,
component="Layout",
keepalive=False,
redirect="/system/user",
)
children_menu = [
Menu(
menu_type=MenuType.MENU,
name="用户管理",
path="user",
order=1,
parent_id=parent_menu.id,
icon="material-symbols:person-outline-rounded",
is_hidden=False,
component="/system/user",
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="角色管理",
path="role",
order=2,
parent_id=parent_menu.id,
icon="carbon:user-role",
is_hidden=False,
component="/system/role",
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="菜单管理",
path="menu",
order=3,
parent_id=parent_menu.id,
icon="material-symbols:list-alt-outline",
is_hidden=False,
component="/system/menu",
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="API管理",
path="api",
order=4,
parent_id=parent_menu.id,
icon="ant-design:api-outlined",
is_hidden=False,
component="/system/api",
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="部门管理",
path="dept",
order=5,
parent_id=parent_menu.id,
icon="mingcute:department-line",
is_hidden=False,
component="/system/dept",
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="审计日志",
path="auditlog",
order=6,
parent_id=parent_menu.id,
icon="ph:clipboard-text-bold",
is_hidden=False,
component="/system/auditlog",
keepalive=False,
),
]
await Menu.bulk_create(children_menu)
await Menu.create(
menu_type=MenuType.MENU,
name="一级菜单",
path="/top-menu",
order=2,
parent_id=0,
icon="material-symbols:featured-play-list-outline",
is_hidden=False,
component="/top-menu",
keepalive=False,
redirect="",
)
async def init_apis():
apis = await api_controller.model.exists()
if not apis:
await api_controller.refresh_api()
async def init_db():
command = Command(tortoise_config=settings.TORTOISE_ORM)
try:
await command.init_db(safe=True)
except FileExistsError:
pass
await command.init()
try:
await command.migrate()
except AttributeError:
logger.warning("unable to retrieve model history from database, model history will be created from scratch")
shutil.rmtree("migrations")
await command.init_db(safe=True)
await command.upgrade(run_in_transaction=True)
async def init_roles():
roles = await Role.exists()
if not roles:
admin_role = await Role.create(
name="管理员",
desc="管理员角色",
)
user_role = await Role.create(
name="普通用户",
desc="普通用户角色",
)
# 分配所有API给管理员角色
all_apis = await Api.all()
await admin_role.apis.add(*all_apis)
# 分配所有菜单给管理员和普通用户
all_menus = await Menu.all()
await admin_role.menus.add(*all_menus)
await user_role.menus.add(*all_menus)
# 为普通用户分配基本API
basic_apis = await Api.filter(Q(method__in=["GET"]) | Q(tags="基础模块"))
await user_role.apis.add(*basic_apis)
async def init_data():
await init_db()
await init_superuser()
await init_menus()
await init_apis()
await init_roles()

@ -0,0 +1,182 @@
import json
import re
from datetime import datetime
from typing import Any, AsyncGenerator
from fastapi import FastAPI
from fastapi.responses import Response
from fastapi.routing import APIRoute
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.types import ASGIApp, Receive, Scope, Send
from app.core.dependency import AuthControl
from app.models.admin import AuditLog, User
from .bgtask import BgTasks
class SimpleBaseMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request = Request(scope, receive=receive)
response = await self.before_request(request) or self.app
await response(request.scope, request.receive, send)
await self.after_request(request)
async def before_request(self, request: Request):
return self.app
async def after_request(self, request: Request):
return None
class BackGroundTaskMiddleware(SimpleBaseMiddleware):
async def before_request(self, request):
await BgTasks.init_bg_tasks_obj()
async def after_request(self, request):
await BgTasks.execute_tasks()
class HttpAuditLogMiddleware(BaseHTTPMiddleware):
def __init__(self, app, methods: list[str], exclude_paths: list[str]):
super().__init__(app)
self.methods = methods
self.exclude_paths = exclude_paths
self.audit_log_paths = ["/api/v1/auditlog/list"]
self.max_body_size = 1024 * 1024 # 1MB 响应体大小限制
async def get_request_args(self, request: Request) -> dict:
args = {}
# 获取查询参数
for key, value in request.query_params.items():
args[key] = value
# 获取请求体
if request.method in ["POST", "PUT", "PATCH"]:
try:
body = await request.json()
args.update(body)
except json.JSONDecodeError:
try:
body = await request.form()
# args.update(body)
for k, v in body.items():
if hasattr(v, "filename"): # 文件上传行为
args[k] = v.filename
elif isinstance(v, list) and v and hasattr(v[0], "filename"):
args[k] = [file.filename for file in v]
else:
args[k] = v
except Exception:
pass
return args
async def get_response_body(self, request: Request, response: Response) -> Any:
# 检查Content-Length
content_length = response.headers.get("content-length")
if content_length and int(content_length) > self.max_body_size:
return {"code": 0, "msg": "Response too large to log", "data": None}
if hasattr(response, "body"):
body = response.body
else:
body_chunks = []
async for chunk in response.body_iterator:
if not isinstance(chunk, bytes):
chunk = chunk.encode(response.charset)
body_chunks.append(chunk)
response.body_iterator = self._async_iter(body_chunks)
body = b"".join(body_chunks)
if any(request.url.path.startswith(path) for path in self.audit_log_paths):
try:
data = self.lenient_json(body)
# 只保留基本信息,去除详细的响应内容
if isinstance(data, dict):
data.pop("response_body", None)
if "data" in data and isinstance(data["data"], list):
for item in data["data"]:
item.pop("response_body", None)
return data
except Exception:
return None
return self.lenient_json(body)
def lenient_json(self, v: Any) -> Any:
if isinstance(v, (str, bytes)):
try:
return json.loads(v)
except (ValueError, TypeError):
pass
return v
async def _async_iter(self, items: list[bytes]) -> AsyncGenerator[bytes, None]:
for item in items:
yield item
async def get_request_log(self, request: Request, response: Response) -> dict:
"""
根据request和response对象获取对应的日志记录数据
"""
data: dict = {"path": request.url.path, "status": response.status_code, "method": request.method}
# 路由信息
app: FastAPI = request.app
for route in app.routes:
if (
isinstance(route, APIRoute)
and route.path_regex.match(request.url.path)
and request.method in route.methods
):
data["module"] = ",".join(route.tags)
data["summary"] = route.summary
# 获取用户信息
try:
token = request.headers.get("token")
user_obj = None
if token:
user_obj: User = await AuthControl.is_authed(token)
data["user_id"] = user_obj.id if user_obj else 0
data["username"] = user_obj.username if user_obj else ""
except Exception:
data["user_id"] = 0
data["username"] = ""
return data
async def before_request(self, request: Request):
request_args = await self.get_request_args(request)
request.state.request_args = request_args
async def after_request(self, request: Request, response: Response, process_time: int):
if request.method in self.methods:
for path in self.exclude_paths:
if re.search(path, request.url.path, re.I) is not None:
return
data: dict = await self.get_request_log(request=request, response=response)
data["response_time"] = process_time
data["request_args"] = request.state.request_args
data["response_body"] = await self.get_response_body(request, response)
await AuditLog.create(**data)
return response
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
start_time: datetime = datetime.now()
await self.before_request(request)
response = await call_next(request)
end_time: datetime = datetime.now()
process_time = int((end_time.timestamp() - start_time.timestamp()) * 1000)
await self.after_request(request, response, process_time)
return response

@ -0,0 +1 @@
from .log import logger as logger

@ -0,0 +1,25 @@
import sys
from loguru import logger as loguru_logger
from app.settings import settings
class Loggin:
def __init__(self) -> None:
debug = settings.DEBUG
if debug:
self.level = "DEBUG"
else:
self.level = "INFO"
def setup_logger(self):
loguru_logger.remove()
loguru_logger.add(sink=sys.stdout, level=self.level)
# logger.add("my_project.log", level=level, rotation="100 MB") # Output log messages to a file
return loguru_logger
loggin = Loggin()
logger = loggin.setup_logger()

@ -0,0 +1,8 @@
from pydantic import BaseModel
class ChatIn(BaseModel):
language: str
message: str
sign: str
timestamp: int
user_id: int

@ -0,0 +1,2 @@
# 新增model需要在这里导入
from .admin import *

@ -0,0 +1,89 @@
from tortoise import fields
from app.schemas.menus import MenuType
from .base import BaseModel, TimestampMixin
from .enums import MethodType
class User(BaseModel, TimestampMixin):
username = fields.CharField(max_length=20, unique=True, description="用户名称", index=True)
alias = fields.CharField(max_length=30, null=True, description="姓名", index=True)
email = fields.CharField(max_length=255, unique=True, description="邮箱", index=True)
phone = fields.CharField(max_length=20, null=True, description="电话", index=True)
password = fields.CharField(max_length=128, null=True, description="密码")
is_active = fields.BooleanField(default=True, description="是否激活", index=True)
is_superuser = fields.BooleanField(default=False, description="是否为超级管理员", index=True)
last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True)
roles = fields.ManyToManyField("models.Role", related_name="user_roles")
dept_id = fields.IntField(null=True, description="部门ID", index=True)
class Meta:
table = "user"
class Role(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, unique=True, description="角色名称", index=True)
desc = fields.CharField(max_length=500, null=True, description="角色描述")
menus = fields.ManyToManyField("models.Menu", related_name="role_menus")
apis = fields.ManyToManyField("models.Api", related_name="role_apis")
class Meta:
table = "role"
class Api(BaseModel, TimestampMixin):
path = fields.CharField(max_length=100, description="API路径", index=True)
method = fields.CharEnumField(MethodType, description="请求方法", index=True)
summary = fields.CharField(max_length=500, description="请求简介", index=True)
tags = fields.CharField(max_length=100, description="API标签", index=True)
class Meta:
table = "api"
class Menu(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, description="菜单名称", index=True)
remark = fields.JSONField(null=True, description="保留字段")
menu_type = fields.CharEnumField(MenuType, null=True, description="菜单类型")
icon = fields.CharField(max_length=100, null=True, description="菜单图标")
path = fields.CharField(max_length=100, description="菜单路径", index=True)
order = fields.IntField(default=0, description="排序", index=True)
parent_id = fields.IntField(default=0, description="父菜单ID", index=True)
is_hidden = fields.BooleanField(default=False, description="是否隐藏")
component = fields.CharField(max_length=100, description="组件")
keepalive = fields.BooleanField(default=True, description="存活")
redirect = fields.CharField(max_length=100, null=True, description="重定向")
class Meta:
table = "menu"
class Dept(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, unique=True, description="部门名称", index=True)
desc = fields.CharField(max_length=500, null=True, description="备注")
is_deleted = fields.BooleanField(default=False, description="软删除标记", index=True)
order = fields.IntField(default=0, description="排序", index=True)
parent_id = fields.IntField(default=0, max_length=10, description="父部门ID", index=True)
class Meta:
table = "dept"
class DeptClosure(BaseModel, TimestampMixin):
ancestor = fields.IntField(description="父代", index=True)
descendant = fields.IntField(description="子代", index=True)
level = fields.IntField(default=0, description="深度", index=True)
class AuditLog(BaseModel, TimestampMixin):
user_id = fields.IntField(description="用户ID", index=True)
username = fields.CharField(max_length=64, default="", description="用户名称", index=True)
module = fields.CharField(max_length=64, default="", description="功能模块", index=True)
summary = fields.CharField(max_length=128, default="", description="请求描述", index=True)
method = fields.CharField(max_length=10, default="", description="请求方法", index=True)
path = fields.CharField(max_length=255, default="", description="请求路径", index=True)
status = fields.IntField(default=-1, description="状态码", index=True)
response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True)
request_args = fields.JSONField(null=True, description="请求参数")
response_body = fields.JSONField(null=True, description="返回数据")

@ -0,0 +1,62 @@
import asyncio
from datetime import datetime
from tortoise import fields, models
from app.settings import settings
class BaseModel(models.Model):
id = fields.BigIntField(pk=True, index=True)
async def to_dict(self, m2m: bool = False, exclude_fields: list[str] | None = None):
if exclude_fields is None:
exclude_fields = []
d = {}
for field in self._meta.db_fields:
if field not in exclude_fields:
value = getattr(self, field)
if isinstance(value, datetime):
value = value.strftime(settings.DATETIME_FORMAT)
d[field] = value
if m2m:
tasks = [
self.__fetch_m2m_field(field, exclude_fields)
for field in self._meta.m2m_fields
if field not in exclude_fields
]
results = await asyncio.gather(*tasks)
for field, values in results:
d[field] = values
return d
async def __fetch_m2m_field(self, field, exclude_fields):
values = await getattr(self, field).all().values()
formatted_values = []
for value in values:
formatted_value = {}
for k, v in value.items():
if k not in exclude_fields:
if isinstance(v, datetime):
formatted_value[k] = v.strftime(settings.DATETIME_FORMAT)
else:
formatted_value[k] = v
formatted_values.append(formatted_value)
return field, formatted_values
class Meta:
abstract = True
class UUIDModel:
uuid = fields.UUIDField(unique=True, pk=False, index=True)
class TimestampMixin:
created_at = fields.DatetimeField(auto_now_add=True, index=True)
updated_at = fields.DatetimeField(auto_now=True, index=True)

@ -0,0 +1,19 @@
from enum import Enum, StrEnum
class EnumBase(Enum):
@classmethod
def get_member_values(cls):
return [item.value for item in cls._member_map_.values()]
@classmethod
def get_member_names(cls):
return [name for name in cls._member_names_]
class MethodType(StrEnum):
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
PATCH = "PATCH"

@ -0,0 +1 @@
from .base import *

@ -0,0 +1,17 @@
from pydantic import BaseModel, Field
from app.models.enums import MethodType
class BaseApi(BaseModel):
path: str = Field(..., description="API路径", example="/api/v1/user/list")
summary: str = Field("", description="API简介", example="查看用户列表")
method: MethodType = Field(..., description="API方法", example="GET")
tags: str = Field(..., description="API标签", example="User")
class ApiCreate(BaseApi): ...
class ApiUpdate(BaseApi):
id: int

@ -0,0 +1,52 @@
from typing import Any, Optional
from fastapi.responses import JSONResponse
class Success(JSONResponse):
def __init__(
self,
code: int = 200,
msg: Optional[str] = "OK",
data: Optional[Any] = None,
**kwargs,
):
content = {"code": code, "msg": msg, "data": data}
content.update(kwargs)
super().__init__(content=content, status_code=code)
class Fail(JSONResponse):
def __init__(
self,
code: int = 400,
msg: Optional[str] = None,
data: Optional[Any] = None,
**kwargs,
):
content = {"code": code, "msg": msg, "data": data}
content.update(kwargs)
super().__init__(content=content, status_code=code)
class SuccessExtra(JSONResponse):
def __init__(
self,
code: int = 200,
msg: Optional[str] = None,
data: Optional[Any] = None,
total: int = 0,
page: int = 1,
page_size: int = 20,
**kwargs,
):
content = {
"code": code,
"msg": msg,
"data": data,
"total": total,
"page": page,
"page_size": page_size,
}
content.update(kwargs)
super().__init__(content=content, status_code=code)

@ -0,0 +1,18 @@
from pydantic import BaseModel, Field
class BaseDept(BaseModel):
name: str = Field(..., description="部门名称", example="研发中心")
desc: str = Field("", description="备注", example="研发中心")
order: int = Field(0, description="排序")
parent_id: int = Field(0, description="父部门ID")
class DeptCreate(BaseDept): ...
class DeptUpdate(BaseDept):
id: int
def update_dict(self):
return self.model_dump(exclude_unset=True, exclude={"id"})

@ -0,0 +1,20 @@
from datetime import datetime
from pydantic import BaseModel, Field
class CredentialsSchema(BaseModel):
username: str = Field(..., description="用户名称", example="admin")
password: str = Field(..., description="密码", example="123456")
class JWTOut(BaseModel):
access_token: str
username: str
class JWTPayload(BaseModel):
user_id: int
username: str
is_superuser: bool
exp: datetime

@ -0,0 +1,52 @@
from enum import StrEnum
from typing import Optional
from pydantic import BaseModel, Field
class MenuType(StrEnum):
CATALOG = "catalog" # 目录
MENU = "menu" # 菜单
class BaseMenu(BaseModel):
id: int
name: str
path: str
remark: Optional[dict]
menu_type: Optional[MenuType]
icon: Optional[str]
order: int
parent_id: int
is_hidden: bool
component: str
keepalive: bool
redirect: Optional[str]
children: Optional[list["BaseMenu"]]
class MenuCreate(BaseModel):
menu_type: MenuType = Field(default=MenuType.CATALOG.value)
name: str = Field(example="用户管理")
icon: Optional[str] = "ph:user-list-bold"
path: str = Field(example="/system/user")
order: Optional[int] = Field(example=1)
parent_id: Optional[int] = Field(example=0, default=0)
is_hidden: Optional[bool] = False
component: str = Field(default="Layout", example="/system/user")
keepalive: Optional[bool] = True
redirect: Optional[str] = ""
class MenuUpdate(BaseModel):
id: int
menu_type: Optional[MenuType] = Field(example=MenuType.CATALOG.value)
name: Optional[str] = Field(example="用户管理")
icon: Optional[str] = "ph:user-list-bold"
path: Optional[str] = Field(example="/system/user")
order: Optional[int] = Field(example=1)
parent_id: Optional[int] = Field(example=0)
is_hidden: Optional[bool] = False
component: str = Field(example="/system/user")
keepalive: Optional[bool] = False
redirect: Optional[str] = ""

@ -0,0 +1,32 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class BaseRole(BaseModel):
id: int
name: str
desc: str = ""
users: Optional[list] = []
menus: Optional[list] = []
apis: Optional[list] = []
created_at: Optional[datetime]
updated_at: Optional[datetime]
class RoleCreate(BaseModel):
name: str = Field(example="管理员")
desc: str = Field("", example="管理员角色")
class RoleUpdate(BaseModel):
id: int = Field(example=1)
name: str = Field(example="管理员")
desc: str = Field("", example="管理员角色")
class RoleUpdateMenusApis(BaseModel):
id: int
menu_ids: list[int] = []
api_infos: list[dict] = []

@ -0,0 +1,44 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, EmailStr, Field
class BaseUser(BaseModel):
id: int
email: Optional[EmailStr] = None
username: Optional[str] = None
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
created_at: Optional[datetime]
updated_at: Optional[datetime]
last_login: Optional[datetime]
roles: Optional[list] = []
class UserCreate(BaseModel):
email: EmailStr = Field(example="admin@qq.com")
username: str = Field(example="admin")
password: str = Field(example="123456")
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
role_ids: Optional[List[int]] = []
dept_id: Optional[int] = Field(0, description="部门ID")
def create_dict(self):
return self.model_dump(exclude_unset=True, exclude={"role_ids"})
class UserUpdate(BaseModel):
id: int
email: EmailStr
username: str
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
role_ids: Optional[List[int]] = []
dept_id: Optional[int] = 0
class UpdatePassword(BaseModel):
old_password: str = Field(description="旧密码")
new_password: str = Field(description="新密码")

@ -0,0 +1,3 @@
from .config import settings as settings
TORTOISE_ORM = settings.TORTOISE_ORM

@ -0,0 +1,104 @@
import os
import typing
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
VERSION: str = "0.1.0"
APP_TITLE: str = "Vue FastAPI Admin"
PROJECT_NAME: str = "Vue FastAPI Admin"
APP_DESCRIPTION: str = "Description"
CORS_ORIGINS: typing.List = ["*"]
CORS_ALLOW_CREDENTIALS: bool = True
CORS_ALLOW_METHODS: typing.List = ["*"]
CORS_ALLOW_HEADERS: typing.List = ["*"]
DEBUG: bool = True
PROJECT_ROOT: str = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
BASE_DIR: str = os.path.abspath(os.path.join(PROJECT_ROOT, os.pardir))
LOGS_ROOT: str = os.path.join(BASE_DIR, "app/logs")
SECRET_KEY: str = "3488a63e1765035d386f05409663f55c83bfae3b3c61a932744b20ad14244dcf" # openssl rand -hex 32
JWT_ALGORITHM: str = "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 day
# DeepSeek
DEEPSEEK_API_KEY: str = "sk-6147b0356b474070a8df1f2703704ea1"
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1"
# Redis
REDIS_HOST: str = "39.105.17.128"
REDIS_PORT: int = 33062
REDIS_DB: int = 1
REDIS_PASSWORD: str = "cjy@123abc"
# 验签秘钥
SIGN_KEY: str = "qIaGO0fZJNW2f0DM6upT"
TORTOISE_ORM: dict = {
"connections": {
# SQLite configuration
# "sqlite": {
# "engine": "tortoise.backends.sqlite",
# "credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
# },
# MySQL/MariaDB configuration
# Install with: tortoise-orm[asyncmy]
"mysql": {
"engine": "tortoise.backends.mysql",
"credentials": {
"host": "localhost", # Database host address
"port": 3306, # Database port
"user": "root", # Database username
"password": "root", # Database password
"database": "bd_ai_fastapi", # Database name
},
},
# PostgreSQL configuration
# Install with: tortoise-orm[asyncpg]
# "postgres": {
# "engine": "tortoise.backends.asyncpg",
# "credentials": {
# "host": "localhost", # Database host address
# "port": 5432, # Database port
# "user": "yourusername", # Database username
# "password": "yourpassword", # Database password
# "database": "yourdatabase", # Database name
# },
# },
# MSSQL/Oracle configuration
# Install with: tortoise-orm[asyncodbc]
# "oracle": {
# "engine": "tortoise.backends.asyncodbc",
# "credentials": {
# "host": "localhost", # Database host address
# "port": 1433, # Database port
# "user": "yourusername", # Database username
# "password": "yourpassword", # Database password
# "database": "yourdatabase", # Database name
# },
# },
# SQLServer configuration
# Install with: tortoise-orm[asyncodbc]
# "sqlserver": {
# "engine": "tortoise.backends.asyncodbc",
# "credentials": {
# "host": "localhost", # Database host address
# "port": 1433, # Database port
# "user": "yourusername", # Database username
# "password": "yourpassword", # Database password
# "database": "yourdatabase", # Database name
# },
# },
},
"apps": {
"models": {
"models": ["app.models", "aerich.models"],
"default_connection": "mysql",
},
},
"use_tz": False, # Whether to use timezone-aware datetimes
"timezone": "Asia/Shanghai", # Timezone setting
}
DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
settings = Settings()

@ -0,0 +1,10 @@
import jwt
from app.schemas.login import JWTPayload
from app.settings.config import settings
def create_access_token(*, data: JWTPayload):
payload = data.model_dump().copy()
encoded_jwt = jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt

@ -0,0 +1,16 @@
from passlib import pwd
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def generate_password() -> str:
return pwd.genword()

@ -0,0 +1,5 @@
#!/bin/sh
set -e
nginx
python run.py

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

@ -0,0 +1,13 @@
server {
listen 80;
server_name localhost;
location / {
root /opt/vue-fastapi-admin/web/dist;
index index.html index.htm;
try_files $uri /index.html;
}
location ^~ /api/ {
proxy_pass http://127.0.0.1:9999;
}
}

@ -0,0 +1,95 @@
[project]
name = "vue-fastapi-admin"
version = "0.1.0"
description = "Vue Fastapi admin"
authors = [
{name = "mizhexiaoxiao", email = "mizhexiaoxiao@gmail.com"},
]
requires-python = ">=3.11"
dependencies = [
"fastapi==0.111.0",
"tortoise-orm==0.23.0",
"pydantic==2.10.5",
"email-validator==2.2.0",
"passlib==1.7.4",
"pyjwt==2.10.1",
"black==24.10.0",
"isort==5.13.2",
"ruff==0.9.1",
"loguru==0.7.3",
"pydantic-settings==2.7.1",
"argon2-cffi==23.1.0",
"pydantic-core==2.27.2",
"annotated-types==0.7.0",
"setuptools==75.8.0",
"uvicorn==0.34.0",
"h11==0.14.0",
"aerich==0.8.1",
"aiosqlite==0.20.0",
"anyio==4.8.0",
"argon2-cffi-bindings==21.2.0",
"asyncclick==8.1.8",
"certifi==2024.12.14",
"cffi==1.17.1",
"click==8.1.8",
"dictdiffer==0.9.0",
"dnspython==2.7.0",
"fastapi-cli==0.0.7",
"httpcore==1.0.7",
"httptools==0.6.4",
"httpx==0.28.1",
"idna==3.10",
"iso8601==2.1.0",
"jinja2==3.1.5",
"markdown-it-py==3.0.0",
"markupsafe==3.0.2",
"mdurl==0.1.2",
"mypy-extensions==1.0.0",
"orjson==3.10.14",
"packaging==24.2",
"pathspec==0.12.1",
"platformdirs==4.3.6",
"pycparser==2.22",
"pygments==2.19.1",
"pypika-tortoise==0.3.2",
"python-dotenv==1.0.1",
"python-multipart==0.0.20",
"pytz==2024.2",
"pyyaml==6.0.2",
"rich==13.9.4",
"rich-toolkit==0.13.2",
"shellingham==1.5.4",
"sniffio==1.3.1",
"starlette==0.37.2",
"typer==0.15.1",
"typing-extensions==4.12.2",
"ujson==5.10.0",
# "uvloop==0.21.0",
"watchfiles==1.0.4",
"websockets==14.1",
"pyproject-toml>=0.1.0",
"uvloop==0.21.0 ; sys_platform != 'win32'",
"openai>=1.97.1",
"aiomysql>=0.2.0",
"redis>=6.2.0",
]
[tool.setuptools]
packages = ["app"]
[tool.black]
line-length = 120
target-version = ["py310", "py311"]
[tool.ruff]
line-length = 120
lint.extend-select = []
lint.ignore = [
"F403",
"F405",
]
[tool.aerich]
tortoise_orm = "app.settings.TORTOISE_ORM"
location = "./migrations"
src_folder = "./."

@ -0,0 +1,63 @@
aerich==0.8.1
aiosqlite==0.20.0
annotated-types==0.7.0
anyio==4.8.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
asyncclick==8.1.8
black==24.10.0
certifi==2024.12.14
cffi==1.17.1
click==8.1.8
dictdiffer==0.9.0
dnspython==2.7.0
email-validator==2.2.0
fastapi==0.111.0
fastapi-cli==0.0.7
h11==0.14.0
httpcore==1.0.7
httptools==0.6.4
httpx==0.28.1
idna==3.10
iso8601==2.1.0
isort==5.13.2
jinja2==3.1.5
loguru==0.7.3
markdown-it-py==3.0.0
markupsafe==3.0.2
mdurl==0.1.2
mypy-extensions==1.0.0
orjson==3.10.14
packaging==24.2
passlib==1.7.4
pathspec==0.12.1
platformdirs==4.3.6
pycparser==2.22
pydantic==2.10.5
pydantic-core==2.27.2
pydantic-settings==2.7.1
pygments==2.19.1
pyjwt==2.10.1
pypika-tortoise==0.3.2
python-dotenv==1.0.1
python-multipart==0.0.20
pytz==2024.2
pyyaml==6.0.2
rich==13.9.4
rich-toolkit==0.13.2
ruff==0.9.1
setuptools==75.8.0
shellingham==1.5.4
sniffio==1.3.1
starlette==0.37.2
tortoise-orm==0.23.0
typer==0.15.1
typing-extensions==4.12.2
ujson==5.10.0
uvicorn==0.34.0
uvloop==0.21.0; sys_platform != 'win32'
watchfiles==1.0.4
websockets==14.1
openai==1.97.1
aiomysql==0.2.0
redis==6.2.0

@ -0,0 +1,13 @@
import uvicorn
from uvicorn.config import LOGGING_CONFIG
if __name__ == "__main__":
# 修改默认日志配置
LOGGING_CONFIG["formatters"]["default"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s"
LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
LOGGING_CONFIG["formatters"]["access"][
"fmt"
] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config=LOGGING_CONFIG)

1202
uv.lock

File diff suppressed because it is too large Load Diff

@ -0,0 +1,69 @@
Metadata-Version: 2.4
Name: vue-fastapi-admin
Version: 0.1.0
Summary: Vue Fastapi admin
Author-email: mizhexiaoxiao <mizhexiaoxiao@gmail.com>
Requires-Python: >=3.11
License-File: LICENSE
Requires-Dist: fastapi==0.111.0
Requires-Dist: tortoise-orm==0.23.0
Requires-Dist: pydantic==2.10.5
Requires-Dist: email-validator==2.2.0
Requires-Dist: passlib==1.7.4
Requires-Dist: pyjwt==2.10.1
Requires-Dist: black==24.10.0
Requires-Dist: isort==5.13.2
Requires-Dist: ruff==0.9.1
Requires-Dist: loguru==0.7.3
Requires-Dist: pydantic-settings==2.7.1
Requires-Dist: argon2-cffi==23.1.0
Requires-Dist: pydantic-core==2.27.2
Requires-Dist: annotated-types==0.7.0
Requires-Dist: setuptools==75.8.0
Requires-Dist: uvicorn==0.34.0
Requires-Dist: h11==0.14.0
Requires-Dist: aerich==0.8.1
Requires-Dist: aiosqlite==0.20.0
Requires-Dist: anyio==4.8.0
Requires-Dist: argon2-cffi-bindings==21.2.0
Requires-Dist: asyncclick==8.1.8
Requires-Dist: certifi==2024.12.14
Requires-Dist: cffi==1.17.1
Requires-Dist: click==8.1.8
Requires-Dist: dictdiffer==0.9.0
Requires-Dist: dnspython==2.7.0
Requires-Dist: fastapi-cli==0.0.7
Requires-Dist: httpcore==1.0.7
Requires-Dist: httptools==0.6.4
Requires-Dist: httpx==0.28.1
Requires-Dist: idna==3.10
Requires-Dist: iso8601==2.1.0
Requires-Dist: jinja2==3.1.5
Requires-Dist: markdown-it-py==3.0.0
Requires-Dist: markupsafe==3.0.2
Requires-Dist: mdurl==0.1.2
Requires-Dist: mypy-extensions==1.0.0
Requires-Dist: orjson==3.10.14
Requires-Dist: packaging==24.2
Requires-Dist: pathspec==0.12.1
Requires-Dist: platformdirs==4.3.6
Requires-Dist: pycparser==2.22
Requires-Dist: pygments==2.19.1
Requires-Dist: pypika-tortoise==0.3.2
Requires-Dist: python-dotenv==1.0.1
Requires-Dist: python-multipart==0.0.20
Requires-Dist: pytz==2024.2
Requires-Dist: pyyaml==6.0.2
Requires-Dist: rich==13.9.4
Requires-Dist: rich-toolkit==0.13.2
Requires-Dist: shellingham==1.5.4
Requires-Dist: sniffio==1.3.1
Requires-Dist: starlette==0.37.2
Requires-Dist: typer==0.15.1
Requires-Dist: typing-extensions==4.12.2
Requires-Dist: ujson==5.10.0
Requires-Dist: watchfiles==1.0.4
Requires-Dist: websockets==14.1
Requires-Dist: pyproject-toml>=0.1.0
Requires-Dist: uvloop==0.21.0; sys_platform != "win32"
Dynamic: license-file

@ -0,0 +1,9 @@
LICENSE
README.md
pyproject.toml
app/__init__.py
vue_fastapi_admin.egg-info/PKG-INFO
vue_fastapi_admin.egg-info/SOURCES.txt
vue_fastapi_admin.egg-info/dependency_links.txt
vue_fastapi_admin.egg-info/requires.txt
vue_fastapi_admin.egg-info/top_level.txt

@ -0,0 +1,63 @@
fastapi==0.111.0
tortoise-orm==0.23.0
pydantic==2.10.5
email-validator==2.2.0
passlib==1.7.4
pyjwt==2.10.1
black==24.10.0
isort==5.13.2
ruff==0.9.1
loguru==0.7.3
pydantic-settings==2.7.1
argon2-cffi==23.1.0
pydantic-core==2.27.2
annotated-types==0.7.0
setuptools==75.8.0
uvicorn==0.34.0
h11==0.14.0
aerich==0.8.1
aiosqlite==0.20.0
anyio==4.8.0
argon2-cffi-bindings==21.2.0
asyncclick==8.1.8
certifi==2024.12.14
cffi==1.17.1
click==8.1.8
dictdiffer==0.9.0
dnspython==2.7.0
fastapi-cli==0.0.7
httpcore==1.0.7
httptools==0.6.4
httpx==0.28.1
idna==3.10
iso8601==2.1.0
jinja2==3.1.5
markdown-it-py==3.0.0
markupsafe==3.0.2
mdurl==0.1.2
mypy-extensions==1.0.0
orjson==3.10.14
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.6
pycparser==2.22
pygments==2.19.1
pypika-tortoise==0.3.2
python-dotenv==1.0.1
python-multipart==0.0.20
pytz==2024.2
pyyaml==6.0.2
rich==13.9.4
rich-toolkit==0.13.2
shellingham==1.5.4
sniffio==1.3.1
starlette==0.37.2
typer==0.15.1
typing-extensions==4.12.2
ujson==5.10.0
watchfiles==1.0.4
websockets==14.1
pyproject-toml>=0.1.0
[:sys_platform != "win32"]
uvloop==0.21.0

@ -0,0 +1,3 @@
VITE_TITLE = 'Vue FastAPI Admin'
VITE_PORT = 3100

@ -0,0 +1,8 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# 是否启用代理
VITE_USE_PROXY = true
# base api
VITE_BASE_API = 'http://localhost:9999/api/v1'

@ -0,0 +1,11 @@
# 资源公共路径,需要以 /开头和结尾
VITE_PUBLIC_PATH = '/'
# base api
VITE_BASE_API = '/api/v1'
# 是否启用压缩
VITE_USE_COMPRESS = true
# 压缩类型
VITE_COMPRESS_TYPE = gzip

@ -0,0 +1,62 @@
{
"globals": {
"$loadingBar": true,
"$message": true,
"defineOptions": true,
"$dialog": true,
"$notification": true,
"EffectScope": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

@ -0,0 +1,4 @@
node_modules
dist
public
package.json

26
web/.gitignore vendored

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
stats.html
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
/node_modules/**
/dist/*
/public/*

@ -0,0 +1,6 @@
{
"printWidth": 100,
"singleQuote": true,
"semi": false,
"endOfLine": "lf"
}

@ -0,0 +1,20 @@
## 快速开始
进入前端目录
```sh
cd web
```
安装依赖(建议使用pnpm: https://pnpm.io/zh/installation)
```sh
npm i -g pnpm # 已安装可忽略
pnpm i # 或者 npm i
```
启动
```sh
pnpm dev
```

@ -0,0 +1,13 @@
import dayjs from 'dayjs'
/**
* * 此处定义的是全局常量启动或打包后将添加到window中
* https://vitejs.cn/config/#define
*/
// 项目构建时间
const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss'))
export const viteDefine = {
_BUILD_TIME_,
}

@ -0,0 +1 @@
export * from './define'

@ -0,0 +1,23 @@
export const OUTPUT_DIR = 'dist'
export const PROXY_CONFIG = {
// /**
// * @desc 替换匹配值
// * @请求路径 http://localhost:3100/api/user
// * @转发路径 http://localhost:9999/api/v1 +/user
// */
// '/api': {
// target: 'http://localhost:9999/api/v1',
// changeOrigin: true,
// rewrite: (path) => path.replace(new RegExp('^/api'), ''),
// },
/**
* @desc 不替换匹配值
* @请求路径 http://localhost:3100/api/v1/user
* @转发路径 http://localhost:9999/api/v1/user
*/
'/api/v1': {
target: 'http://127.0.0.1:9999',
changeOrigin: true,
},
}

@ -0,0 +1,15 @@
import { createHtmlPlugin } from 'vite-plugin-html'
export function configHtmlPlugin(viteEnv, isBuild) {
const { VITE_TITLE } = viteEnv
const htmlPlugin = createHtmlPlugin({
minify: isBuild,
inject: {
data: {
title: VITE_TITLE,
},
},
})
return htmlPlugin
}

@ -0,0 +1,35 @@
import vue from '@vitejs/plugin-vue'
/**
* * unocss插件原子css
* https://github.com/antfu/unocss
*/
import Unocss from 'unocss/vite'
// rollup打包分析插件
import visualizer from 'rollup-plugin-visualizer'
// 压缩
import viteCompression from 'vite-plugin-compression'
import { configHtmlPlugin } from './html'
import unplugin from './unplugin'
export function createVitePlugins(viteEnv, isBuild) {
const plugins = [vue(), ...unplugin, configHtmlPlugin(viteEnv, isBuild), Unocss()]
if (viteEnv.VITE_USE_COMPRESS) {
plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' }))
}
if (isBuild) {
plugins.push(
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
)
}
return plugins
}

@ -0,0 +1,46 @@
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
/**
* * unplugin-icons插件自动引入iconify图标
* usage: https://github.com/antfu/unplugin-icons
* 图标库: https://icones.js.org/
*/
import Icons from 'unplugin-icons/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { getSrcPath } from '../utils'
const customIconPath = resolve(getSrcPath(), 'assets/svg')
export default [
AutoImport({
imports: ['vue', 'vue-router'],
dts: false,
}),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath),
},
scale: 1,
defaultClass: 'inline-block',
}),
Components({
resolvers: [
NaiveUiResolver(),
IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }),
],
dts: false,
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__',
}),
]

@ -0,0 +1,15 @@
import { resolve } from 'path'
import chalk from 'chalk'
import { writeFileSync } from 'fs-extra'
import { OUTPUT_DIR } from '../constant'
import { getEnvConfig, getRootPath } from '../utils'
export function runBuildCNAME() {
const { VITE_CNAME } = getEnvConfig()
if (!VITE_CNAME) return
try {
writeFileSync(resolve(getRootPath(), `${OUTPUT_DIR}/CNAME`), VITE_CNAME)
} catch (error) {
console.log(chalk.red('CNAME file failed to package:\n' + error))
}
}

@ -0,0 +1,14 @@
import chalk from 'chalk'
import { runBuildCNAME } from './build-cname'
export const runBuild = async () => {
try {
runBuildCNAME()
console.log(`${chalk.cyan('build successfully!')}`)
} catch (error) {
console.log(chalk.red('vite build error:\n' + error))
process.exit(1)
}
}
runBuild()

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

Loading…
Cancel
Save