Files
DjangoAdmin_FastAPI_EleVue/CLAUDE.md
T
2026-09-12 17:39:13 +08:00

137 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
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
```bash
# Install dependencies
pip install -r requirements.txt
# Run dev server (default: 127.0.0.1:8031)
python src/main.py
# 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 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`).
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
`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
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:
- **`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
Pydantic v2 is used for all request body validation. The pattern for an endpoint:
```python
@router.post('/add')
@permission_required("sys:level:add")
@check_demo
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`).
### Key Directories
- **`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 (`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
Redis uses async client via `core/redis_client.py`. Connection pool created in `redis_lifespan` (composed with scheduler startup in `core/app.py` lifespan), injected per-request via `redis_middleware` into contextvars. Business code calls `get_redis()` to get the current request's async Redis client.
### Authentication & Authorization
- JWT tokens via `core/jwt.py` (HS256, configurable expiry via JWT_EXPIRE_MINUTES, secret via JWT_SALT env var). Token sent as `Authorization: Bearer <token>` header. Supports token refresh and blacklist (logout) via Redis.
- `login_required` is a FastAPI `Depends` applied globally to the v1 router. On success, it sets `request.state.user_id`, `request.state.username`, `request.state.realname`.
- `permission_required` is a per-endpoint decorator checking permission strings like `"sys:level:list"`. User ID 1 (admin) bypasses all permission checks. It lazily queries `modules.system.menu.service.get_permissions_list_sync`.
- `check_demo` is a per-endpoint decorator that returns `R.failed("演示环境,暂无操作权限")` when `FASTAPI_DEMO=True`. Apply it after `@permission_required` on all write endpoints (add, update, delete, status, import, export).
### API Response Format
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
```
`R.ok()` supports `**kwargs` for extra fields (e.g., `R.ok(data=result, count=total)` adds a `count` field).
### Pagination Pattern
List endpoints read `pageNo` and `pageSize` from `request.query_params`, defaulting to page 1 and `PAGE_SIZE` (10). Services use SQLAlchemy `.limit(limit).offset((page - 1) * limit).all()` and return `R.ok(data=result, count=count)`.
### Delete Pattern
Single delete: `DELETE /delete/{id}` where id can be comma-separated (e.g., `"1,2,3"`). Batch delete: `DELETE /batchDelete` with JSON body `[1, 2, 3]`, parsed via `parse_batch_ids(request)`. Both use soft delete (set `is_delete = 1`).
## Adding a New Module
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)
- 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