签入版本
This commit is contained in:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
DjangoAdmin FastAPI+EleVue is a backend admin framework built with FastAPI (Python), Vue2.x + ElementUI (frontend, separate repo), and MySQL. It provides RBAC-based user/role/menu management, dictionary/config management, and CMS-like modules (notices, ads, members, etc.). The UI language and code comments are in Chinese.
|
||||
DjangoAdmin FastAPI+EleVue is a backend admin framework built with FastAPI (Python), Vue2.x + ElementUI (frontend, separate repo), and MySQL (default; switchable to PostgreSQL / SQL Server / SQLite / Oracle via `DB_DRIVER`). It provides RBAC-based user/role/menu management, dictionary/config management, and content modules (notices, articles, links, etc.). The UI language and code comments are in Chinese.
|
||||
|
||||
## Running the Application
|
||||
|
||||
@@ -13,33 +13,42 @@ DjangoAdmin FastAPI+EleVue is a backend admin framework built with FastAPI (Pyth
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run dev server (default: 127.0.0.1:8031)
|
||||
python main.py
|
||||
python src/main.py
|
||||
|
||||
# Run via uvicorn directly
|
||||
uvicorn main:app --host 127.0.0.1 --port 8031
|
||||
# Run via uvicorn directly (must use --app-dir src, since src/main.py imports
|
||||
# are src/-relative, e.g. `from core.app import create_app`)
|
||||
uvicorn main:app --app-dir src --host 127.0.0.1 --port 8031
|
||||
|
||||
# Docker
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Requires MySQL and Redis running. Configure connection details in `.env` (see `.env` for all variables). The database table prefix is `fastapi_` (set via `DB_PREFIX`).
|
||||
Requires a database (MySQL by default; set `DB_DRIVER=postgresql`/`mssql`/`sqlite`/`oracle` to switch — see `.env` for per-driver examples) and Redis running. Configure connection details in `.env` (see `.env` for all variables). The database table prefix is `fastapi_` (set via `DB_PREFIX`).
|
||||
|
||||
No test suite, linter, or formatter configuration exists in this project.
|
||||
Tests: `pytest` (contract snapshot + unit + integration). Contract snapshot tests (`tests/contract/golden`) guard route paths, permission strings, and table names; run with `pytest` (integration tests require real MySQL/Redis and are skipped by default, see `pytest.ini`). No linter or formatter configuration exists.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Request Flow
|
||||
|
||||
`main.py` -> `apps.create_app()` -> registers CORS, Redis, routers, middleware, exception handlers -> all routes go through `login_required` dependency (except `/login` and `/captcha`) -> `permission_required` decorator checks node-level RBAC per endpoint -> `check_demo` decorator blocks write operations in demo mode.
|
||||
`src/main.py` -> `src/core/app.py:create_app()` -> registers CORS, Redis, routers, middleware, exception handlers -> `src/api/__init__.py:register_router` mounts everything under the `/api/v1` prefix (login/captcha unauthenticated on app, v1 behind `login_required`; the frontend VITE_PROXY targets `http://127.0.0.1:8031/api/v1`) -> `permission_required` decorator checks node-level RBAC per endpoint -> `check_demo` decorator blocks write operations in demo mode.
|
||||
|
||||
### Layered Module Pattern
|
||||
|
||||
Each business module (e.g., level, position, dept, role, user, menu, notice, dicts, config, etc.) follows the same 4-file pattern under `apps/`:
|
||||
HTTP layer and business layer are separated. Each business module (e.g., level, position, dept, role, user, menu, notice, dicts, config, etc.) follows the same pattern:
|
||||
|
||||
- **`views/{module}.py`** - FastAPI router with endpoint definitions. Uses `@permission_required("sys:{module}:{action}")` and `@check_demo` decorators. Returns `R.ok()`/`R.failed()`. Endpoints that accept a body declare a Pydantic form parameter (e.g., `data: PositionForm`) — FastAPI parses and validates the JSON automatically before the handler runs.
|
||||
- **`services/{module}.py`** - Business logic: queries, CRUD operations, Excel import/export. Uses `db.query(Model)` directly with SQLAlchemy. Reads query params via `request.query_params`; body params arrive as already-validated Pydantic models, so services access fields via `data.field` or `data.model_dump()` (no `await request.json()` in service bodies).
|
||||
- **`forms/{module}.py`** - Pydantic `BaseModel` validation classes. Fields use `Field(...)` for constraints (`min_length`, `max_length`, `ge`, `le`, `gt`). Custom validators use `@field_validator('field')` raising `ValueError(msg)`. Uniqueness checks query `db.query(Model).filter(...).first()` inside validators.
|
||||
- **`models/{module}.py`** - SQLAlchemy ORM model inheriting `base_model` (provides id, create_user, create_time, update_user, update_time, is_delete) and `base_db` (provides save(), save_all(), delete()). Table name uses `DB_PREFIX` prefix.
|
||||
- **`src/api/v1/endpoints/{name}.py`** - HTTP layer: FastAPI router with endpoint definitions. Uses `@permission_required("sys:{module}:{action}")` and `@check_demo` decorators. Returns `R.ok()`/`R.failed()`. Endpoints that accept a body declare a Pydantic form parameter (e.g., `data: LevelForm`) — FastAPI parses and validates the JSON automatically before the handler runs. Handlers delegate to `{module}.service`; no business logic lives here.
|
||||
- **`src/modules/{group}/{name}/models.py`** - SQLAlchemy ORM model inheriting `base_model` (provides id, create_user, create_time, update_user, update_time, is_delete) and `base_db` (provides `save()`). Table name uses `DB_PREFIX` prefix.
|
||||
- **`src/modules/{group}/{name}/repository.py`** - Data access layer: subclass `BaseRepository[Model]` (from `src/core/base_repository.py`) + module-level singleton instance `{name}_repo`. Common operations (get_by_id, get_by_ids, get_one, get_all, exists, exists_by_field, count, filter, filter_by, create, update, paginate, batch_delete [软删,纯数据原语,返回删除条数]) come from the base; subclasses add custom queries. 等值查询类(get_one/get_all/count/filter_by/exists_by_field)自动软删过滤且跳过 None 值;`filter(*criterion)` 为原始出口(不自动软删,供 in_/and_/区间等复杂条件),需调用方自行追加 `is_delete==0`。Repository层不做 R 封装——删除的 R 响应由 service 层 `BaseService.batch_delete_with_r(repo, ids)`(静态方法)统一产出。
|
||||
- **`src/modules/{group}/{name}/schemas.py`** - Pydantic `BaseModel` validation classes. Fields use `Field(...)` for constraints (`min_length`, `max_length`, `ge`, `le`, `gt`). Custom validators use `@field_validator('field')` raising `ValueError(msg)`. Uniqueness checks query `db.query(Model).filter(...).first()` inside validators.
|
||||
- **`src/modules/{group}/{name}/service.py`** - Business logic. Simple CRUD modules subclass `BaseService[Model]` (from `src/core/base_service.py`) + module-level singleton `{name}_service`: declare differences (`repo`/`model`/`page_like_fields`/`page_eq_fields`/`page_order_by`/`unique_fields`/`serialize_maps`) and override hooks (`_apply_page_filters` for fixed filters, `_serialize`/`_serialize_detail` for payload shaping, `_build_create_fields`/`_build_update_fields` for field assembly). Complex modules (user/role/menu/dept/article/category/job/city) keep custom service functions. For delete, custom services reuse `BaseService.batch_delete_with_r(repo, ids)` (静态方法,软删 + 统一 R 文案) instead of calling `repo.batch_delete` directly. 简单等值查询(唯一性校验、按字段取记录)优先用 `repo.get_one(...)`/`repo.exists_by_field(field, value, exclude_id)`/`repo.filter_by(...)` 等基类方法,不再手写 `repo.filter(Model.is_delete == 0, ...)` 原始查询。Body params arrive as already-validated Pydantic models, so services access fields via `data.field` or `data.model_dump()` (no `await request.json()` in service bodies).
|
||||
|
||||
### 同步/异步约定
|
||||
|
||||
- **service 需要 `await`**(Redis 缓存读写/失效、`request.json()`、异步文件读取等)→ 用 `async def`,其中 DB/磁盘等同步操作一律用 `asyncio.to_thread` 包裹;对应端点为 `async def` + `await`。
|
||||
- **纯 DB CRUD**(BaseService 简单模块及自定义同步 service)→ 保持同步 `def`;FastAPI 自动在线程池执行,不阻塞事件循环。
|
||||
- **禁止**在 `async def` 端点或 service 内直接调用同步 DB 查询(会阻塞事件循环)。
|
||||
- 批量删除端点需 `await parse_batch_ids(request)`(异步读请求体),故 `batch_delete` 端点恒为 `async def`,属正常差异。
|
||||
|
||||
### Request Validation
|
||||
|
||||
@@ -53,20 +62,33 @@ async def add(request: Request, data: LevelForm):
|
||||
return await level.LevelAdd(request, data)
|
||||
```
|
||||
|
||||
Validation errors are caught by a global `RequestValidationError` handler registered in `core/app.py` and converted to the standard response shape `{"code": -1, "data": null, "msg": "field: message/..."}` with HTTP 200. Services receive the validated Pydantic instance and use `data.model_dump()` (full dict), `data.model_dump(exclude={'id'})` (for updates), or direct attribute access (`data.id`, `data.status`).
|
||||
Validation errors are caught by a global `RequestValidationError` handler registered in `core/app.py` and converted to the standard response shape `{"code": 1, "data": null, "msg": "field: message/..."}` with HTTP 200. Services receive the validated Pydantic instance and use `data.model_dump()` (full dict), `data.model_dump(exclude={'id'})` (for updates), or direct attribute access (`data.id`, `data.status`).
|
||||
|
||||
### Key Directories
|
||||
|
||||
- **`config/`** - Configuration split into sub-modules: `app.py` (app settings, CORS, upload limits), `database.py` (DB connection, pool config, `SQLALCHEMY_MYSQL_URL`), `redis.py`, `auth.py` (JWT), `captcha.py`, `email.py`, `sms.py`. All loaded from `.env` via `python-dotenv`. `config/__init__.py` loads `.env` first, then re-exports everything.
|
||||
- **`core/`** - Core infrastructure: `database.py` (engine/SessionLocal/`_db_ctx`/`get_db_session()`/`commit_independently`/`Base`), `redis_client.py` (`get_redis()`/`redis_lifespan`/`redis_middleware`), `jwt.py` (JWT create/parse/refresh/blacklist with HS256), `password.py` (bcrypt hashing with salt), `response.py` (`R.ok()`/`R.failed()`), `exceptions.py` (AuthorizationException/BusinessException), `security.py` (`get_user_id`/`get_username`), `middleware.py` (`login_required`/`permission_required`/`check_demo`/`db_session_middleware`/`operation_log_middleware`/`register_cros`), `deps.py` (DI), `logger.py` (structured logging), `app.py` (create_app factory + lifespan).
|
||||
- **`shared/`** - Cross-module shared layer: `enums.py` (enum dicts), `constants.py` (PAGE_SIZE/DEFAULT_PASSWORD/SERIALIZE_EXCLUDE_FIELDS), `pagination.py`, `base_model.py`, `base_db.py`, `base_repository.py` (Generic repository base), `base_service.py`.
|
||||
- **`modules/`** - Business modules, each a directory with `router.py` (may export multiple routers, e.g. `dict_router` + `dict_item_router`), `schemas.py`, `service.py`, `repository.py`, `models.py`. Module groups: `auth/`, `system/` (user, role, menu, dept, position, level), `dictionary/`, `configuration/`, `cms/` (item, item_cate, ad, ad_sort), `member/`, `region/`, `link/`, `notice/`, `job/` (job + job_log + scheduler + job_executor), `log/`, `upload/`, `index/`.
|
||||
- **`routers/`** - `v1.py` central route registration (imports routers from `modules/`), `__init__.py` mounts login (免认证) on app and v1 behind `login_required`.
|
||||
- **`utils/`** - Remaining utilities that stay here: `request` (`parse_batch_ids`, `like_escape`, `like_contains`), `captcha`, `file`, `ip`/`ip2region`, `ua`, `rich_text`, `perm_cache`. `R`/`password` are re-exported from `core` via `utils/__init__.py` for back-compat.
|
||||
- **`src/core/config/`** - Configuration split into sub-modules: `app.py` (app settings, CORS, upload limits), `database.py` (DB connection, driver registry via `DB_DRIVER`, pool config, active URL `SQLALCHEMY_DATABASE_URL`), `redis.py`, `auth.py` (JWT), `captcha.py`, `email.py`, `sms.py`. All loaded from `.env` via `python-dotenv`. `config/__init__.py` loads `.env` first, then re-exports everything.
|
||||
- **`src/core/`** - Core infrastructure: `database.py` (engine/SessionLocal/`_db_ctx`/`get_db_session()`/`commit_independently`/`Base`), `redis_client.py` (`get_redis()`/`redis_lifespan`/`redis_middleware`), `jwt.py` (JWT create/parse/refresh/blacklist with HS256), `password.py` (bcrypt hashing with salt), `response.py` (`R.ok()`/`R.failed()`), `exceptions.py` (AuthorizationException/BusinessException), `security.py` (`get_user_id`/`get_username`), `access_decorators.py` (`permission_required`/`check_demo` decorators, from the former `core/middleware.py`), `logger.py` (structured logging), `app.py` (create_app factory + lifespan). Also hosts `base_model.py`/`base_db.py`/`base_repository.py` (moved from the former `shared/`) and `base_service.py` (generic CRUD service base with overridable hooks; the 10 simple modules subclass it).
|
||||
- **`src/constants/`** - Constants & enums (split out from the former `shared/`): `constants.py` (PAGE_SIZE/DEFAULT_PASSWORD/MESSAGE_OK/SERIALIZE_EXCLUDE_FIELDS), `enums.py` (enum dicts), `pagination.py`. Re-exported via `src/constants/__init__.py`.
|
||||
- **`src/modules/`** - Business modules (business layer only), each a directory with `models.py`, `repository.py`, `schemas.py`, `service.py`. Module groups: `auth/`, `system/` (user, role, menu, dept, position, level), `dictionary/`, `configuration/`, `file_template/`, `category/`, `article/`, `city/`, `link/`, `notice/`, `param/`, `job/` (job + job_log + scheduler + job_executor), `login_log/`, `operation_log/`, `upload/`.
|
||||
- **`src/api/`** - Route assembly (from the former `routers/` + `core/deps.py`): `__init__.py` (`register_router` 统一把全部路由挂到 `/api/v1` 前缀下:auth 路由免认证挂 app,v1 路由带 `login_required` 挂 app), `deps.py` (`login_required` auth dependency), `v1/router.py` (central route registration, fixed prefixes per module), `v1/endpoints/` (per-module HTTP layer).
|
||||
- **`src/utils/`** - Utilities: `request.py` (`parse_batch_ids`, `like_escape`, `like_contains`, `parse_id_list`), `captcha`, `file`, `ip`/`ip2region`, `ua`, `rich_text`, `perm_cache`. `R`/`password` are re-exported from `core` via `src/utils/__init__.py` for back-compat.
|
||||
- **`src/middleware/`** - Generic HTTP middlewares (all moved from `core/middleware.py`): `cors.py` (`register_cors`), `rate_limit.py` (per-IP Redis sliding-window limiter, opt-in via `RATE_LIMIT_ENABLED`; on exceed returns standard `{code:1}` 200 response directly, since `BaseHTTPMiddleware` exceptions bypass global handlers), `db_session.py` (request-scoped DB session, commits/rolls back per business success), `operation_log.py` (auto-records write-operation logs, lazy-imports the service), `logger.py` (`register_middleware` empty request hook). Registered in `core/app.py` in order: CORS → redis → rate_limit → db_session → operation_log → logger hook.
|
||||
- **`migrations/`** - Alembic migrations. `alembic.ini` + `migrations/env.py` (injects `src/`, loads active `SQLALCHEMY_DATABASE_URL` from `core.config`, `create_app()` to fill `Base.metadata`) + `versions/0001_baseline.py` (baseline = `Base.metadata.create_all`). Fresh installs: `python scripts/init_db.py` (auto-stamps head, driver-aware per `DB_DRIVER`) or `make db-init`. Future schema changes: `alembic revision --autogenerate` -> `alembic upgrade head` (see Makefile `migrate-*` targets). Requirements: `alembic>=1.13.0`.
|
||||
|
||||
### Database Access
|
||||
|
||||
Database sessions are request-scoped via `contextvars` + Starlette middleware (`core/middleware.py: db_session_middleware`). Services call `db = get_db_session()` (from `core.database`) to get the current request's session. Models use `self.save()` / `self.delete()` from `shared/base_db.py`. The middleware auto-closes the session after each request (with rollback). Soft deletes via `is_delete` flag (0=active, 1=deleted).
|
||||
Database sessions are request-scoped via `contextvars` + Starlette middleware (`src/middleware/db_session.py: db_session_middleware`). Services call `db = get_db_session()` (from `core.database`) to get the current request's session. Models use `self.save()` / `self.delete()` from `src/core/base_db.py`. The middleware auto-closes the session after each request (with rollback). Soft deletes via `is_delete` flag (0=active, 1=deleted).
|
||||
|
||||
### Cross-Database Migration(跨库迁移)
|
||||
|
||||
把源库(通常 MySQL)的「结构 + 数据」整体迁到目标库(PostgreSQL / SQL Server / SQLite),补上多驱动接入后缺的数据迁移与整体流程。结构可移植已由 `Base.metadata.create_all()` / alembic 打通(方言 DDL),`scripts/migrate_db.py` 负责数据泵 + 端到端操作。
|
||||
|
||||
- **`scripts/migrate_db.py`** - CLI 驱动,`--src-*`/`--dst-*` 显式连接参数(**不读 `.env`**)。流程:`create_database_if_missing()`(镜像 `init_db.py` 的维护库建库分支:mysql 无库连接 / pg 连 `postgres` / mssql 连 `master`)→ 目标端 `Base.metadata.create_all()` 建结构(模型即 head,含 0002 的 `city.parent_code nullable`)→ 泵数据 → 重置自增序列 → 逐表 `COUNT(*)` 校验。
|
||||
- **数据泵**:拓扑序(基础表 dept/menu/role/level/position/city/category/dict/config/file_template/param/job/link 在前,依赖表 role_menu/dict_item/config_item/job_log/user/user_role/article/notice/operation_log/login_log 在后)+ 每表 `SELECT * ... ORDER BY id` + 分块 executemany **显式带 id 插入**,保住无外键裸列引用(`user.dept_id`、`role_menu.role_id/menu_id`、`menu.parent_id` 等)。`sanitize()` 防御性把零日期 `0000-00-00*` 统一转 None。
|
||||
- **自增序列重置**:PG `setval(pg_get_serial_sequence(...))`;MSSQL 需 `SET IDENTITY_INSERT ON/OFF` 包住显式插 id + `DBCC CHECKIDENT` 重置;MySQL/SQLite 显式插 id 后自动跟随 `MAX(id)`,免处理。
|
||||
- **用法**:`make db-migrate-dry-run ARGS="..."` 干跑核对计划 → `make db-migrate ARGS="... --drop-target-first"`(加 `--drop-target-first` 可幂等重跑)。完整操作手册 + 目标端校验清单见 `document/数据库迁移_runbook.md`。
|
||||
- **测试**:`tests/unit/scripts/test_migrate_db.py`(sqlite 同时充当源/目标双库,无需真实 MySQL;9 例覆盖 id/引用保真、分块边界、only-tables、dry-run、PG/MSSQL 重置 SQL、count 不一致上报)。
|
||||
- 常用选项:`--dry-run` / `--only-tables`(续传)/ `--skip-existing` / `--single-transaction` / `--skip-verify` / `--chunk-size`。
|
||||
|
||||
### Redis Access
|
||||
|
||||
@@ -84,7 +106,7 @@ Redis uses async client via `core/redis_client.py`. Connection pool created in `
|
||||
All endpoints return `JSONResponse` via `R.ok()` or `R.failed()`:
|
||||
```json
|
||||
{"code": 0, "data": ..., "msg": "操作成功", "ok": true} // success
|
||||
{"code": -1, "data": null, "msg": "错误信息", "ok": false} // failure
|
||||
{"code": 1, "data": null, "msg": "错误信息", "ok": false} // failure
|
||||
```
|
||||
`R.ok()` supports `**kwargs` for extra fields (e.g., `R.ok(data=result, count=total)` adds a `count` field).
|
||||
|
||||
@@ -98,17 +120,17 @@ Single delete: `DELETE /delete/{id}` where id can be comma-separated (e.g., `"1,
|
||||
|
||||
## Adding a New Module
|
||||
|
||||
1. Create `modules/{group}/{name}/models.py` - model class inheriting `base_model` and `base_db` (from `shared/`), set `__tablename__ = DB_PREFIX + "{name}"`
|
||||
2. Create `modules/{group}/{name}/schemas.py` - Pydantic `BaseModel` validation classes with `Field(...)` constraints and `@field_validator` for custom checks
|
||||
3. Create `modules/{group}/{name}/repository.py` - subclass `shared.base_repository.BaseRepository` + module-level instance
|
||||
4. Create `modules/{group}/{name}/service.py` - CRUD service functions following the `get_level_page`/`get_level_detail`/`add_level`/`update_level`/`delete_level`/`update_level_status` pattern
|
||||
5. Create `modules/{group}/{name}/router.py` - APIRouter with endpoints, apply `@permission_required` and `@check_demo` on write endpoints
|
||||
6. Register the router in `routers/v1.py` - `v1.include_router({name}_router, prefix="/{prefix}", tags=["标签"])`
|
||||
1. Create `src/modules/{group}/{name}/models.py` - model class inheriting `base_model` and `base_db` (from `src/core/`), set `__tablename__ = DB_PREFIX + "{name}"`
|
||||
2. Create `src/modules/{group}/{name}/schemas.py` - Pydantic `BaseModel` validation classes with `Field(...)` constraints and `@field_validator` for custom checks
|
||||
3. Create `src/modules/{group}/{name}/repository.py` - subclass `core.base_repository.BaseRepository` + module-level instance
|
||||
4. Create `src/modules/{group}/{name}/service.py` - for simple CRUD, subclass `core.base_service.BaseService[Model]` declaring differences (`repo`/`model`/`page_like_fields`/`page_eq_fields`/`page_order_by`/`unique_fields`/`serialize_maps`) + module-level singleton `{name}_service`; override hooks for special business. For complex modules, write custom service functions instead (see `src/modules/system/user/service.py`)
|
||||
5. Create `src/api/v1/endpoints/{name}.py` - APIRouter with endpoints, apply `@permission_required` and `@check_demo` on write endpoints, delegate logic to the service
|
||||
6. Register the router in `src/api/v1/router.py` - `v1.include_router({name}_router, prefix="/{prefix}", tags=["标签"])`
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- FastAPI 0.140.0, Uvicorn 0.51.0
|
||||
- SQLAlchemy 2.0.51 (declarative style with `declarative_base()`, not 2.0 `mapped_column` style)
|
||||
- PyMySQL 1.2.0, Redis 8.0.1 (async), PyJWT 2.13.0
|
||||
- DB drivers: PyMySQL 1.2.0 (MySQL), psycopg[binary] (PostgreSQL), pymssql (SQL Server), oracledb (Oracle); Redis 8.0.1 (async), PyJWT 2.13.0
|
||||
- Pydantic 2.13.4 (for request validation via BaseModel + Field + field_validator), openpyxl (for Excel import/export)
|
||||
- python-dotenv for `.env` loading, bcrypt for password hashing, Pillow for captcha generation
|
||||
|
||||
Reference in New Issue
Block a user